Skip to content

Commit edb3ade

Browse files
authored
feat: 예외 처리 추가
1 parent ba7f000 commit edb3ade

1 file changed

Lines changed: 257 additions & 2 deletions

File tree

src/main/java/com/howWeather/howWeather_backend/domain/member/service/MyAccountService.java

Lines changed: 257 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,85 @@ public void updateLocation(Member member, RegionDto regionDto) {
195195
}
196196

197197
/**
198-
* 단일 회원에 대해 스케줄러 AI 예측 전송 로직 재사용
198+
* 단일 회원에 대해 AI 예측을 요청하고 결과를 저장합니다.
199+
* 응답이 없거나 처리 실패 시 Fallback 로직(지역명 업데이트)을 시도합니다.
200+
* @param member 대상 회원 객체
201+
* @param region 새로운 지역명
199202
*/
200203
private void pushPredictionForSingleMember(Member member, String region) {
204+
Long memberId = member.getId(); // memberId 변수 사용
205+
try {
206+
if (member.getCloset() == null) {
207+
log.warn("[AI 예측 생략] memberId={} 클로젯 정보 없음", memberId);
208+
return;
209+
}
210+
211+
AiPredictionRequestDto dto = aiInternalService.makePredictRequest(member);
212+
if (dto == null) {
213+
log.warn("[AI 예측 데이터 없음] memberId={}", memberId);
214+
// AI 요청 데이터 생성 실패 시 Fallback 실행
215+
handleFallbackForMissingResponse(memberId, region);
216+
return;
217+
}
218+
219+
// 날씨 정보 정렬
220+
List<WeatherPredictDto> sortedWeather = Optional.ofNullable(dto.getWeatherForecast()).orElseGet(Collections::emptyList)
221+
.stream()
222+
.sorted(Comparator.comparingInt(WeatherPredictDto::getHour))
223+
.toList();
224+
dto.setWeatherForecast(sortedWeather);
225+
226+
log.info("[AI 예측 DTO 확인] memberId={}, bodyType={}, weatherForecast={}, clothingCombinations={}",
227+
memberId, dto.getBodyTypeLabel(), dto.getWeatherForecast(), dto.getClothingCombinations());
228+
229+
// 데이터 암호화
230+
Map<String, String> encryptedData = encryptPredictionData(List.of(dto));
231+
if (encryptedData == null || encryptedData.isEmpty()) {
232+
log.warn("[AI 예측 생략] memberId={} 데이터 암호화 실패 또는 결과 없음", memberId);
233+
// 암호화 실패 시 Fallback 실행
234+
handleFallbackForMissingResponse(memberId, region);
235+
return;
236+
}
237+
238+
// AI 서버 요청
239+
HttpHeaders headers = new HttpHeaders();
240+
headers.setContentType(MediaType.APPLICATION_JSON);
241+
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(encryptedData, headers);
242+
ResponseEntity<Map> response = restTemplate.postForEntity(aiServerUrl, requestEntity, Map.class);
243+
244+
// 응답 상태 확인
245+
if (!response.getStatusCode().is2xxSuccessful()) {
246+
log.warn("[AI 서버 응답 실패] memberId={}, 상태코드={}, 본문={}",
247+
memberId, response.getStatusCode(), response.getBody());
248+
// AI 서버 응답 실패 시 Fallback 실행
249+
handleFallbackForMissingResponse(memberId, region);
250+
return;
251+
}
252+
253+
// 응답 본문 확인
254+
Map<String, String> encryptedResponseData = response.getBody();
255+
log.info("[AI 서버 응답 본문] memberId={}, body={}", memberId, encryptedResponseData);
256+
if (encryptedResponseData == null || encryptedResponseData.isEmpty()) {
257+
log.warn("[AI 서버 응답 없음] memberId={}, 응답 본문 null/empty.", memberId);
258+
// 응답 본문 없을 시 Fallback 실행
259+
handleFallbackForMissingResponse(memberId, region);
260+
return;
261+
}
262+
263+
saveRecommendationsInternal(encryptedResponseData, memberId, region); // memberId 추가
264+
265+
} catch (Exception e) {
266+
// AI 예측 요청/처리 중 발생한 모든 예외 처리
267+
log.error("[AI 예측 처리 실패] memberId={}, message={}", memberId, e.getMessage(), e);
268+
// ✨ 예측 처리 자체 실패 시에도 Fallback 실행 ✨
269+
handleFallbackForMissingResponse(memberId, region);
270+
}
271+
}
272+
273+
/**
274+
* 단일 회원에 대해 스케줄러 AI 예측 전송 로직 재사용
275+
*/
276+
private void pushOldPredictionForSingleMember(Member member, String region) {
201277
try {
202278
if (member.getCloset() == null) {
203279
log.warn("[AI 예측 생략] memberId={} 클로젯 정보 없음", member.getId());
@@ -301,8 +377,187 @@ private Map<String, String> encryptPredictionData(List<AiPredictionRequestDto> d
301377
}
302378
}
303379

380+
/**
381+
* AI 서버로부터 받은 암호화된 추천 데이터를 복호화하고 저장합니다.
382+
* JSON 파싱 오류 발생 시, 에러 로그만 남기고 마치 'AI 결과 없음' 시나리오처럼
383+
* 기존 데이터의 지역명을 업데이트하고 정상 종료합니다.
384+
*
385+
* @param encryptedData 암호화된 데이터 Map ("iv", "payload" 포함)
386+
* @param memberId 대상 사용자 ID (Fallback 시 사용)
387+
* @param newRegionName 업데이트할 새로운 지역명
388+
*/
389+
@Transactional
390+
public void saveRecommendationsInternal(Map<String, String> encryptedData, Long memberId, String newRegionName) { // memberId 파라미터 추가
391+
// --- 1. 입력 데이터 기본 유효성 검사 ---
392+
if (encryptedData == null || encryptedData.isEmpty()) {
393+
log.warn("[saveInternal] memberId={} 전달된 데이터 맵 null/empty. 처리 중단.", memberId);
394+
return;
395+
}
396+
if (!encryptedData.containsKey("iv") || !encryptedData.containsKey("payload")) {
397+
log.warn("[saveInternal] memberId={} 필수 키 'iv'/'payload' 없음. 응답: {}. 처리 중단.", memberId, encryptedData);
398+
return;
399+
}
400+
String iv = encryptedData.get("iv"); String payload = encryptedData.get("payload");
401+
if (iv == null || iv.isBlank() || payload == null || payload.isBlank()) {
402+
log.warn("[saveInternal] memberId={} 'iv'/'payload' 값 null/blank. 응답: {}. 처리 중단.", memberId, encryptedData);
403+
return;
404+
}
405+
406+
String decryptedJson = null; // 로그용 변수
407+
boolean errorOccurred = false; // 에러 발생 여부 플래그
408+
boolean dataProcessed = false; // DB 변경 발생 여부 플래그
409+
410+
try {
411+
// --- 2. 복호화 ---
412+
log.info("[saveInternal] memberId={} 복호화 시도...", memberId);
413+
decryptedJson = aesCipher.decrypt(encryptedData);
414+
log.debug("[saveInternal] memberId={} 복호화된 JSON: {}", memberId, decryptedJson);
415+
416+
// --- 3. JSON 파싱 시도 (별도 try-catch) ---
417+
ModelClothingRecommendationDto[] dtoArray;
418+
try {
419+
if (decryptedJson.startsWith("\"") && decryptedJson.endsWith("\"")) {
420+
decryptedJson = objectMapper.readValue(decryptedJson, String.class);
421+
}
422+
dtoArray = objectMapper.readValue(
423+
decryptedJson, ModelClothingRecommendationDto[].class);
424+
log.info("[saveInternal] memberId={} 파싱 성공 ({}개 항목)", memberId, dtoArray.length);
425+
426+
} catch (Exception parsingException) {
427+
// ✨ 파싱 실패 시: 로그 남기고 에러 플래그 설정 후 catch 블록 밖으로 나감 ✨
428+
log.error("[saveInternal] memberId={} JSON 파싱 실패. Fallback 로직 실행 예정. 에러: {}", memberId, parsingException.getMessage());
429+
if (decryptedJson != null) {
430+
log.error("[saveInternal] memberId={} 파싱 실패 시 원본 JSON: {}", memberId, decryptedJson);
431+
}
432+
errorOccurred = true; // 에러 발생 플래그 설정
433+
dtoArray = new ModelClothingRecommendationDto[0]; // 빈 배열로 초기화 (아래 로직 진행 위함)
434+
}
435+
436+
// --- 4. 데이터 처리 (파싱 성공 시) 또는 Fallback 실행 ---
437+
if (!errorOccurred) {
438+
// 파싱 성공 시 기존 로직 실행
439+
LocalDate today = LocalDate.now();
440+
for (ModelClothingRecommendationDto dto : dtoArray) {
441+
// userId 일치 확인 등 기존 로직 수행...
442+
if (!memberId.equals(dto.getUserId())) {
443+
log.warn("[saveInternal] DTO userId({}) 불일치 (대상 memberId={}). 건너<0xEB><0x9B><0x84>.", dto.getUserId(), memberId);
444+
continue;
445+
}
446+
Member member = memberRepository.findById(dto.getUserId())
447+
.orElseThrow(() -> new CustomException(ErrorCode.ID_NOT_FOUND));
448+
449+
List<ModelRecommendationResult> results = Optional.ofNullable(dto.getResult()).orElseGet(ArrayList::new);
450+
451+
if (!results.isEmpty()) {
452+
// 새 추천 결과 저장
453+
recommendationRepository.deleteByMemberIdAndDate(member.getId(), today);
454+
for (ModelRecommendationResult r : results) {
455+
ClothingRecommendation entity = convertToEntityWithBuilder(r, member.getId(), newRegionName);
456+
recommendationRepository.save(entity);
457+
}
458+
log.info("[추천 데이터 저장 완료] memberId={}", member.getId());
459+
dataProcessed = true;
460+
} else {
461+
// AI 결과 비었을 때 기존 데이터 지역명 업데이트
462+
dataProcessed = handleEmptyAiResult(member, newRegionName) || dataProcessed;
463+
}
464+
} // End of for loop
465+
} else { // ✨ errorOccurred가 true일 때 (파싱 실패 시) ✨
466+
// Fallback 실행: "AI 결과 없음" 시나리오와 동일하게 기존 데이터 지역명 업데이트 시도
467+
log.info("[Fallback] memberId={} 파싱 실패로 기존 데이터 지역명 업데이트 시도.", memberId);
468+
Member member = memberRepository.findById(memberId).orElse(null); // memberId로 Member 조회
469+
if (member != null) {
470+
// handleEmptyAiResult 호출하여 지역명 업데이트
471+
dataProcessed = handleEmptyAiResult(member, newRegionName) || dataProcessed; // 여기도 dataProcessed 업데이트
472+
} else {
473+
log.warn("[Fallback] memberId={} 사용자 정보를 찾을 수 없어 지역명 업데이트 불가.", memberId);
474+
}
475+
}
476+
477+
// DB 변경이 있었을 경우 flush/clear
478+
if (dataProcessed) {
479+
recommendationRepository.flush();
480+
entityManager.clear();
481+
}
482+
483+
} catch (CustomException e) {
484+
// ID_NOT_FOUND 등 CustomException은 그대로 던짐
485+
log.error("[saveInternal] 처리 중 CustomException 발생. memberId={} message={}", memberId, e.getMessage(), e);
486+
throw e;
487+
} catch (Exception e) {
488+
// 복호화 실패 등 파싱 외의 예상 못한 Exception 처리
489+
log.error("[saveInternal] 처리 중 예상 못한 Exception 발생. memberId={} message={}", memberId, e.getMessage(), e);
490+
throw new CustomException(ErrorCode.UNKNOWN_ERROR, "추천 데이터 처리 중 오류 발생: " + e.getMessage());
491+
}
492+
}
493+
494+
/**
495+
* (Helper) AI 결과가 비었거나 파싱 실패 시 기존 추천 데이터의 지역명만 업데이트하는 로직
496+
* @return 업데이트 성공(변경 발생) 여부
497+
*/
498+
private boolean handleEmptyAiResult(Member member, String newRegionName) {
499+
LocalDate today = LocalDate.now();
500+
List<ClothingRecommendation> existingData = recommendationRepository.findByMemberIdAndDate(member.getId(), today);
501+
boolean updated = false; // 업데이트 발생 여부 플래그
502+
if (!existingData.isEmpty()) {
503+
for (ClothingRecommendation rec : existingData) {
504+
// 지역명이 이미 동일하면 업데이트 건너뛰기
505+
if (newRegionName.equals(rec.getRegionName())) {
506+
log.debug("[handleEmptyAiResult] memberId={} 지역명({}) 이미 최신. 업데이트 불필요.", member.getId(), newRegionName);
507+
continue; // 다음 레코드로
508+
}
509+
// 엔티티 빌더를 사용하여 업데이트 객체 생성 (null 방어 포함)
510+
ClothingRecommendation updatedEntity = ClothingRecommendation.builder()
511+
.id(rec.getId()) // 기존 ID 유지
512+
.memberId(rec.getMemberId())
513+
.regionName(newRegionName) // 새 지역명 설정
514+
.tops(Optional.ofNullable(rec.getTops()).orElseGet(ArrayList::new))
515+
.outers(Optional.ofNullable(rec.getOuters()).orElseGet(ArrayList::new))
516+
.predictionMap(Optional.ofNullable(rec.getPredictionMap()).orElseGet(HashMap::new))
517+
.date(today) // 날짜 유지
518+
.build();
519+
recommendationRepository.save(updatedEntity); // 업데이트 실행
520+
updated = true; // 업데이트 발생 표시
521+
}
522+
if (updated) {
523+
log.info("[handleEmptyAiResult] 기존 데이터 지역명 업데이트 완료. memberId={}", member.getId());
524+
// flush/clear는 호출한 쪽에서 관리
525+
}
526+
} else {
527+
log.info("[handleEmptyAiResult] 업데이트할 기존 데이터 없음. memberId={}", member.getId());
528+
}
529+
return updated; // 업데이트 발생 여부 반환
530+
}
531+
532+
/**
533+
* (Helper) AI 서버 응답이 없거나 처리 중 예외 발생 시 Fallback 처리 (기존 데이터 지역명 업데이트)
534+
* @param memberId 사용자 ID
535+
* @param newRegionName 새로운 지역명
536+
*/
537+
private void handleFallbackForMissingResponse(Long memberId, String newRegionName) {
538+
try {
539+
log.warn("[Fallback 시도] memberId={} AI 응답 없거나 처리 실패. 기존 데이터 지역명 업데이트 시도.", memberId);
540+
Member member = memberRepository.findById(memberId).orElse(null);
541+
if (member != null) {
542+
boolean updated = handleEmptyAiResult(member, newRegionName); // 기존 헬퍼 재사용
543+
if (updated) {
544+
recommendationRepository.flush(); // DB 반영
545+
entityManager.clear(); // 영속성 컨텍스트 초기화
546+
log.info("[Fallback 시도] memberId={} 기존 데이터 지역명 업데이트 성공.", memberId);
547+
} else {
548+
log.info("[Fallback 시도] memberId={} 기존 데이터 지역명 업데이트 불필요(이미 최신) 또는 대상 없음.", memberId);
549+
}
550+
} else {
551+
log.warn("[Fallback 실패] memberId={} 사용자 정보를 찾을 수 없음.", memberId);
552+
}
553+
} catch (Exception fallbackEx) {
554+
// Fallback 로직 자체 실패 시 로그만 남김 (예외 숨김)
555+
log.error("[Fallback 실패] memberId={} 기존 데이터 업데이트 중 오류: {}", memberId, fallbackEx.getMessage(), fallbackEx);
556+
}
557+
}
558+
304559
@Transactional
305-
public void saveRecommendationsInternal(Map<String, String> encryptedData, String newRegionName) {
560+
public void saveOldRecommendationsInternal(Map<String, String> encryptedData, String newRegionName) {
306561
if (encryptedData == null || encryptedData.isEmpty()) {
307562
log.warn("[추천 데이터 처리 중단] 전달된 데이터 맵이 null이거나 비어있습니다.");
308563
return;

0 commit comments

Comments
 (0)