diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelScheduler.java b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelScheduler.java index d4a58d7..8fe570e 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelScheduler.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelScheduler.java @@ -43,7 +43,7 @@ public class ModelScheduler { * 매일 새벽 5시에 모델 서버로 예측에 필요한 데이터를 암호화된 형태로 전송합니다. */ @Transactional - @Scheduled(cron = "0 10 16 * * *") + @Scheduled(cron = "0 0 5 * * *") public void pushPredictionAESDataToAiServer() { try { List members = memberRepository.findAllByIsDeletedFalse(); diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/RecommendationService.java b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/RecommendationService.java index c0e1bbc..19606be 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/RecommendationService.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/RecommendationService.java @@ -44,20 +44,110 @@ public List getRecommendList(Member member) { LocalDate targetDate = now.isBefore(LocalTime.of(6, 0)) ? today.minusDays(1) : today; List modelPredictList = getModelPrediction(member.getId(), targetDate); - - Closet closet = getClosetWithAll(member); List result = new ArrayList<>(); - for (ClothingRecommendation recommendation : modelPredictList) { - RecommendPredictDto dto = makeResultForPredict(closet, recommendation, member); + Closet closet = getClosetWithAll(member); + if (!modelPredictList.isEmpty()) { + log.info("[AI 추천] memberId={} AI 예측 결과 사용 ({}개)", member.getId(), modelPredictList.size()); + for (ClothingRecommendation recommendation : modelPredictList) { + RecommendPredictDto dto = makeResultForPredict(closet, recommendation, member); + boolean isUppersExist = dto.getUppersTypeList() != null && !dto.getUppersTypeList().isEmpty(); + boolean isFeelingExist = dto.getFeelingList() != null && !dto.getFeelingList().isEmpty(); + if (isUppersExist && isFeelingExist) { + result.add(dto); + } + } + } else { + result.addAll(generateFallbackRecommendation(member, targetDate, closet)); + } - boolean isUppersExist = dto.getUppersTypeList() != null && !dto.getUppersTypeList().isEmpty(); - boolean isFeelingExist = dto.getFeelingList() != null && !dto.getFeelingList().isEmpty(); + return result; + } + + private List generateFallbackRecommendation(Member member, LocalDate targetDate, Closet closet) { + log.warn("[Fallback 추천] memberId={} AI 예측 결과 없음. 대체 추천 로직 시작.", member.getId()); + List fallbackResult = new ArrayList<>(); - if (isUppersExist && isFeelingExist) { - result.add(dto); + try { + final Long MIN_OUTER_TYPE = 10L; + final Long MAX_OUTER_TYPE = 25L; + final Long MIN_UPPER_TYPE = 3L; + final Long MAX_UPPER_TYPE = 9L; + + List fallbackOuterTypes = closet.getOuterList().stream() + .filter(Outer::isActive) + .filter(outer -> outer.getOuterType() != null && + outer.getOuterType() >= MIN_OUTER_TYPE && + outer.getOuterType() <= MAX_OUTER_TYPE) + .map(outer -> outer.getOuterType().intValue()) + .distinct() + .collect(Collectors.toList()); + + List fallbackUpperTypes = closet.getUpperList().stream() + .filter(Upper::isActive) + .filter(upper -> upper.getUpperType() != null && + upper.getUpperType() >= MIN_UPPER_TYPE && + upper.getUpperType() <= MAX_UPPER_TYPE) + .map(upper -> upper.getUpperType().intValue()) + .distinct() + .collect(Collectors.toList()); + + if (fallbackOuterTypes.isEmpty() && fallbackUpperTypes.isEmpty()) { + log.info("[Fallback 추천] memberId={} 추천할 활성 상의 또는 아우터 없음 (기준: Upper {}-{}, Outer {}-{}). 예외 발생.", + member.getId(), MIN_UPPER_TYPE, MAX_UPPER_TYPE, MIN_OUTER_TYPE, MAX_OUTER_TYPE); + throw new CustomException(ErrorCode.NO_PREDICT_DATA); } + + List fallbackFeelingList = createFallbackFeelingList(member, targetDate); + + RecommendPredictDto fallbackDto = RecommendPredictDto.builder() + .feelingList(fallbackFeelingList) + .uppersTypeList(fallbackUpperTypes) + .outersTypeList(fallbackOuterTypes) + .build(); + fallbackResult.add(fallbackDto); + log.info("[Fallback 추천] memberId={} 최종 추천 결과: Uppers={}, Outers={}, FeelingsGenerated={}", + member.getId(), fallbackUpperTypes, fallbackOuterTypes, !fallbackFeelingList.isEmpty()); + + } catch (CustomException e) { + log.error("[Fallback 추천 실패] memberId={} 처리 중 Custom 오류: {}", member.getId(), e.getMessage()); + throw new CustomException(ErrorCode.NO_PREDICT_DATA); + } catch (Exception e) { + log.error("[Fallback 추천 실패] memberId={} 알 수 없는 오류 발생: {}", member.getId(), e.getMessage(), e); + throw new CustomException(ErrorCode.NO_PREDICT_DATA); } - return result; + + return fallbackResult; + } + + private List createFallbackFeelingList(Member member, LocalDate targetDate) { + List fallbackFeelingList = new ArrayList<>(); + String regionName = member.getRegionName() != null ? member.getRegionName() : "서울특별시 용산구"; + List targetHours = List.of(9, 12, 15, 18, 21); + final int DEFAULT_FEELING = 2; + + try { + List forecasts = weatherForecastRepository + .findByRegionNameAndForecastDateAndHourInOrderByHourAsc(regionName, targetDate, targetHours); + + if (!forecasts.isEmpty()) { + for (WeatherForecast forecast : forecasts) { + WeatherFeelingDto dto = WeatherFeelingDto.builder() + .date(forecast.getForecastDate()) + .time(forecast.getHour()) + .feeling(DEFAULT_FEELING) + .temperature(forecast.getTemperature()) + .build(); + fallbackFeelingList.add(dto); + } + log.info("[Fallback 추천] memberId={} 기본 체감온도(2)로 그래프 생성 완료 ({}개 시간대)", member.getId(), fallbackFeelingList.size()); + } else { + log.warn("[Fallback 추천] memberId={} 날씨 예보 데이터가 없어 체감온도 그래프를 생성할 수 없습니다. region={}, date={}, hours={}", + member.getId(), regionName, targetDate, targetHours); + } + } catch (Exception e) { + log.error("[Fallback 추천] memberId={} 날씨 예보 조회 중 오류 발생: {}", member.getId(), e.getMessage(), e); + } + return fallbackFeelingList; } private RecommendPredictDto makeResultForPredict(Closet closet, ClothingRecommendation recommendation, Member member) { @@ -83,7 +173,9 @@ private List makeOuterList(Closet closet, List outers) { if (outers.isEmpty()) return new ArrayList<>(); Set ownedClothTypes = closet.getOuterList().stream() + .filter(Outer::isActive) .map(Outer::getOuterType) + .filter(Objects::nonNull) .collect(Collectors.toSet()); Set resultSet = new HashSet<>(); @@ -101,7 +193,9 @@ private List makeOuterList(Closet closet, List outers) { private List makeUpperList(Closet closet, List tops) { Set ownedClothTypes = closet.getUpperList().stream() + .filter(Upper::isActive) .map(Upper::getUpperType) + .filter(Objects::nonNull) .collect(Collectors.toSet()); Set resultSet = new HashSet<>(); @@ -119,52 +213,42 @@ private List makeUpperList(Closet closet, List tops) { private List makeWeatherFeeling(Map predictionMap, ClothingRecommendation recommendation) { List feelingList = new ArrayList<>(); - LocalDate forecastDate = recommendation.getDate(); - String regionName = recommendation.getRegionName(); - List hours = predictionMap.keySet().stream() - .map(Integer::parseInt) - .collect(Collectors.toList()); + List targetHours = List.of(9, 12, 15, 18, 21); List forecasts = weatherForecastRepository - .findByRegionNameAndForecastDateAndHourInOrderByCreatedAtDesc(regionName, forecastDate, hours); - - Map hourToForecastMap = forecasts.stream() - .collect(Collectors.toMap( - WeatherForecast::getHour, - forecast -> forecast, - (oldVal, newVal) -> oldVal - )); - - for (Map.Entry entry : predictionMap.entrySet()) { - int hour = Integer.parseInt(entry.getKey()); - int feeling = entry.getValue(); - WeatherForecast forecast = hourToForecastMap.get(hour); - - if (forecast != null) { - WeatherFeelingDto dto = WeatherFeelingDto.builder() - .date(forecast.getForecastDate()) - .time(hour) - .feeling(feeling) - .temperature(forecast.getTemperature()) - .build(); - feelingList.add(dto); - } else { - log.warn("날씨 데이터 없음: region={}, date={}, hour={}", regionName, forecastDate, hour); + .findByRegionNameAndForecastDateAndHourInOrderByHourAsc(regionName, forecastDate, targetHours); + + Map safePredictionMap = Optional.ofNullable(predictionMap).orElseGet(Collections::emptyMap); + final int DEFAULT_FEELING = 2; + + for (WeatherForecast forecast : forecasts) { + int hour = forecast.getHour(); + int feeling = safePredictionMap.getOrDefault(String.valueOf(hour), DEFAULT_FEELING); + + WeatherFeelingDto dto = WeatherFeelingDto.builder() + .date(forecast.getForecastDate()) + .time(hour) + .feeling(feeling) + .temperature(forecast.getTemperature()) + .build(); + feelingList.add(dto); + + if (feeling == DEFAULT_FEELING && !safePredictionMap.containsKey(String.valueOf(hour))) { + log.debug("AI 체감온도 예측값 없음. 기본값(2) 사용: region={}, date={}, hour={}", regionName, forecastDate, hour); } } + + if (forecasts.isEmpty()) { + log.warn("날씨 예보 데이터 없음: region={}, date={}, hours={}", regionName, forecastDate, targetHours); + } return feelingList; } - private List getModelPrediction(Long id, LocalDate now) { - List list = clothingRecommendationRepository.findByMemberIdAndDate(id, now); - if (list.isEmpty()) { - throw new CustomException(ErrorCode.NO_PREDICT_DATA); - } - return list; + return clothingRecommendationRepository.findByMemberIdAndDate(id, now); } @Transactional @@ -196,7 +280,6 @@ public void save(ModelClothingRecommendationDto dto, Member member) { } } - private Closet getClosetWithAll(Member member) { Closet closetWithUppers = closetRepository.findClosetWithUppers(member.getId()) .orElseThrow(() -> new CustomException(ErrorCode.CLOSET_NOT_FOUND)); diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/WeatherForecastRepository.java b/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/WeatherForecastRepository.java index 5685ec1..9118dc7 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/WeatherForecastRepository.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/WeatherForecastRepository.java @@ -22,4 +22,6 @@ List findByRegionNameAndForecastDateAndHourInOrderByCreatedAtDe void deleteByForecastDateGreaterThanEqual(LocalDate baseDate); List findByRegionNameAndForecastDateAndHourIn(String regionName, LocalDate now, List targetHours); + + List findByRegionNameAndForecastDateAndHourInOrderByHourAsc(String regionName, LocalDate forecastDate, List targetHours); }