@@ -44,20 +44,110 @@ public List<RecommendPredictDto> getRecommendList(Member member) {
4444 LocalDate targetDate = now .isBefore (LocalTime .of (6 , 0 )) ? today .minusDays (1 ) : today ;
4545
4646 List <ClothingRecommendation > modelPredictList = getModelPrediction (member .getId (), targetDate );
47-
48- Closet closet = getClosetWithAll (member );
4947 List <RecommendPredictDto > result = new ArrayList <>();
50- for (ClothingRecommendation recommendation : modelPredictList ) {
51- RecommendPredictDto dto = makeResultForPredict (closet , recommendation , member );
48+ Closet closet = getClosetWithAll (member );
49+ if (!modelPredictList .isEmpty ()) {
50+ log .info ("[AI 추천] memberId={} AI 예측 결과 사용 ({}개)" , member .getId (), modelPredictList .size ());
51+ for (ClothingRecommendation recommendation : modelPredictList ) {
52+ RecommendPredictDto dto = makeResultForPredict (closet , recommendation , member );
53+ boolean isUppersExist = dto .getUppersTypeList () != null && !dto .getUppersTypeList ().isEmpty ();
54+ boolean isFeelingExist = dto .getFeelingList () != null && !dto .getFeelingList ().isEmpty ();
55+ if (isUppersExist && isFeelingExist ) {
56+ result .add (dto );
57+ }
58+ }
59+ } else {
60+ result .addAll (generateFallbackRecommendation (member , targetDate , closet ));
61+ }
5262
53- boolean isUppersExist = dto .getUppersTypeList () != null && !dto .getUppersTypeList ().isEmpty ();
54- boolean isFeelingExist = dto .getFeelingList () != null && !dto .getFeelingList ().isEmpty ();
63+ return result ;
64+ }
65+
66+ private List <RecommendPredictDto > generateFallbackRecommendation (Member member , LocalDate targetDate , Closet closet ) {
67+ log .warn ("[Fallback 추천] memberId={} AI 예측 결과 없음. 대체 추천 로직 시작." , member .getId ());
68+ List <RecommendPredictDto > fallbackResult = new ArrayList <>();
5569
56- if (isUppersExist && isFeelingExist ) {
57- result .add (dto );
70+ try {
71+ final Long MIN_OUTER_TYPE = 10L ;
72+ final Long MAX_OUTER_TYPE = 25L ;
73+ final Long MIN_UPPER_TYPE = 3L ;
74+ final Long MAX_UPPER_TYPE = 9L ;
75+
76+ List <Integer > fallbackOuterTypes = closet .getOuterList ().stream ()
77+ .filter (Outer ::isActive )
78+ .filter (outer -> outer .getOuterType () != null &&
79+ outer .getOuterType () >= MIN_OUTER_TYPE &&
80+ outer .getOuterType () <= MAX_OUTER_TYPE )
81+ .map (outer -> outer .getOuterType ().intValue ())
82+ .distinct ()
83+ .collect (Collectors .toList ());
84+
85+ List <Integer > fallbackUpperTypes = closet .getUpperList ().stream ()
86+ .filter (Upper ::isActive )
87+ .filter (upper -> upper .getUpperType () != null &&
88+ upper .getUpperType () >= MIN_UPPER_TYPE &&
89+ upper .getUpperType () <= MAX_UPPER_TYPE )
90+ .map (upper -> upper .getUpperType ().intValue ())
91+ .distinct ()
92+ .collect (Collectors .toList ());
93+
94+ if (fallbackOuterTypes .isEmpty () && fallbackUpperTypes .isEmpty ()) {
95+ log .info ("[Fallback 추천] memberId={} 추천할 활성 상의 또는 아우터 없음 (기준: Upper {}-{}, Outer {}-{}). 예외 발생." ,
96+ member .getId (), MIN_UPPER_TYPE , MAX_UPPER_TYPE , MIN_OUTER_TYPE , MAX_OUTER_TYPE );
97+ throw new CustomException (ErrorCode .NO_PREDICT_DATA );
5898 }
99+
100+ List <WeatherFeelingDto > fallbackFeelingList = createFallbackFeelingList (member , targetDate );
101+
102+ RecommendPredictDto fallbackDto = RecommendPredictDto .builder ()
103+ .feelingList (fallbackFeelingList )
104+ .uppersTypeList (fallbackUpperTypes )
105+ .outersTypeList (fallbackOuterTypes )
106+ .build ();
107+ fallbackResult .add (fallbackDto );
108+ log .info ("[Fallback 추천] memberId={} 최종 추천 결과: Uppers={}, Outers={}, FeelingsGenerated={}" ,
109+ member .getId (), fallbackUpperTypes , fallbackOuterTypes , !fallbackFeelingList .isEmpty ());
110+
111+ } catch (CustomException e ) {
112+ log .error ("[Fallback 추천 실패] memberId={} 처리 중 Custom 오류: {}" , member .getId (), e .getMessage ());
113+ throw new CustomException (ErrorCode .NO_PREDICT_DATA );
114+ } catch (Exception e ) {
115+ log .error ("[Fallback 추천 실패] memberId={} 알 수 없는 오류 발생: {}" , member .getId (), e .getMessage (), e );
116+ throw new CustomException (ErrorCode .NO_PREDICT_DATA );
59117 }
60- return result ;
118+
119+ return fallbackResult ;
120+ }
121+
122+ private List <WeatherFeelingDto > createFallbackFeelingList (Member member , LocalDate targetDate ) {
123+ List <WeatherFeelingDto > fallbackFeelingList = new ArrayList <>();
124+ String regionName = member .getRegionName () != null ? member .getRegionName () : "서울특별시 용산구" ;
125+ List <Integer > targetHours = List .of (9 , 12 , 15 , 18 , 21 );
126+ final int DEFAULT_FEELING = 2 ;
127+
128+ try {
129+ List <WeatherForecast > forecasts = weatherForecastRepository
130+ .findByRegionNameAndForecastDateAndHourInOrderByHourAsc (regionName , targetDate , targetHours );
131+
132+ if (!forecasts .isEmpty ()) {
133+ for (WeatherForecast forecast : forecasts ) {
134+ WeatherFeelingDto dto = WeatherFeelingDto .builder ()
135+ .date (forecast .getForecastDate ())
136+ .time (forecast .getHour ())
137+ .feeling (DEFAULT_FEELING )
138+ .temperature (forecast .getTemperature ())
139+ .build ();
140+ fallbackFeelingList .add (dto );
141+ }
142+ log .info ("[Fallback 추천] memberId={} 기본 체감온도(2)로 그래프 생성 완료 ({}개 시간대)" , member .getId (), fallbackFeelingList .size ());
143+ } else {
144+ log .warn ("[Fallback 추천] memberId={} 날씨 예보 데이터가 없어 체감온도 그래프를 생성할 수 없습니다. region={}, date={}, hours={}" ,
145+ member .getId (), regionName , targetDate , targetHours );
146+ }
147+ } catch (Exception e ) {
148+ log .error ("[Fallback 추천] memberId={} 날씨 예보 조회 중 오류 발생: {}" , member .getId (), e .getMessage (), e );
149+ }
150+ return fallbackFeelingList ;
61151 }
62152
63153 private RecommendPredictDto makeResultForPredict (Closet closet , ClothingRecommendation recommendation , Member member ) {
@@ -83,7 +173,9 @@ private List<Integer> makeOuterList(Closet closet, List<Integer> outers) {
83173 if (outers .isEmpty ()) return new ArrayList <>();
84174
85175 Set <Long > ownedClothTypes = closet .getOuterList ().stream ()
176+ .filter (Outer ::isActive )
86177 .map (Outer ::getOuterType )
178+ .filter (Objects ::nonNull )
87179 .collect (Collectors .toSet ());
88180
89181 Set <Integer > resultSet = new HashSet <>();
@@ -101,7 +193,9 @@ private List<Integer> makeOuterList(Closet closet, List<Integer> outers) {
101193
102194 private List <Integer > makeUpperList (Closet closet , List <Integer > tops ) {
103195 Set <Long > ownedClothTypes = closet .getUpperList ().stream ()
196+ .filter (Upper ::isActive )
104197 .map (Upper ::getUpperType )
198+ .filter (Objects ::nonNull )
105199 .collect (Collectors .toSet ());
106200
107201 Set <Integer > resultSet = new HashSet <>();
@@ -119,52 +213,42 @@ private List<Integer> makeUpperList(Closet closet, List<Integer> tops) {
119213 private List <WeatherFeelingDto > makeWeatherFeeling (Map <String , Integer > predictionMap ,
120214 ClothingRecommendation recommendation ) {
121215 List <WeatherFeelingDto > feelingList = new ArrayList <>();
122-
123216 LocalDate forecastDate = recommendation .getDate ();
124-
125217 String regionName = recommendation .getRegionName ();
126218
127- List <Integer > hours = predictionMap .keySet ().stream ()
128- .map (Integer ::parseInt )
129- .collect (Collectors .toList ());
219+ List <Integer > targetHours = List .of (9 , 12 , 15 , 18 , 21 );
130220
131221 List <WeatherForecast > forecasts = weatherForecastRepository
132- .findByRegionNameAndForecastDateAndHourInOrderByCreatedAtDesc (regionName , forecastDate , hours );
133-
134- Map <Integer , WeatherForecast > hourToForecastMap = forecasts .stream ()
135- .collect (Collectors .toMap (
136- WeatherForecast ::getHour ,
137- forecast -> forecast ,
138- (oldVal , newVal ) -> oldVal
139- ));
140-
141- for (Map .Entry <String , Integer > entry : predictionMap .entrySet ()) {
142- int hour = Integer .parseInt (entry .getKey ());
143- int feeling = entry .getValue ();
144- WeatherForecast forecast = hourToForecastMap .get (hour );
145-
146- if (forecast != null ) {
147- WeatherFeelingDto dto = WeatherFeelingDto .builder ()
148- .date (forecast .getForecastDate ())
149- .time (hour )
150- .feeling (feeling )
151- .temperature (forecast .getTemperature ())
152- .build ();
153- feelingList .add (dto );
154- } else {
155- log .warn ("날씨 데이터 없음: region={}, date={}, hour={}" , regionName , forecastDate , hour );
222+ .findByRegionNameAndForecastDateAndHourInOrderByHourAsc (regionName , forecastDate , targetHours );
223+
224+ Map <String , Integer > safePredictionMap = Optional .ofNullable (predictionMap ).orElseGet (Collections ::emptyMap );
225+ final int DEFAULT_FEELING = 2 ;
226+
227+ for (WeatherForecast forecast : forecasts ) {
228+ int hour = forecast .getHour ();
229+ int feeling = safePredictionMap .getOrDefault (String .valueOf (hour ), DEFAULT_FEELING );
230+
231+ WeatherFeelingDto dto = WeatherFeelingDto .builder ()
232+ .date (forecast .getForecastDate ())
233+ .time (hour )
234+ .feeling (feeling )
235+ .temperature (forecast .getTemperature ())
236+ .build ();
237+ feelingList .add (dto );
238+
239+ if (feeling == DEFAULT_FEELING && !safePredictionMap .containsKey (String .valueOf (hour ))) {
240+ log .debug ("AI 체감온도 예측값 없음. 기본값(2) 사용: region={}, date={}, hour={}" , regionName , forecastDate , hour );
156241 }
157242 }
243+
244+ if (forecasts .isEmpty ()) {
245+ log .warn ("날씨 예보 데이터 없음: region={}, date={}, hours={}" , regionName , forecastDate , targetHours );
246+ }
158247 return feelingList ;
159248 }
160249
161-
162250 private List <ClothingRecommendation > getModelPrediction (Long id , LocalDate now ) {
163- List <ClothingRecommendation > list = clothingRecommendationRepository .findByMemberIdAndDate (id , now );
164- if (list .isEmpty ()) {
165- throw new CustomException (ErrorCode .NO_PREDICT_DATA );
166- }
167- return list ;
251+ return clothingRecommendationRepository .findByMemberIdAndDate (id , now );
168252 }
169253
170254 @ Transactional
@@ -196,7 +280,6 @@ public void save(ModelClothingRecommendationDto dto, Member member) {
196280 }
197281 }
198282
199-
200283 private Closet getClosetWithAll (Member member ) {
201284 Closet closetWithUppers = closetRepository .findClosetWithUppers (member .getId ())
202285 .orElseThrow (() -> new CustomException (ErrorCode .CLOSET_NOT_FOUND ));
0 commit comments