Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e4c4c7e
feat: 필요한 의존성 추가
yeonjae02 Jun 29, 2025
44c8d89
feat: dto 구현
yeonjae02 Jun 29, 2025
7f88ab1
feat: 날짜 검증 어노테이션 추가 (어제와 오늘만 허용) 및 적용
yeonjae02 Jun 29, 2025
4b72818
feat: 위경도 데이터를 받아 지역명 및 온도 반환 dto 수정
yeonjae02 Jun 29, 2025
4756897
chore: 카카오 지오코딩 api 관련 에러 코드 추가
yeonjae02 Jun 29, 2025
840e015
feat: 컨트롤러 코드 구현
yeonjae02 Jun 29, 2025
2244602
feat: 서비스 코드 구현
yeonjae02 Jun 29, 2025
bd96249
feat: 멤버에 지역 필드 추가
yeonjae02 Jun 29, 2025
057b210
feat: 지역 dto 생성
yeonjae02 Jun 29, 2025
68c6898
feat: 지역 수정 로직 구현
yeonjae02 Jun 29, 2025
15edcee
chore: 누락한 어노테이션 추가
yeonjae02 Jun 29, 2025
c7c0457
fix: post -> get으로 수정
yeonjae02 Jun 30, 2025
a7c36c9
refactor: 반환 타입 간소화
yeonjae02 Jun 30, 2025
1053507
feat: 완전히 똑같은 지역명을 찾지 못할 때 처리
yeonjae02 Jun 30, 2025
a950fb7
feat: 사용자 위치 조회 로직 구현
yeonjae02 Jul 4, 2025
1b6de10
chore: 커스텀 에러 추가
yeonjae02 Jul 4, 2025
deb4a63
feat: 예보가 필요한 지역만 예보 api 호출하도록 수정
yeonjae02 Jul 4, 2025
53b8fe3
chore: 주석 처리 및 불필요한 log문 삭제
yeonjae02 Jul 4, 2025
678a772
feat: 지역 수정 시 지역 카운트 변경 로직 추가
yeonjae02 Jul 4, 2025
1795980
feat: 기본 지역인 용산구는 무조건 예보를 가져오도록 수정
yeonjae02 Jul 4, 2025
082f312
feat: 사용자가 선택한 지역 데이터를 ai 모델에 보내도록 수정
yeonjae02 Jul 4, 2025
519710e
feat: 포른트에 데이터 전송 시에도 사용자가 선택한 지역으로 보내도록 수정
yeonjae02 Jul 4, 2025
af3577a
feat: 지역 변경 불가 시간대 설정
yeonjae02 Jul 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public ResponseEntity<Map<String, String>> sendAllUsersPredictionDataSecure() {
public ResponseEntity<ApiResponse<String>> saveRecommendations(@RequestBody Map<String, String> encryptedBody) {
try {
String decryptedJson = aesCipher.decrypt(encryptedBody);
log.info("🔓 복호화된 데이터:\n{}", decryptedJson);

if (decryptedJson.startsWith("\"") && decryptedJson.endsWith("\"")) {
decryptedJson = objectMapper.readValue(decryptedJson, String.class);
Expand Down Expand Up @@ -118,34 +117,33 @@ private Map<String, String> encryptPredictionData(List<AiPredictionRequestDto> d
}

// TODO : 암/복호화 API 연결 이후 아래 코드 삭제

@PostMapping("/prediction")
public ResponseEntity<List<AiPredictionRequestDto>> sendAllUsersPredictionData() {
try {
List<Member> members = memberRepository.findAllByIsDeletedFalse();

List<AiPredictionRequestDto> 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<ApiResponse<String>> saveRecommendations(@RequestBody ModelClothingRecommendationDto dto) {
recommendationService.save(dto);
return ApiResponse.success(HttpStatus.OK, "예측 결과를 성공적으로 저장했습니다.");
}

@PostMapping("/history")
public ResponseEntity<ApiResponse<List<RecordForModelDto>>> getSimilarHistory(@RequestBody HistoryRequestDto dto) {
dto.validate();
List<RecordForModelDto> result = recordCalendarService.getMemberHistory(dto);
return ApiResponse.success(HttpStatus.OK, result);
}
// @PostMapping("/prediction")
// public ResponseEntity<List<AiPredictionRequestDto>> sendAllUsersPredictionData() {
// try {
// List<Member> members = memberRepository.findAllByIsDeletedFalse();
//
// List<AiPredictionRequestDto> 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<ApiResponse<String>> saveRecommendations(@RequestBody ModelClothingRecommendationDto dto) {
// recommendationService.save(dto);
// return ApiResponse.success(HttpStatus.OK, "예측 결과를 성공적으로 저장했습니다.");
// }
//
// @PostMapping("/history")
// public ResponseEntity<ApiResponse<List<RecordForModelDto>>> getSimilarHistory(@RequestBody HistoryRequestDto dto) {
// dto.validate();
// List<RecordForModelDto> result = recordCalendarService.getMemberHistory(dto);
// return ApiResponse.success(HttpStatus.OK, result);
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,8 @@ public void pushPredictionAESDataToAiServer() {
return;
}

ObjectMapper objectMapper = new ObjectMapper();
String plainJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(allDtos);
log.info("✅ 예측 데이터 (평문 JSON):\n{}", plainJson);

Map<String, String> encrypted = encryptPredictionData(allDtos);

String encryptedJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(encrypted);
log.info("🔒 예측 데이터 (암호화된 JSON):\n{}", encryptedJson);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(encrypted, headers);
Expand All @@ -139,7 +132,7 @@ public void pushPredictionAESDataToAiServer() {
private Map<String, String> encryptPredictionData(List<AiPredictionRequestDto> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class AiInternalService {

@Transactional
public AiPredictionRequestDto makePredictRequest(Member member) {
List<WeatherPredictDto> dtoList = getWeatherForecast();
List<WeatherPredictDto> dtoList = getWeatherForecast(member);
return AiPredictionRequestDto.builder()
.userId(member.getId())
.bodyTypeLabel(member.getConstitution())
Expand All @@ -37,11 +37,12 @@ public AiPredictionRequestDto makePredictRequest(Member member) {
.build();
}

private List<WeatherPredictDto> getWeatherForecast() {
private List<WeatherPredictDto> getWeatherForecast(Member member) {
List<Integer> targetHours = List.of(9, 12, 15, 18, 21);
String regionName = member.getRegionName() != null ? member.getRegionName() : "서울특별시 용산구";

List<WeatherForecast> forecasts = weatherForecastRepository
.findByRegionNameAndForecastDateAndHourIn("서울특별시 용산구", LocalDate.now(), targetHours);
.findByRegionNameAndForecastDateAndHourIn(regionName, LocalDate.now(), targetHours);

return forecasts.stream()
.map(this::convertToDto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ public List<RecommendPredictDto> getRecommendList(Member member) {
Closet closet = getClosetWithAll(member);
List<RecommendPredictDto> 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<Integer> upperTypeList = makeUpperList(closet, recommendation.getTops());
List<Integer> outerTypeList = makeOuterList(closet, recommendation.getOuters());
List<WeatherFeelingDto> weatherFeelingDto = makeWeatherFeeling(recommendation.getPredictionMap());
List<WeatherFeelingDto> weatherFeelingDto = makeWeatherFeeling(recommendation.getPredictionMap(), member);

return RecommendPredictDto.builder()
.feelingList(weatherFeelingDto)
Expand All @@ -65,7 +65,6 @@ private RecommendPredictDto makeResultForPredict(Closet closet, ClothingRecommen
throw new CustomException(ErrorCode.UNKNOWN_ERROR, "예측 데이터를 변환하는 중 오류가 발생하였습니다: " + e.getMessage());
}
}

// TODO : AI 학습 이후 사용자가 대량으로 의상을 삭제해서 추천할 의상이 없어져버리는 경우에 대한 고민이 필요해보임.

private List<Integer> makeOuterList(Closet closet, List<Integer> outers) {
Expand All @@ -85,7 +84,6 @@ private List<Integer> makeOuterList(Closet closet, List<Integer> outers) {
}
}

// if (result.isEmpty()) throw new CustomException(ErrorCode.NO_RECOMMEND_OUTER);
return new ArrayList<>(resultSet);
}

Expand All @@ -103,18 +101,17 @@ private List<Integer> makeUpperList(Closet closet, List<Integer> tops) {
}
}
}
// if (result.isEmpty()) throw new CustomException(ErrorCode.NO_RECOMMEND_UPPER);
return new ArrayList<>(resultSet);
}

private List<WeatherFeelingDto> makeWeatherFeeling(Map<String, Integer> predictionMap) {
private List<WeatherFeelingDto> makeWeatherFeeling(Map<String, Integer> predictionMap, Member member) {
List<WeatherFeelingDto> 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<Integer> hours = predictionMap.keySet().stream()
.map(Integer::parseInt)
Expand All @@ -123,12 +120,11 @@ private List<WeatherFeelingDto> makeWeatherFeeling(Map<String, Integer> predicti
List<WeatherForecast> forecasts = weatherForecastRepository
.findByRegionNameAndForecastDateAndHourIn(regionName, forecastDate, hours);

// hour 기준으로 forecast 객체를 통째로 저장 (중복 방지 및 날짜 포함용)
Map<Integer, WeatherForecast> hourToForecastMap = forecasts.stream()
.collect(Collectors.toMap(
WeatherForecast::getHour,
forecast -> forecast,
(oldVal, newVal) -> newVal // 중복 시간대가 있다면 최신 데이터로 덮기
(oldVal, newVal) -> newVal
));

for (Map.Entry<String, Integer> entry : predictionMap.entrySet()) {
Expand All @@ -138,12 +134,11 @@ private List<WeatherFeelingDto> makeWeatherFeeling(Map<String, Integer> 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);
Expand All @@ -153,14 +148,12 @@ private List<WeatherFeelingDto> makeWeatherFeeling(Map<String, Integer> predicti
return feelingList;
}


private List<ClothingRecommendation> getModelPrediction(Long id, LocalDate now) {
List<ClothingRecommendation> list = clothingRecommendationRepository.findByMemberIdAndDate(id, now);
if (list.isEmpty()) {
throw new CustomException(ErrorCode.NO_PREDICT_DATA);
}
return list;
// return clothingRecommendationRepository.findByMemberIdAndDate(id, now);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<String>> 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<ApiResponse<RegionTemperatureDto>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading