diff --git a/src/main/java/com/memoryshade/domain/diary/repository/DiaryRepository.java b/src/main/java/com/memoryshade/domain/diary/repository/DiaryRepository.java index 83db24b..54dc3bd 100644 --- a/src/main/java/com/memoryshade/domain/diary/repository/DiaryRepository.java +++ b/src/main/java/com/memoryshade/domain/diary/repository/DiaryRepository.java @@ -13,6 +13,10 @@ public interface DiaryRepository extends Repository { Optional findByDiaryIdAndUserUserId(Long diaryId, Long userId); + Diary save(Diary diary); + + Optional findById(Long diaryId); + default Diary getDiary(Long diaryId, Long userId) { return findByDiaryIdAndUserUserId(diaryId, userId) .orElseThrow(() -> new ExceptionList(DiaryErrorCode.DIARY_NOT_FOUND)); diff --git a/src/main/java/com/memoryshade/domain/emotion/controller/EmotionController.java b/src/main/java/com/memoryshade/domain/emotion/controller/EmotionController.java new file mode 100644 index 0000000..bcaee94 --- /dev/null +++ b/src/main/java/com/memoryshade/domain/emotion/controller/EmotionController.java @@ -0,0 +1,29 @@ +package com.memoryshade.domain.emotion.controller; + +import com.memoryshade.domain.diary.exception.DiaryErrorCode; +import com.memoryshade.domain.diary.model.Diary; +import com.memoryshade.domain.diary.repository.DiaryRepository; +import com.memoryshade.domain.emotion.dto.EmotionRequest; +import com.memoryshade.domain.emotion.service.EmotionService; +import com.memoryshade.global.exception.ExceptionList; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/emotions") +@RequiredArgsConstructor +public class EmotionController { + + private final EmotionService emotionService; + private final DiaryRepository diaryRepository; + @PostMapping("/analyze") + public ResponseEntity analyze(@RequestBody EmotionRequest request) { + Diary diary = diaryRepository.findById(request.diaryId()) + .orElseThrow(() -> new ExceptionList(DiaryErrorCode.DIARY_NOT_FOUND)); + + emotionService.analyzeAndSave(diary, diary.getContentStt()); + + return ResponseEntity.ok(request.diaryId() + "번 일기에 대한 감정 분석이 시작되었습니다."); + } +} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/domain/emotion/dto/EmotionRequest.java b/src/main/java/com/memoryshade/domain/emotion/dto/EmotionRequest.java new file mode 100644 index 0000000..5148489 --- /dev/null +++ b/src/main/java/com/memoryshade/domain/emotion/dto/EmotionRequest.java @@ -0,0 +1,5 @@ +package com.memoryshade.domain.emotion.dto; + +public record EmotionRequest( + Long diaryId // 분석 대상 일기 ID +) {} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/domain/emotion/dto/EmotionResponse.java b/src/main/java/com/memoryshade/domain/emotion/dto/EmotionResponse.java new file mode 100644 index 0000000..79eab9b --- /dev/null +++ b/src/main/java/com/memoryshade/domain/emotion/dto/EmotionResponse.java @@ -0,0 +1,15 @@ +package com.memoryshade.domain.emotion.dto; + +import java.util.Map; + +/** + * AI 서버(FastAPI)의 감정 분석 응답을 담는 DTO + * @param top_emotion 가장 높게 분석된 감정 레이블 + * @param confidence 해당 감정의 확신도 (0~100) + * @param emotions 전체 6가지 감정별 점수 리스트 + */ +public record EmotionResponse( + String top_emotion, + Double confidence, + Map emotions +) {} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/domain/emotion/model/EmotionAnalysis.java b/src/main/java/com/memoryshade/domain/emotion/model/EmotionAnalysis.java index ba46c40..0214e25 100644 --- a/src/main/java/com/memoryshade/domain/emotion/model/EmotionAnalysis.java +++ b/src/main/java/com/memoryshade/domain/emotion/model/EmotionAnalysis.java @@ -32,6 +32,15 @@ public class EmotionAnalysis { @Column(name = "anger_score") private Integer angerScore; + @Column(name = "anxiety_score") // 추가 + private Integer anxietyScore; + + @Column(name = "embarrassment_score") // 추가 + private Integer embarrassmentScore; + + @Column(name = "hurt_score") // 추가 + private Integer hurtScore; + @Column(name = "neutral_score") private Integer neutralScore; @@ -39,12 +48,16 @@ public class EmotionAnalysis { private LocalDateTime analyzedAt; @Builder - public EmotionAnalysis(Diary diary, Integer joyScore, Integer sadnessScore, Integer angerScore, Integer neutralScore) { - this.diary = diary; - this.joyScore = joyScore; - this.sadnessScore = sadnessScore; - this.angerScore = angerScore; - this.neutralScore = neutralScore; - this.analyzedAt = LocalDateTime.now(); - } + public EmotionAnalysis(Diary diary, Integer joyScore, Integer sadnessScore, Integer angerScore, + Integer anxietyScore, Integer embarrassmentScore, Integer hurtScore, Integer neutralScore) { + this.diary = diary; + this.joyScore = joyScore; + this.sadnessScore = sadnessScore; + this.angerScore = angerScore; + this.anxietyScore = anxietyScore; + this.embarrassmentScore = embarrassmentScore; + this.hurtScore = hurtScore; + this.neutralScore = neutralScore; + this.analyzedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/memoryshade/domain/emotion/repository/EmotionAnalysisRepository.java b/src/main/java/com/memoryshade/domain/emotion/repository/EmotionAnalysisRepository.java new file mode 100644 index 0000000..116de2e --- /dev/null +++ b/src/main/java/com/memoryshade/domain/emotion/repository/EmotionAnalysisRepository.java @@ -0,0 +1,16 @@ +package com.memoryshade.domain.emotion.repository; + +import com.memoryshade.domain.diary.model.Diary; +import com.memoryshade.domain.emotion.model.EmotionAnalysis; +import org.springframework.data.repository.Repository; // 기본 Repository 사용 +import java.util.Optional; + +public interface EmotionAnalysisRepository extends Repository { + EmotionAnalysis save(EmotionAnalysis emotionAnalysis); + + Optional findByDiary(Diary diary); + + void delete(EmotionAnalysis emotionAnalysis); + + void flush(); +} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/domain/emotion/service/EmotionService.java b/src/main/java/com/memoryshade/domain/emotion/service/EmotionService.java new file mode 100644 index 0000000..ac77d58 --- /dev/null +++ b/src/main/java/com/memoryshade/domain/emotion/service/EmotionService.java @@ -0,0 +1,98 @@ +package com.memoryshade.domain.emotion.service; + +import com.memoryshade.domain.diary.model.Diary; +import com.memoryshade.domain.emotion.dto.EmotionResponse; +import com.memoryshade.domain.emotion.model.EmotionAnalysis; +import com.memoryshade.domain.emotion.repository.EmotionAnalysisRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmotionService { + + private final EmotionAnalysisRepository emotionAnalysisRepository; + private final RestTemplate restTemplate; + + @Value("${ai.server.url}") + private String aiServerUrl; + + /** + * AI 서버에 분석을 요청하고 결과를 저장 + */ + @Transactional + public void analyzeAndSave(Diary diary, String text) { + try { + // FastAPI AI 서버 호출 + EmotionResponse response = restTemplate.postForObject( + aiServerUrl, + Map.of("text", text), + EmotionResponse.class + ); + + // 필터링 로직: 중립/모호함이 아닐 때만 저장 진행 + if (response != null && isReliable(response.top_emotion())) { + saveEmotionAnalysis(diary, response); + log.info("Diary ID: {} - 감정 분석 저장 완료 ({})", diary.getDiaryId(), response.top_emotion()); + } else { + log.info("Diary ID: {} - 감정이 모호하여 DB 저장을 건너뜀", diary.getDiaryId()); + } + } catch (Exception e) { + log.error("AI 감정 분석 중 오류 발생: {}", e.getMessage()); + } + } + + private boolean isReliable(String topEmotion) { + return !topEmotion.contains("중립") && !topEmotion.contains("모호함"); + } + + /** + * AI 응답을 엔티티로 변환하여 DB에 저장합니다. + * 기존에 동일한 Diary ID로 저장된 데이터가 있다면 삭제 후 갱신합니다. + */ + private void saveEmotionAnalysis(Diary diary, EmotionResponse res) { + log.info("AI 응답 데이터: {}", res.emotions()); // 수신 데이터 로그 확인 + + emotionAnalysisRepository.findByDiary(diary).ifPresent(existing -> { + log.info("Diary ID: {}에 대한 기존 분석 결과 삭제 중...", diary.getDiaryId()); + emotionAnalysisRepository.delete(existing); + emotionAnalysisRepository.flush(); // 즉시 반영하여 중복 제약 조건 충돌 방지 + }); + + Map scores = res.emotions(); + + EmotionAnalysis analysis = EmotionAnalysis.builder() + .diary(diary) + .joyScore(convertToInteger(scores.get("기쁨"))) + .sadnessScore(convertToInteger(scores.get("슬픔"))) + .angerScore(convertToInteger(scores.get("분노"))) + .anxietyScore(convertToInteger(scores.get("불안"))) + .embarrassmentScore(convertToInteger(scores.get("당황"))) + .hurtScore(convertToInteger(scores.get("상처"))) + .build(); + + emotionAnalysisRepository.save(analysis); + } + + /** + * Object 타입 -> Integer로 변환. + */ + private Integer convertToInteger(Object value) { + if (value == null) return 0; + if (value instanceof Number) { + return ((Number) value).intValue(); + } + try { + return Integer.valueOf(value.toString()); + } catch (NumberFormatException e) { + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/global/config/RestTemplateConfig.java b/src/main/java/com/memoryshade/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..97c985c --- /dev/null +++ b/src/main/java/com/memoryshade/global/config/RestTemplateConfig.java @@ -0,0 +1,15 @@ +package com.memoryshade.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +// 스프링부트 to 외부 FastApi 서버 통신 위해서 사용 +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/main/java/com/memoryshade/global/config/SecurityConfig.java b/src/main/java/com/memoryshade/global/config/SecurityConfig.java index 6485396..ecfb6ae 100644 --- a/src/main/java/com/memoryshade/global/config/SecurityConfig.java +++ b/src/main/java/com/memoryshade/global/config/SecurityConfig.java @@ -27,6 +27,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/emotions/**").permitAll() // 임시 허용 .requestMatchers( "/swagger-ui/**", "/swagger-ui.html",