Skip to content
Merged
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
@@ -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);
}
}
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
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.devkor.ifive.nadab.domain.report.api.dto.request;
package com.devkor.ifive.nadab.domain.dailyreport.api.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
Expand Down
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
) {
}
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
) {
}
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
) {
}
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()
);
}
}
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());
Copy link
Collaborator

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()로 또 조회해서 중복되는거 같은데 한 번 조회하도록 줄일 수 있을까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dto에 AnswerEntry 자체를 포함시켜서 중복 제거했습니다


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()
);
}
}
Loading