diff --git a/build.gradle b/build.gradle index 398b4c9..341f336 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation 'org.springframework.data:spring-data-redis' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.auth:google-auth-library-oauth2-http:1.17.0' implementation 'com.google.code.gson:gson:2.8.9' diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/api/AiInternalController.java b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/api/AiInternalController.java index ca9a03e..8d481f1 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/api/AiInternalController.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/api/AiInternalController.java @@ -62,7 +62,6 @@ public ResponseEntity> sendAllUsersPredictionDataSecure() { public ResponseEntity> saveRecommendations(@RequestBody Map encryptedBody) { try { String decryptedJson = aesCipher.decrypt(encryptedBody); - log.info("๐Ÿ”“ ๋ณตํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ:\n{}", decryptedJson); if (decryptedJson.startsWith("\"") && decryptedJson.endsWith("\"")) { decryptedJson = objectMapper.readValue(decryptedJson, String.class); @@ -118,34 +117,33 @@ private Map encryptPredictionData(List d } // TODO : ์•”/๋ณตํ˜ธํ™” API ์—ฐ๊ฒฐ ์ดํ›„ ์•„๋ž˜ ์ฝ”๋“œ ์‚ญ์ œ - - @PostMapping("/prediction") - public ResponseEntity> sendAllUsersPredictionData() { - try { - List members = memberRepository.findAllByIsDeletedFalse(); - - List allDtos = members.stream() - .filter(member -> member.getCloset() != null) - .map(aiInternalService::makePredictRequest) - .collect(Collectors.toList()); - - return ResponseEntity.ok(allDtos); - } catch (Exception e) { - log.error(e.getMessage()); - } - return null; - } - - @PostMapping("/recommendation") - public ResponseEntity> saveRecommendations(@RequestBody ModelClothingRecommendationDto dto) { - recommendationService.save(dto); - return ApiResponse.success(HttpStatus.OK, "์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - - @PostMapping("/history") - public ResponseEntity>> getSimilarHistory(@RequestBody HistoryRequestDto dto) { - dto.validate(); - List result = recordCalendarService.getMemberHistory(dto); - return ApiResponse.success(HttpStatus.OK, result); - } +// @PostMapping("/prediction") +// public ResponseEntity> sendAllUsersPredictionData() { +// try { +// List members = memberRepository.findAllByIsDeletedFalse(); +// +// List allDtos = members.stream() +// .filter(member -> member.getCloset() != null) +// .map(aiInternalService::makePredictRequest) +// .collect(Collectors.toList()); +// +// return ResponseEntity.ok(allDtos); +// } catch (Exception e) { +// log.error(e.getMessage()); +// } +// return null; +// } +// +// @PostMapping("/recommendation") +// public ResponseEntity> saveRecommendations(@RequestBody ModelClothingRecommendationDto dto) { +// recommendationService.save(dto); +// return ApiResponse.success(HttpStatus.OK, "์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค."); +// } +// +// @PostMapping("/history") +// public ResponseEntity>> getSimilarHistory(@RequestBody HistoryRequestDto dto) { +// dto.validate(); +// List result = recordCalendarService.getMemberHistory(dto); +// return ApiResponse.success(HttpStatus.OK, result); +// } } diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelSchedular.java b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelSchedular.java index 30c93d6..4bb8d23 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelSchedular.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/schedular/ModelSchedular.java @@ -112,15 +112,8 @@ public void pushPredictionAESDataToAiServer() { return; } - ObjectMapper objectMapper = new ObjectMapper(); - String plainJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(allDtos); - log.info("โœ… ์˜ˆ์ธก ๋ฐ์ดํ„ฐ (ํ‰๋ฌธ JSON):\n{}", plainJson); - Map encrypted = encryptPredictionData(allDtos); - String encryptedJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(encrypted); - log.info("๐Ÿ”’ ์˜ˆ์ธก ๋ฐ์ดํ„ฐ (์•”ํ˜ธํ™”๋œ JSON):\n{}", encryptedJson); - HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity> requestEntity = new HttpEntity<>(encrypted, headers); @@ -139,7 +132,7 @@ public void pushPredictionAESDataToAiServer() { private Map encryptPredictionData(List dtos) { try { String jsonData = objectMapper.writeValueAsString(dtos); - return aesCipher.encrypt(jsonData); // { "iv": "...", "payload": "..." } + return aesCipher.encrypt(jsonData); } catch (Exception e) { log.error("์˜ˆ์ธก ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™” ์‹คํŒจ: {}", e.getMessage()); return null; diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/AiInternalService.java b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/AiInternalService.java index d7a16a9..cb1f9e7 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/AiInternalService.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/ai_model/service/AiInternalService.java @@ -28,7 +28,7 @@ public class AiInternalService { @Transactional public AiPredictionRequestDto makePredictRequest(Member member) { - List dtoList = getWeatherForecast(); + List dtoList = getWeatherForecast(member); return AiPredictionRequestDto.builder() .userId(member.getId()) .bodyTypeLabel(member.getConstitution()) @@ -37,11 +37,12 @@ public AiPredictionRequestDto makePredictRequest(Member member) { .build(); } - private List getWeatherForecast() { + private List getWeatherForecast(Member member) { List targetHours = List.of(9, 12, 15, 18, 21); + String regionName = member.getRegionName() != null ? member.getRegionName() : "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; List forecasts = weatherForecastRepository - .findByRegionNameAndForecastDateAndHourIn("์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ", LocalDate.now(), targetHours); + .findByRegionNameAndForecastDateAndHourIn(regionName, LocalDate.now(), targetHours); return forecasts.stream() .map(this::convertToDto) 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 574c5e4..60fa746 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 @@ -43,16 +43,16 @@ public List getRecommendList(Member member) { Closet closet = getClosetWithAll(member); List result = new ArrayList<>(); for (ClothingRecommendation recommendation : modelPredictList) { - result.add(makeResultForPredict(closet, recommendation)); + result.add(makeResultForPredict(closet, recommendation, member)); } return result; } - private RecommendPredictDto makeResultForPredict(Closet closet, ClothingRecommendation recommendation) { + private RecommendPredictDto makeResultForPredict(Closet closet, ClothingRecommendation recommendation, Member member) { try { List upperTypeList = makeUpperList(closet, recommendation.getTops()); List outerTypeList = makeOuterList(closet, recommendation.getOuters()); - List weatherFeelingDto = makeWeatherFeeling(recommendation.getPredictionMap()); + List weatherFeelingDto = makeWeatherFeeling(recommendation.getPredictionMap(), member); return RecommendPredictDto.builder() .feelingList(weatherFeelingDto) @@ -65,7 +65,6 @@ private RecommendPredictDto makeResultForPredict(Closet closet, ClothingRecommen throw new CustomException(ErrorCode.UNKNOWN_ERROR, "์˜ˆ์ธก ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค: " + e.getMessage()); } } - // TODO : AI ํ•™์Šต ์ดํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ๋Œ€๋Ÿ‰์œผ๋กœ ์˜์ƒ์„ ์‚ญ์ œํ•ด์„œ ์ถ”์ฒœํ•  ์˜์ƒ์ด ์—†์–ด์ ธ๋ฒ„๋ฆฌ๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ํ•„์š”ํ•ด๋ณด์ž„. private List makeOuterList(Closet closet, List outers) { @@ -85,7 +84,6 @@ private List makeOuterList(Closet closet, List outers) { } } - // if (result.isEmpty()) throw new CustomException(ErrorCode.NO_RECOMMEND_OUTER); return new ArrayList<>(resultSet); } @@ -103,18 +101,17 @@ private List makeUpperList(Closet closet, List tops) { } } } - // if (result.isEmpty()) throw new CustomException(ErrorCode.NO_RECOMMEND_UPPER); return new ArrayList<>(resultSet); } - private List makeWeatherFeeling(Map predictionMap) { + private List makeWeatherFeeling(Map predictionMap, Member member) { List feelingList = new ArrayList<>(); LocalDate today = LocalDate.now(); LocalTime now = LocalTime.now(); LocalDate forecastDate = now.isBefore(LocalTime.of(6, 0)) ? today.minusDays(1) : today; - String regionName = "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; + String regionName = (member.getRegionName() != null) ? member.getRegionName() : "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; List hours = predictionMap.keySet().stream() .map(Integer::parseInt) @@ -123,12 +120,11 @@ private List makeWeatherFeeling(Map predicti List forecasts = weatherForecastRepository .findByRegionNameAndForecastDateAndHourIn(regionName, forecastDate, hours); - // hour ๊ธฐ์ค€์œผ๋กœ forecast ๊ฐ์ฒด๋ฅผ ํ†ต์งธ๋กœ ์ €์žฅ (์ค‘๋ณต ๋ฐฉ์ง€ ๋ฐ ๋‚ ์งœ ํฌํ•จ์šฉ) Map hourToForecastMap = forecasts.stream() .collect(Collectors.toMap( WeatherForecast::getHour, forecast -> forecast, - (oldVal, newVal) -> newVal // ์ค‘๋ณต ์‹œ๊ฐ„๋Œ€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ตœ์‹  ๋ฐ์ดํ„ฐ๋กœ ๋ฎ๊ธฐ + (oldVal, newVal) -> newVal )); for (Map.Entry entry : predictionMap.entrySet()) { @@ -138,12 +134,11 @@ private List makeWeatherFeeling(Map predicti if (forecast != null) { WeatherFeelingDto dto = WeatherFeelingDto.builder() - .date(forecast.getForecastDate()) // ์ถ”๊ฐ€ + .date(forecast.getForecastDate()) .time(hour) .feeling(feeling) .temperature(forecast.getTemperature()) .build(); - feelingList.add(dto); } else { throw new CustomException(ErrorCode.WEATHER_DATA_NOT_FOUND); @@ -153,14 +148,12 @@ private List makeWeatherFeeling(Map predicti 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 diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/location/api/LocationController.java b/src/main/java/com/howWeather/howWeather_backend/domain/location/api/LocationController.java new file mode 100644 index 0000000..70fb8ff --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/location/api/LocationController.java @@ -0,0 +1,37 @@ +package com.howWeather.howWeather_backend.domain.location.api; + +import com.howWeather.howWeather_backend.domain.location.dto.LocationWeatherRequestDto; +import com.howWeather.howWeather_backend.domain.location.dto.RegionTemperatureDto; +import com.howWeather.howWeather_backend.domain.location.service.LocationService; +import com.howWeather.howWeather_backend.global.Response.ApiResponse; +import com.howWeather.howWeather_backend.global.jwt.CheckAuthenticatedUser; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/api/location") +@RequiredArgsConstructor +public class LocationController { + private final LocationService locationService; + @GetMapping("/region") + @CheckAuthenticatedUser + public ResponseEntity> getRegionFromCoords(@RequestHeader("Authorization") String accessTokenHeader, + @RequestBody @Valid LocationWeatherRequestDto request) { + String region = locationService.reverseGeocode(request.getLatitude(), request.getLongitude()); + return ApiResponse.success(HttpStatus.OK, region); + } + + @GetMapping("/region/temperature") + @CheckAuthenticatedUser + public ResponseEntity> getRegionTemperature(@RequestHeader("Authorization") String accessTokenHeader, + @RequestBody @Valid LocationWeatherRequestDto request) { + RegionTemperatureDto dto = locationService.getRegionTemperatureFromCoords( + request.getLatitude(), request.getLongitude(), request.getTimeSlot(), request.getDate()); + return ApiResponse.success(HttpStatus.OK, dto); + } +} diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/LocationWeatherRequestDto.java b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/LocationWeatherRequestDto.java new file mode 100644 index 0000000..57cb562 --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/LocationWeatherRequestDto.java @@ -0,0 +1,25 @@ +package com.howWeather.howWeather_backend.domain.location.dto; + +import com.howWeather.howWeather_backend.global.custom.YesterdayOrToday; +import jakarta.validation.constraints.*; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class LocationWeatherRequestDto { + @DecimalMin(value = "-90.0", message = "latitude ์ตœ์†Œ๊ฐ’์€ -90์ž…๋‹ˆ๋‹ค.") + @DecimalMax(value = "90.0", message = "latitude ์ตœ๋Œ€๊ฐ’์€ 90์ž…๋‹ˆ๋‹ค.") + private double latitude; + + @DecimalMin(value = "-180.0", message = "longitude ์ตœ์†Œ๊ฐ’์€ -180์ž…๋‹ˆ๋‹ค.") + @DecimalMax(value = "180.0", message = "longitude ์ตœ๋Œ€๊ฐ’์€ 180์ž…๋‹ˆ๋‹ค.") + private double longitude; + + @Min(value = 1, message = "timeSlot์€ 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Max(value = 3, message = "timeSlot์€ 3 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + private int timeSlot; + + @YesterdayOrToday + private LocalDate date; +} diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionInfoDto.java b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionInfoDto.java new file mode 100644 index 0000000..94cc042 --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionInfoDto.java @@ -0,0 +1,10 @@ +package com.howWeather.howWeather_backend.domain.location.dto; + +import lombok.Data; + +@Data +public class RegionInfoDto { + private String address_name; + private String region_1depth_name; + private String region_2depth_name; +} diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionTemperatureDto.java b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionTemperatureDto.java new file mode 100644 index 0000000..299abdd --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/location/dto/RegionTemperatureDto.java @@ -0,0 +1,11 @@ +package com.howWeather.howWeather_backend.domain.location.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class RegionTemperatureDto { + private String regionName; + private Double temperature; +} diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/location/service/LocationService.java b/src/main/java/com/howWeather/howWeather_backend/domain/location/service/LocationService.java new file mode 100644 index 0000000..1d59f60 --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/location/service/LocationService.java @@ -0,0 +1,140 @@ +package com.howWeather.howWeather_backend.domain.location.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.howWeather.howWeather_backend.domain.location.dto.RegionInfoDto; +import com.howWeather.howWeather_backend.domain.weather.entity.Region; +import com.howWeather.howWeather_backend.domain.weather.entity.Weather; +import com.howWeather.howWeather_backend.domain.location.dto.RegionTemperatureDto; +import com.howWeather.howWeather_backend.domain.weather.repository.RegionRepository; +import com.howWeather.howWeather_backend.domain.weather.repository.WeatherRepository; +import com.howWeather.howWeather_backend.global.exception.CustomException; +import com.howWeather.howWeather_backend.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LocationService { + + @Value("${kakao.api.key}") + private String kakaoApiKey; + + private final WeatherRepository weatherRepository; + private final RegionRepository regionRepository; + + public String reverseGeocode(double latitude, double longitude) { + try { + boolean latitudeValidation = validateData("latitude", latitude, -90, 90); + boolean longitudeValidation = validateData("longitude", longitude, -180, 180); + + if (!latitudeValidation || !longitudeValidation) { + throw new CustomException(ErrorCode.INVALID_INPUT, "์œ„/๊ฒฝ๋„ ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + String url = "https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=" + longitude + "&y=" + latitude; + + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, JsonNode.class); + + JsonNode documents = response.getBody().get("documents"); + if (documents.isArray() && !documents.isEmpty()) { + JsonNode region = documents.get(0); + + String regionName = region.get("region_1depth_name").asText() + " " + region.get("region_2depth_name").asText(); + return regionName; + } else { + throw new CustomException(ErrorCode.KAKAO_API_REGION_NOT_FOUND); + } + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("์นด์นด์˜ค ์œ„์น˜ ์ฐพ๊ธฐ ์‹คํŒจ: {}", e.getMessage(), e); + throw new CustomException(ErrorCode.UNKNOWN_ERROR, "์นด์นด์˜ค ์œ„์น˜ ์ฐพ๊ธฐ ์‹คํŒจ"); + } + } + + private boolean validateData(String name, double data, int low, int high) { + if (data < low || data > high) { + return false; + } + return true; + } + + @Transactional + public RegionTemperatureDto getRegionTemperatureFromCoords( + double latitude, double longitude, int timeSlot, LocalDate date) { + try { + String fullRegion = reverseGeocode(latitude, longitude); + String matchedRegionName = resolveRegionName(fullRegion); + + if (date == null) { + date = LocalDate.now(); + } + + Weather weather = weatherRepository.findByRegionNameAndDateAndTimeSlot( + matchedRegionName, date, timeSlot + ).orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); + + return new RegionTemperatureDto(matchedRegionName, weather.getTemperature()); + + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("์œ„์น˜ ์ฒ˜๋ฆฌ ์‹คํŒจ: {}", e.getMessage(), e); + throw new CustomException(ErrorCode.UNKNOWN_ERROR, "์œ„์น˜ ์ฒ˜๋ฆฌ ์‹คํŒจ"); + } + } + + public String resolveRegionName(String fullRegionFromKakao) { + Optional exactMatch = regionRepository.findByName(fullRegionFromKakao); + if (exactMatch.isPresent()) { + return exactMatch.get().getName(); + } + + List regionNames = regionRepository.findAllRegionNames(); + + String bestMatch = null; + int maxPrefixLength = 0; + + for (String regionName : regionNames) { + int prefixLength = getCommonPrefixLength(fullRegionFromKakao, regionName); + if (prefixLength > maxPrefixLength) { + maxPrefixLength = prefixLength; + bestMatch = regionName; + } + } + + if (bestMatch == null) { + throw new CustomException(ErrorCode.REGION_NOT_FOUND); + } + + return bestMatch; + } + + private int getCommonPrefixLength(String a, String b) { + int minLength = Math.min(a.length(), b.length()); + int i = 0; + while (i < minLength && a.charAt(i) == b.charAt(i)) { + i++; + } + return i; + } +} + diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/member/api/MyAccountController.java b/src/main/java/com/howWeather/howWeather_backend/domain/member/api/MyAccountController.java index 12ae721..dded81b 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/member/api/MyAccountController.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/member/api/MyAccountController.java @@ -3,6 +3,7 @@ import com.howWeather.howWeather_backend.domain.member.dto.NicknameDto; import com.howWeather.howWeather_backend.domain.member.dto.ProfileChangeIntDto; import com.howWeather.howWeather_backend.domain.member.dto.ProfileDto; +import com.howWeather.howWeather_backend.domain.member.dto.RegionDto; import com.howWeather.howWeather_backend.domain.member.entity.Member; import com.howWeather.howWeather_backend.domain.member.repository.MemberRepository; import com.howWeather.howWeather_backend.domain.member.service.AuthService; @@ -67,4 +68,21 @@ public ResponseEntity> updateConstitution(@RequestHeader("Au authService.updateConstitution(member, profileChangeDto); return ApiResponse.success(HttpStatus.OK, "์ฒด์งˆ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค."); } + + @GetMapping("/location") + @CheckAuthenticatedUser + public ResponseEntity> getLocation(@RequestHeader("Authorization") String accessTokenHeader, + @AuthenticationPrincipal Member member) { + String location = authService.getLoccation(member); + return ApiResponse.success(HttpStatus.OK, location); + } + + @PatchMapping("/update-location") + @CheckAuthenticatedUser + public ResponseEntity> updateLocation(@RequestHeader("Authorization") String accessTokenHeader, + @Valid @RequestBody RegionDto regionDto, + @AuthenticationPrincipal Member member) { + authService.updateLocation(member, regionDto); + return ApiResponse.success(HttpStatus.OK, "์ง€์—ญ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค."); + } } diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/member/dto/RegionDto.java b/src/main/java/com/howWeather/howWeather_backend/domain/member/dto/RegionDto.java new file mode 100644 index 0000000..0bff4b6 --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/domain/member/dto/RegionDto.java @@ -0,0 +1,11 @@ +package com.howWeather.howWeather_backend.domain.member.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class RegionDto { + @NotNull @NotBlank + private String regionName; +} diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/member/entity/Member.java b/src/main/java/com/howWeather/howWeather_backend/domain/member/entity/Member.java index 694bedc..8f6b49f 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/member/entity/Member.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/member/entity/Member.java @@ -50,6 +50,9 @@ public class Member implements UserDetails { @Column(name="body_type") private int bodyType; + @Column(name="region_name") + private String regionName; + @Column(name="gender", nullable = false) private int gender; @@ -116,6 +119,10 @@ public void changeNickname(String s) { this.nickname = s; } + public void changeRegion(String regionName) { + this.regionName = regionName; + } + public void changeConstitution(int v) { this.constitution = v; } diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/member/service/AuthService.java b/src/main/java/com/howWeather/howWeather_backend/domain/member/service/AuthService.java index 3291b08..d2aa3ba 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/member/service/AuthService.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/member/service/AuthService.java @@ -7,6 +7,8 @@ import com.howWeather.howWeather_backend.domain.member.entity.Member; import com.howWeather.howWeather_backend.domain.member.repository.MemberRepository; import com.howWeather.howWeather_backend.domain.closet.repository.ClosetRepository; +import com.howWeather.howWeather_backend.domain.weather.entity.Region; +import com.howWeather.howWeather_backend.domain.weather.repository.RegionRepository; import com.howWeather.howWeather_backend.global.exception.CustomException; import com.howWeather.howWeather_backend.global.exception.ErrorCode; import com.howWeather.howWeather_backend.global.exception.LoginException; @@ -29,6 +31,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalTime; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; @@ -48,6 +51,7 @@ public class AuthService { private final FcmAlarmPreferenceService fcmAlarmPreferenceService; private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2AuthorizedClientService authorizedClientService; + private final RegionRepository regionRepository; @Transactional public void signup(SignupRequestDto signupRequestDto) { @@ -299,6 +303,63 @@ public void updateConstitution(Member member, ProfileChangeIntDto profileChangeD } } + @Transactional(readOnly = true) + public String getLoccation(Member member) { + try { + String region = member.getRegionName(); + + if (region == null || region.trim().isEmpty()) + return "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; + + return region; + + } catch (CustomException e) { + throw e; + }catch (Exception e) { + log.error("์ง€์—ญ ์กฐํšŒ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {}", e.getMessage(), e); + throw new CustomException(ErrorCode.UNKNOWN_ERROR, "์ง€์—ญ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + @Transactional + public void updateLocation(Member member, RegionDto regionDto) { + try { + LocalTime now = LocalTime.now(); + if (!now.isBefore(LocalTime.of(4, 00)) && now.isBefore(LocalTime.of(7, 0))) { + throw new CustomException(ErrorCode.TIME_RESTRICTED_FOR_REGION_CHANGE); + } + validateRegion(regionDto.getRegionName()); + + Member persistedMember = memberRepository.findById(member.getId()) + .orElseThrow(() -> new CustomException(ErrorCode.ID_NOT_FOUND, "ํšŒ์› ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + String oldRegionName = persistedMember.getRegionName(); + String newRegionName = regionDto.getRegionName(); + + if (oldRegionName == null || !newRegionName.equals(oldRegionName)) { + if (oldRegionName != null) { + regionRepository.findByName(oldRegionName).ifPresent(Region::decrementCurrentUserCount); + } + + Region newRegion = regionRepository.findByName(newRegionName) + .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); + newRegion.incrementCurrentUserCount(); + persistedMember.changeRegion(newRegionName); + } + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("์ง€์—ญ ์ˆ˜์ • ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {}", e.getMessage(), e); + throw new CustomException(ErrorCode.UNKNOWN_ERROR, "์ง€์—ญ ์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + private void validateRegion(String regionName) { + if (regionName == null || regionName.isEmpty() || !regionRepository.existsByName(regionName)) { + throw new CustomException(ErrorCode.REGION_NOT_FOUND); + } + } + private void validateIntData(Integer data, int s, int e) { if (data < s || data > e) { throw new CustomException(ErrorCode.INVALID_INPUT, "์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค."); @@ -366,7 +427,7 @@ public String resetPassword(String identifier) { memberRepository.flush(); mailService.sendTemporaryPassword(member.getEmail(), tempPassword); return member.getEmail(); - } catch (CustomException e) { + } catch (CustomException e) { throw e; } catch (Exception e) { log.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์ดˆ๊ธฐํ™” ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {}", e.getMessage(), e); diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/record_calendar/dto/RecordRequestDto.java b/src/main/java/com/howWeather/howWeather_backend/domain/record_calendar/dto/RecordRequestDto.java index aa003d2..ea897b0 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/record_calendar/dto/RecordRequestDto.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/record_calendar/dto/RecordRequestDto.java @@ -1,10 +1,10 @@ package com.howWeather.howWeather_backend.domain.record_calendar.dto; import com.howWeather.howWeather_backend.global.custom.ValidateUppersOrOuters; +import com.howWeather.howWeather_backend.global.custom.YesterdayOrToday; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -23,7 +23,7 @@ public class RecordRequestDto { @Min(1) @Max(3) int feeling; - @NotNull + @YesterdayOrToday LocalDate date; List uppers; diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/weather/entity/Region.java b/src/main/java/com/howWeather/howWeather_backend/domain/weather/entity/Region.java index a62eeb2..46cd3b3 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/weather/entity/Region.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/weather/entity/Region.java @@ -21,4 +21,16 @@ public class Region { private double lon; private String name; + + private int currentUserCount; + + public void incrementCurrentUserCount() { + this.currentUserCount++; + } + + public void decrementCurrentUserCount() { + if (this.currentUserCount > 0) { + this.currentUserCount--; + } + } } diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/RegionRepository.java b/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/RegionRepository.java index b0f36af..0070d0d 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/RegionRepository.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/weather/repository/RegionRepository.java @@ -3,7 +3,19 @@ import com.howWeather.howWeather_backend.domain.weather.entity.Region; import jdk.jfr.Registered; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; @Registered public interface RegionRepository extends JpaRepository { + boolean existsByName(String name); + + Optional findByName(String name); + + @Query("SELECT r.name FROM Region r") + List findAllRegionNames(); + + List findByCurrentUserCountGreaterThan(int count); } diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/weather/scheduler/WeatherScheduler.java b/src/main/java/com/howWeather/howWeather_backend/domain/weather/scheduler/WeatherScheduler.java index dc1f99d..1b27c67 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/weather/scheduler/WeatherScheduler.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/weather/scheduler/WeatherScheduler.java @@ -57,16 +57,16 @@ public void deleteOldWeatherData() { /** * ๋งค์ผ ์˜ค์ „ 4์‹œ 30๋ถ„์— ai ํ•™์Šต์— ์ด์šฉ๋  ์˜ค๋Š˜์˜ ์˜ˆ๋ณด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. */ - @Scheduled(cron = "0 30 4 * * *", zone = "Asia/Seoul") // ๋งค์ผ ์˜ค์ „ 4์‹œ 30๋ถ„์— ์˜ˆ๋ณด ๋ฐ์ดํ„ฐ + @Scheduled(cron = "0 30 4 * * *", zone = "Asia/Seoul") public void fetchDailyOneCallWeather() { - log.info("Forecast API๋กœ ์šฉ์‚ฐ๊ตฌ์˜ ๋‚ ์”จ ์˜ˆ๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์‹œ๊ฐ„: {}", LocalDateTime.now()); + log.info("Forecast API๋กœ ์šฉ์‚ฐ๊ตฌ๋ฅผ ํฌํ•จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ๋ณด๋ฅผ ํฌ๋งํ•˜๋Š” ์ง€์—ญ์˜ ๋‚ ์”จ ์˜ˆ๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์‹œ๊ฐ„: {}", LocalDateTime.now()); weatherService.fetchHourlyForecast(); } /** * ๋งค์ผ ์˜ค์ „ 7์‹œ์— ์˜ค๋Š˜ ์ด์ „์˜ ์˜ˆ๋ณด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. */ - @Scheduled(cron = "0 0 7 * * *", zone = "Asia/Seoul") // ๋งค์ผ ์˜ค์ „ 7์‹œ + @Scheduled(cron = "0 0 7 * * *", zone = "Asia/Seoul") public void deleteOldForecastData() { LocalDate today = LocalDate.now(); weatherForecastRepository.deleteByForecastDateBefore(today); diff --git a/src/main/java/com/howWeather/howWeather_backend/domain/weather/service/WeatherService.java b/src/main/java/com/howWeather/howWeather_backend/domain/weather/service/WeatherService.java index d4e4ea6..12063ae 100644 --- a/src/main/java/com/howWeather/howWeather_backend/domain/weather/service/WeatherService.java +++ b/src/main/java/com/howWeather/howWeather_backend/domain/weather/service/WeatherService.java @@ -53,7 +53,7 @@ public double getTemperature(WeatherSimpleDto dto) { LocalDate localDate = LocalDate.parse(dto.getDate()); return weatherRepository.findByRegionNameAndDateAndTimeSlot(dto.getCity(), localDate, dto.getTimeSlot()) .map(Weather::getTemperature) - .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND, "ํ•ด๋‹น ์ง€์—ญ๊ณผ ์‹œ๊ฐ„๋Œ€์˜ ์˜จ๋„๋Š” ํ˜„์žฌ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new CustomException(ErrorCode. REGION_AND_TIME_NOT_FOUND)); } catch (CustomException e){ throw e; } catch (Exception e) { @@ -61,37 +61,47 @@ public double getTemperature(WeatherSimpleDto dto) { } } + @Transactional public void fetchHourlyForecast() { - double lat = 37.53138497; - double lon = 126.979907; - String regionName = "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; // TODO: ์ถ”ํ›„ ๋™์  ์ง€์—ญ๋ช…์œผ๋กœ ๋ณ€๊ฒฝ + List regions = regionRepository.findByCurrentUserCountGreaterThan(0); + LocalDate baseDate = LocalDate.now(); - List forecasts = weatherApiClient.fetchForecast(lat, lon); + String defaultRegionName = "์„œ์šธํŠน๋ณ„์‹œ ์šฉ์‚ฐ๊ตฌ"; + boolean hasDefaultRegion = regions.stream() + .anyMatch(r -> r.getName().equals(defaultRegionName)); + if (!hasDefaultRegion) { + regionRepository.findByName(defaultRegionName).ifPresent(regions::add); + } - LocalDate baseDate = LocalDate.now(); - int cnt = 0; + List allForecasts = new ArrayList<>(); - List entities = new ArrayList<>(); + for (Region region : regions) { + double lat = region.getLat(); + double lon = region.getLon(); + String regionName = region.getName(); + + List forecasts = weatherApiClient.fetchForecast(lat, lon); - for (WeatherPredictDto dto : forecasts) { - LocalDate forecastDate = baseDate.plusDays(cnt / 5); + int cnt = 0; + for (WeatherPredictDto dto : forecasts) { + LocalDate forecastDate = baseDate.plusDays(cnt / 5); - WeatherForecast entity = WeatherForecast.builder() - .regionName(regionName) - .forecastDate(forecastDate) - .hour(dto.getHour()) - .temperature(dto.getTemperature()) - .humidity(dto.getHumidity()) - .windSpeed(dto.getWindSpeed()) - .precipitation(dto.getPrecipitation()) - .cloudAmount(dto.getCloudAmount()) - .feelsLike(dto.getFeelsLike()) - .build(); + WeatherForecast entity = WeatherForecast.builder() + .regionName(regionName) + .forecastDate(forecastDate) + .hour(dto.getHour()) + .temperature(dto.getTemperature()) + .humidity(dto.getHumidity()) + .windSpeed(dto.getWindSpeed()) + .precipitation(dto.getPrecipitation()) + .cloudAmount(dto.getCloudAmount()) + .feelsLike(dto.getFeelsLike()) + .build(); - entities.add(entity); - cnt++; + allForecasts.add(entity); + cnt++; + } } - weatherForecastRepository.saveAll(entities); + weatherForecastRepository.saveAll(allForecasts); } - } diff --git a/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayOrToday.java b/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayOrToday.java new file mode 100644 index 0000000..ed629dc --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayOrToday.java @@ -0,0 +1,16 @@ +package com.howWeather.howWeather_backend.global.custom; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = YesterdayTodayValidator.class) +@Documented +public @interface YesterdayOrToday { + String message() default "๋‚ ์งœ๋Š” ์–ด์ œ ๋˜๋Š” ์˜ค๋Š˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayTodayValidator.java b/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayTodayValidator.java new file mode 100644 index 0000000..7cde576 --- /dev/null +++ b/src/main/java/com/howWeather/howWeather_backend/global/custom/YesterdayTodayValidator.java @@ -0,0 +1,16 @@ +package com.howWeather.howWeather_backend.global.custom; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDate; + +public class YesterdayTodayValidator implements ConstraintValidator { + @Override + public boolean isValid(LocalDate value, ConstraintValidatorContext context) { + if (value == null) return true; + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDate today = LocalDate.now(); + return !value.isBefore(yesterday) && !value.isAfter(today); + } +} diff --git a/src/main/java/com/howWeather/howWeather_backend/global/exception/ErrorCode.java b/src/main/java/com/howWeather/howWeather_backend/global/exception/ErrorCode.java index 5737fb1..79797ee 100644 --- a/src/main/java/com/howWeather/howWeather_backend/global/exception/ErrorCode.java +++ b/src/main/java/com/howWeather/howWeather_backend/global/exception/ErrorCode.java @@ -58,7 +58,8 @@ public enum ErrorCode { // ๋‹์”จ WEATHER_API_CALL_ERROR("๋‚ ์”จ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค", HttpStatus.SERVICE_UNAVAILABLE), NO_BODY_ERROR("๋‚ ์”จ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค", HttpStatus.NOT_FOUND), - REGION_NOT_FOUND("ํ•ด๋‹น ์ง€์—ญ๊ณผ ์‹œ๊ฐ„๋Œ€์— ๋Œ€ํ•ด์„œ๋Š” ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + REGION_AND_TIME_NOT_FOUND("ํ•ด๋‹น ์ง€์—ญ๊ณผ ์‹œ๊ฐ„๋Œ€์˜ ์˜จ๋„๋Š” ํ˜„์žฌ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + REGION_NOT_FOUND("ํ•ด๋‹น ์ง€์—ญ์— ๋Œ€ํ•ด์„œ๋Š” ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), // ์•Œ๋ฆผ ๊ด€๋ฆฌ DUPLICATE_FCM_TOKEN("์ด๋ฏธ ๋“ฑ๋ก๋œ FCM ํ† ํฐ์ž…๋‹ˆ๋‹ค.", HttpStatus.CONFLICT), @@ -77,6 +78,10 @@ public enum ErrorCode { GOOGLE_INVALID_ACCESS_TOKEN("์œ ํšจํ•˜์ง€ ์•Š์€ ๊ตฌ๊ธ€ ์•ก์„ธ์Šค ํ† ํฐ์ž…๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), NO_OAUTH2_CLIENT("Auth2AuthorizedClient๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), OAUTH2_TOKEN_NOT_FOUND("OAuth2 access token์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), + + // ์ง€์—ญ + KAKAO_API_REGION_NOT_FOUND("ํ•ด๋‹น ์ง€์—ญ์„ ์ฐพ๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + TIME_RESTRICTED_FOR_REGION_CHANGE("์˜ค์ „ 4:00 ~ ์˜ค์ „ 7:00 ์‚ฌ์ด์—๋Š” ์ง€์—ญ ์„ค์ •์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST) ; private final String message;