-
Notifications
You must be signed in to change notification settings - Fork 0
feat(report): 오늘의 리포트 생성 API 구현 #59
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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
d2f5dcd
feat(report): emotions, daily_reports 테이블 생성 및 엔티티 구현
1Seob f39bffd
feat(report): daily_reports 테이블에 status 컬럼 추가
1Seob ea26286
feat(report): AnswerEntry, DailyReport 레포지토리 클래스 구현
1Seob f13e2b6
feat: CrystalLogRepository 업데이트
1Seob c2ac61e
feat(report): 오늘의 리포트 생성 API 구현
1Seob 80cd8b5
feat(report): 오늘의 리포트 생성하기 api 구현 (2)
1Seob c101226
feat(report): 오늘의 리포트 조회 API 구현
1Seob 4e4edca
feat: 오늘의 질문 응답 DTO에 관심 주제 코드 필드 추가
1Seob 8393d78
refactor(report): 스웨거 문서화, 메소드명 리팩토링
1Seob 39fa06f
refactor: 오늘의 질문 응답 스웨거 문서화 개선
1Seob c67595a
refactor(report): 증복 코드 리팩토링
1Seob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
178 changes: 178 additions & 0 deletions
178
src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/DailyReportController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.api; | ||
|
|
||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.DailyReportRequest; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.TestDailyReportRequest; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.CreateDailyReportResponse; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.DailyReportResponse; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.TestDailyReportResponse; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.application.DailyReportQueryService; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.application.DailyReportService; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.service.TestDailyReportService; | ||
| import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; | ||
| import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; | ||
| import com.devkor.ifive.nadab.global.security.principal.UserPrincipal; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.annotation.security.PermitAll; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
|
|
||
| @Tag(name = "오늘의 리포트 API", description = "오늘의 리포트 생성 및 조회 관련 API") | ||
| @RestController | ||
| @RequestMapping("${api_prefix}/daily-report") | ||
| @RequiredArgsConstructor | ||
| public class DailyReportController { | ||
|
|
||
| private final TestDailyReportService testDailyReportService; | ||
| private final DailyReportService dailyReportService; | ||
| private final DailyReportQueryService dailyReportQueryService; | ||
|
|
||
| @PostMapping("/generate/test") | ||
| @PermitAll | ||
| @Operation( | ||
| summary = "(테스트용) 오늘의 리포트 생성 API", | ||
| description = """ | ||
| 오늘의 리포트 생성 테스트입니다. 이하의 내용을 지켜 프롬프트를 입력해주세요(기존의 프롬프트를 참고해주세요). | ||
| 1. | ||
| 출력 형식은 반드시 다음과 같도록 프롬프트에 작성해야 합니다: | ||
| ```json | ||
| { | ||
| "message": "(분석 내용)", | ||
| "emotion": "(감정 키워드)" | ||
| } | ||
| ``` | ||
| 2. | ||
| 분석 대상을 명시해야 합니다. | ||
| 예시) | ||
| ```json | ||
| [분석 대상] | ||
| 질문: {question} | ||
| 답변: {answer} | ||
| ``` | ||
| 이하는 temperature에 대한 설명입니다.<br/> | ||
| temperature는 AI가 응답을 생성할 때 얼마나 자유롭게(창의적으로) 단어와 표현을 선택할지를 조절하는 값입니다.<br/> | ||
| 값이 낮을수록 항상 비슷하고 예측 가능한 답변을 생성하며, 값이 높을수록 다양한 표현과 새로운 관점이 섞인 답변을 생성합니다.<br/> | ||
| 허용 가능한 값의 범위는 0.0 이상 1.0 이하이며, 일반적으로 0.0에 가까울수록 사실 전달·요약·분석과 같은 정형적인 작업에 적합하고, 0.6 이상부터는 감정 표현이나 공감, 창의적인 문장 생성에 더 적합해집니다.<br/> | ||
| 다만 temperature가 높아질수록 응답의 일관성이 낮아지고, 정해진 형식(JSON 등)을 지키지 못할 가능성도 함께 증가합니다.<br/> | ||
| 따라서 구조화된 결과나 안정적인 응답이 필요한 경우에는 0.0~0.3, 자연스럽고 감정적인 표현이 중요한 경우에는 0.4~0.8 범위 내에서 사용하는 것을 권장합니다.<br/> | ||
| """, | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "테스트용 오늘의 리포트 생성 성공", | ||
| content = @Content(schema = @Schema(implementation = CreateDailyReportResponse.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "400", | ||
| description = "잘못된 요청" | ||
| ), | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<TestDailyReportResponse>> generateDailyReport( | ||
| @Valid @RequestBody TestDailyReportRequest request, | ||
| @RequestParam String prompt | ||
| ) { | ||
| TestDailyReportResponse response = testDailyReportService.generateTestDailyReport(request, prompt); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @PostMapping("/generate") | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "오늘의 리포트 생성 API", | ||
| description = """ | ||
| 유저의 오늘의 리포트를 생성합니다. <br/> | ||
| 생성 실패 시에도 이 API를 다시 호출하면 됩니다. <br/> | ||
| 이 때 유저의 답변은 기존의 답변으로 자동으로 사용됩니다. <br/> | ||
| 소요 시간이 최대 3~4초밖에 안 되어 동기처리로 구현했습니다. <br/> | ||
|
|
||
| | 응답의 emotion | 해당 감정 | | ||
| | :--- | :--- | | ||
| | `JOY` | 기쁨 | | ||
| | `PLEASURE` | 즐거움 | | ||
| | `LOVE` | 사랑 | | ||
| | `SADNESS` | 슬픔 | | ||
| | `ANGER` | 분노 | | ||
| | `PAIN` | 고통 | | ||
| | `REGRET` | 후회 | | ||
| | `FRUSTRATION` | 좌절 | | ||
| | `GROWTH` | 성장 | | ||
| | `ETC` | 기타 | | ||
| """, | ||
| security = @SecurityRequirement(name = "bearerAuth"), | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "오늘의 리포트 생성 성공", | ||
| content = @Content(schema = @Schema(implementation = CreateDailyReportResponse.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "인증 실패" | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "400", | ||
| description = "잘못된 요청" | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "502", | ||
| description = "AI 응답 JSON 파싱 실패" | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "503", | ||
| description = "외부 AI 서비스 연동 실패" | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "409", | ||
| description = "오늘의 리포트가 이미 생성된 경우" | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<CreateDailyReportResponse>> generateDailyReport( | ||
| @AuthenticationPrincipal UserPrincipal principal, | ||
| @Valid @RequestBody DailyReportRequest request | ||
| ) { | ||
| CreateDailyReportResponse response = dailyReportService.generateDailyReport(principal.getId(), request); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @GetMapping | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "오늘의 리포트 조회 API", | ||
| description = "유저의 오늘의 리포트를 조회합니다.", | ||
| security = @SecurityRequirement(name = "bearerAuth"), | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "오늘의 리포트 조회 성공", | ||
| content = @Content(schema = @Schema(implementation = DailyReportResponse.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "인증 실패", | ||
| content = @Content | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = "오늘의 리포트가 존재하지 않는 경우", | ||
| content = @Content | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<DailyReportResponse>> getDailyReport( | ||
| @AuthenticationPrincipal UserPrincipal principal | ||
| ) { | ||
| DailyReportResponse response = dailyReportQueryService.getDailyReport(principal.getId()); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
...in/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/DailyReportRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.api.dto.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.*; | ||
|
|
||
| @Schema(description = "오늘의 리포트 생성 요청") | ||
| public record DailyReportRequest( | ||
| @Schema(description = "질문 ID", example = "1") | ||
| @NotNull(message = "questionId는 필수입니다") | ||
| Long questionId, | ||
|
|
||
| @Schema(description = "유저의 답변 내용") | ||
| @Size(max = 200, message = "answer는 최대 200자까지 입력 가능합니다") | ||
| @Size(min = 1, message = "answer는 최소 1자 이상 입력해야 합니다") | ||
| @NotBlank(message = "answer는 필수입니다") | ||
| String answer | ||
| ) { | ||
| } |
2 changes: 1 addition & 1 deletion
2
...i/dto/request/TestDailyReportRequest.java → ...i/dto/request/TestDailyReportRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
...com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CreateDailyReportResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "오늘의 리포트 생성 응답") | ||
| public record CreateDailyReportResponse( | ||
| @Schema(description = "오늘의 리포트 ID", example = "1") | ||
| Long reportId, | ||
|
|
||
| @Schema(description = "오늘의 리포트 내용") | ||
| String content, | ||
|
|
||
| @Schema(description = "오늘의 리포트 감정 상태", example = "GROWTH") | ||
| String emotion, | ||
|
|
||
| @Schema(description = "리포트 작성 후 크리스탈 잔액", example = "100") | ||
| Long balanceAfter | ||
| ) { | ||
| } |
13 changes: 13 additions & 0 deletions
13
.../java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/DailyReportResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "오늘의 리포트 조회 응답") | ||
| public record DailyReportResponse( | ||
| @Schema(description = "오늘의 리포트 내용") | ||
| String content, | ||
|
|
||
| @Schema(description = "오늘의 리포트 감정 상태", example = "GROWTH") | ||
| String emotion | ||
| ) { | ||
| } |
8 changes: 8 additions & 0 deletions
8
...a/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/TestDailyReportResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; | ||
|
|
||
| public record TestDailyReportResponse( | ||
| String message, | ||
| String emotion, | ||
| int length | ||
| ) { | ||
| } |
54 changes: 54 additions & 0 deletions
54
...n/java/com/devkor/ifive/nadab/domain/dailyreport/application/DailyReportQueryService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.application; | ||
|
|
||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.DailyReportResponse; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.entity.AnswerEntry; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReport; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryRepository; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.repository.DailyReportRepository; | ||
| import com.devkor.ifive.nadab.domain.user.core.entity.User; | ||
| import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository; | ||
| import com.devkor.ifive.nadab.global.exception.NotFoundException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.OffsetDateTime; | ||
| import java.time.ZoneId; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class DailyReportQueryService { | ||
|
|
||
| private final UserRepository userRepository; | ||
| private final DailyReportRepository dailyReportRepository; | ||
| private final AnswerEntryRepository answerEntryRepository; | ||
|
|
||
| public DailyReportResponse getDailyReport(Long id) { | ||
| User user = userRepository.findById(id) | ||
| .orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다. id: " + id)); | ||
|
|
||
| LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul")); | ||
|
|
||
| OffsetDateTime startOfToday = | ||
| today.atStartOfDay(ZoneId.of("Asia/Seoul")) | ||
| .toOffsetDateTime(); | ||
|
|
||
| OffsetDateTime startOfTomorrow = | ||
| today.plusDays(1) | ||
| .atStartOfDay(ZoneId.of("Asia/Seoul")) | ||
| .toOffsetDateTime(); | ||
|
|
||
| AnswerEntry entry = answerEntryRepository.findByUserAndCreatedAtBetween(user, startOfToday, startOfTomorrow) | ||
| .orElseThrow(() -> new NotFoundException("오늘의 답변 항목을 찾을 수 없습니다. userId: " + id)); | ||
|
|
||
| DailyReport report = dailyReportRepository.findByAnswerEntryAndCreatedAtBetween(entry, startOfToday, startOfTomorrow) | ||
| .orElseThrow(() -> new NotFoundException("오늘의 리포트를 찾을 수 없습니다. userId: " + id)); | ||
|
|
||
| return new DailyReportResponse( | ||
| report.getContent(), | ||
| report.getEmotion().getCode().toString() | ||
| ); | ||
| } | ||
| } |
58 changes: 58 additions & 0 deletions
58
src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/DailyReportService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.devkor.ifive.nadab.domain.dailyreport.application; | ||
|
|
||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.DailyReportRequest; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.CreateDailyReportResponse; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.dto.ConfirmDailyAndRewardDto; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.dto.PrepareDailyResultDto; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.dto.AiReportResultDto; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.core.entity.AnswerEntry; | ||
| import com.devkor.ifive.nadab.domain.dailyreport.infra.DailyReportLlmClient; | ||
| import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion; | ||
| import com.devkor.ifive.nadab.domain.question.core.repository.DailyQuestionRepository; | ||
| import com.devkor.ifive.nadab.domain.user.core.entity.User; | ||
| import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository; | ||
| import com.devkor.ifive.nadab.global.exception.NotFoundException; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class DailyReportService { | ||
|
|
||
| private final UserRepository userRepository; | ||
| private final DailyQuestionRepository dailyQuestionRepository; | ||
|
|
||
| private final DailyReportTxService dailyReportTxService; | ||
|
|
||
| private final DailyReportLlmClient dailyReportLlmClient; | ||
|
|
||
| public CreateDailyReportResponse generateDailyReport(Long userId, DailyReportRequest request) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다. id: " + userId)); | ||
|
|
||
| DailyQuestion question = dailyQuestionRepository.findById(request.questionId()) | ||
| .orElseThrow(() -> new NotFoundException("질문을 찾을 수 없습니다. id: " + request.questionId())); | ||
|
|
||
| PrepareDailyResultDto prep = dailyReportTxService.prepareDaily(user, question, request.answer()); | ||
|
|
||
| AnswerEntry answerEntry = prep.entry(); | ||
|
|
||
| AiReportResultDto dto; | ||
| try { | ||
| dto = dailyReportLlmClient.generate(question.getQuestionText(), answerEntry.getContent()); | ||
| } catch (Exception e) { | ||
| dailyReportTxService.failDaily(prep.reportId()); | ||
| throw e; | ||
| } | ||
|
|
||
| ConfirmDailyAndRewardDto confirmDto = dailyReportTxService.confirmDailyAndReward(prep, dto); | ||
|
|
||
| return new CreateDailyReportResponse( | ||
| prep.reportId(), | ||
| dto.message(), | ||
| confirmDto.emotion().getCode().toString(), | ||
| confirmDto.balanceAfter() | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
prepareDaily 메서드에서 answerEntryService.getOrCreateTodayAnswerEntry()로 answerEntry를 조회하고 그 후에 answerEntryRepository.findById()로 또 조회해서 중복되는거 같은데 한 번 조회하도록 줄일 수 있을까요??
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.
Dto에 AnswerEntry 자체를 포함시켜서 중복 제거했습니다