Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public interface DiaryRepository extends Repository<Diary, Long> {

Optional<Diary> findByDiaryIdAndUserUserId(Long diaryId, Long userId);

Diary save(Diary diary);

Optional<Diary> findById(Long diaryId);

default Diary getDiary(Long diaryId, Long userId) {
return findByDiaryIdAndUserUserId(diaryId, userId)
.orElseThrow(() -> new ExceptionList(DiaryErrorCode.DIARY_NOT_FOUND));
Expand Down
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() + "번 일기에 대한 감정 분석이 시작되었습니다.");
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EOF 설정 intellij에 있으니 찾아보고 적용해주세용

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.memoryshade.domain.emotion.dto;

public record EmotionRequest(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뒤에 DTO

그건 차치해도, diaryId를 왜 dto로 받나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api 명세서에 별도로 pathvariable 형태로 정의되어 있지 않아서 그렇게 진행했습니당
아니면 컨트롤러를 /api/emotions/analyze/{diaryId} 요런식으로 바꾸는 방향이 더 낫다고 보나요??

Copy link
Member

@dldb-chamchi dldb-chamchi Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넹 처음 명세서에 작성된 엔드포인트는 diary기준으로 할줄 모르고 초반에 작성해서 좀 차이가 있을 수 있습니다.
dto에 id를 전달하는 것은 dto 자체에 정의에 어긋난다 생각합니당

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • API 엔드포인트는 리소스와 리소스에 대해 수행하는 액션 순이기 때문에

/api/emotions/{diaryId}/analyze 이렇게 가는게 맞다고 생각합니다

Long diaryId // 분석 대상 일기 ID
Copy link
Member

Choose a reason for hiding this comment

The 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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

백엔드 코드는 camel but 모바일을 위해 snake case 적용(jsonProperty 찾아보시면 좋을거 같아요)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 클래스명에 Dto

Double confidence,
Map<String, Double> emotions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 지금 이거 구조체를 넘기는건가요...? 뭔지 설명해주실수 있나요 잘 이해를 못함... 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 AI 모델이 확신도 점수를 담은 맵이에요, 모바일에서 각 감정별 수치를 보여주기 위해 키-값 형태 구조체로 설계했어요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 굿굿~ 근데 감정은 열거형으로 정의하는게 더 좋을거 같아용

) {}
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,32 @@ public class EmotionAnalysis {
@Column(name = "anger_score")
private Integer angerScore;

@Column(name = "anxiety_score") // 추가
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 컬럼 DB 다이어그램 V2 페이지에 반영됐나용? 혹시 까먹으셨다면 지금! 🙇

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diary를 통해 찾는건가요? 속성관련해서 찾는게 아닌? 어떤 메소드인가요?


void delete(EmotionAnalysis emotionAnalysis);

void flush();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? 이건 어디다가 쓰나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 과정에서 남은 코드인데 삭제하겠습니당

Copy link
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작성된 다른 코드 보고 컨벤션 맞춰주세요. create, update, get, delete

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 이거 왜 와일드카드 쓰나요? double아닌가요


EmotionAnalysis analysis = EmotionAnalysis.builder()
Copy link
Member

Choose a reason for hiding this comment

The 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로 변환.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 왜 필요한건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai_server.py에서는 double 형태로 결과값이 넘어와서 넣었어요. 개발 많이 완료됐을 때 refactor 과정에서 손보도록 하겠습니다

Copy link
Member

@dldb-chamchi dldb-chamchi Mar 17, 2026

Choose a reason for hiding this comment

The 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
Expand Up @@ -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() // 임시 허용
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

임시 허용 굳이 이유 있나요? 혹시 잘 안되나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원가입 - 로그인 - authen token 넣었는데도 저거 안 넣어주면 403 에러가 뜨더라고요,..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이따 보여주세용~

.requestMatchers(
"/swagger-ui/**",
"/swagger-ui.html",
Expand Down