-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 감정 분석 기능 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: 감정 분석 기능 #16
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> 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() + "번 일기에 대한 감정 분석이 시작되었습니다."); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.memoryshade.domain.emotion.dto; | ||
|
|
||
| public record EmotionRequest( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뒤에 DTO 그건 차치해도, diaryId를 왜 dto로 받나요??
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. api 명세서에 별도로 pathvariable 형태로 정의되어 있지 않아서 그렇게 진행했습니당
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넹 처음 명세서에 작성된 엔드포인트는 diary기준으로 할줄 모르고 초반에 작성해서 좀 차이가 있을 수 있습니다.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
/api/emotions/{diaryId}/analyze 이렇게 가는게 맞다고 생각합니다 |
||
| Long diaryId // 분석 대상 일기 ID | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 작성한 다른 DTO 보고 JsonProperty 설정 해주세요. 이렇게 camelCase면 모바일이 불편해용 |
||
| ) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 백엔드 코드는 camel but 모바일을 위해 snake case 적용(jsonProperty 찾아보시면 좋을거 같아요)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Double confidence, | ||
| Map<String, Double> emotions | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 근데 지금 이거 구조체를 넘기는건가요...? 뭔지 설명해주실수 있나요 잘 이해를 못함... 👀
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네네 AI 모델이 확신도 점수를 담은 맵이에요, 모바일에서 각 감정별 수치를 보여주기 위해 키-값 형태 구조체로 설계했어요
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 굿굿~ 근데 감정은 열거형으로 정의하는게 더 좋을거 같아용 |
||
| ) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,19 +32,32 @@ public class EmotionAnalysis { | |
| @Column(name = "anger_score") | ||
| private Integer angerScore; | ||
|
|
||
| @Column(name = "anxiety_score") // 추가 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 컬럼 DB 다이어그램 V2 페이지에 반영됐나용? 혹시 까먹으셨다면 지금! 🙇
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반영했습니다!! ✅ |
||
| private Integer anxietyScore; | ||
|
|
||
| @Column(name = "embarrassment_score") // 추가 | ||
| private Integer embarrassmentScore; | ||
|
|
||
| @Column(name = "hurt_score") // 추가 | ||
| private Integer hurtScore; | ||
|
|
||
| @Column(name = "neutral_score") | ||
| private Integer neutralScore; | ||
|
|
||
| @Column(name = "analyzed_at", nullable = false, updatable = false) | ||
| 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, Long> { | ||
| EmotionAnalysis save(EmotionAnalysis emotionAnalysis); | ||
|
|
||
| Optional<EmotionAnalysis> findByDiary(Diary diary); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Diary를 통해 찾는건가요? 속성관련해서 찾는게 아닌? 어떤 메소드인가요? |
||
|
|
||
| void delete(EmotionAnalysis emotionAnalysis); | ||
|
|
||
| void flush(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ?? 이건 어디다가 쓰나요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정 과정에서 남은 코드인데 삭제하겠습니당
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 밑에 있는거 같던데 그러면 안쓰는 코드인가요?? |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 작성된 다른 코드 보고 컨벤션 맞춰주세요. create, update, get, delete
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 Diary 전문 자체를 받는데 이것도 따로 Dto를 만들어야할거 같네요. 그리고 text는 무엇인가요? |
||
| try { | ||
| // FastAPI AI 서버 호출 | ||
| EmotionResponse response = restTemplate.postForObject( | ||
| aiServerUrl, | ||
| Map.of("text", text), | ||
| EmotionResponse.class | ||
| ); | ||
|
Comment on lines
+34
to
+38
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동작 자체가 이해가 안가는게 많아서... 이따 설명해주시면 감사하겠습니다 |
||
|
|
||
| // 필터링 로직: 중립/모호함이 아닐 때만 저장 진행 | ||
| 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<String, ?> scores = res.emotions(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 근데 이거 왜 와일드카드 쓰나요? double아닌가요 |
||
|
|
||
| EmotionAnalysis analysis = EmotionAnalysis.builder() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. response에서 메소드 만들고 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로 변환. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 왜 필요한건가요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ai_server.py에서는 double 형태로 결과값이 넘어와서 넣었어요. 개발 많이 완료됐을 때 refactor 과정에서 손보도록 하겠습니다
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 그럼 double -> integr변환인가여 |
||
| */ | ||
| 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() // 임시 허용 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 임시 허용 굳이 이유 있나요? 혹시 잘 안되나요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 회원가입 - 로그인 - authen token 넣었는데도 저거 안 넣어주면 403 에러가 뜨더라고요,..
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이따 보여주세용~ |
||
| .requestMatchers( | ||
| "/swagger-ui/**", | ||
| "/swagger-ui.html", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EOF 설정 intellij에 있으니 찾아보고 적용해주세용