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
10 changes: 7 additions & 3 deletions .github/workflows/deploy-to-dev-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,13 @@ jobs:
WEEKLY_PROMPT="$(printf '%s' "$WEEKLY_PROMPT_B64" | base64 -d | tr -d '\r')"
export WEEKLY_PROMPT

MONTHLY_PROMPT_B64="${{ secrets.MONTHLY_PROMPT_B64 }}"
MONTHLY_PROMPT="$(printf '%s' "$MONTHLY_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_PROMPT
MONTHLY_V1_PROMPT_B64="${{ secrets.MONTHLY_V1_PROMPT_B64 }}"
MONTHLY_V1_PROMPT="$(printf '%s' "$MONTHLY_V1_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V1_PROMPT

MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_BASELINE_PROMPT

EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/deploy-to-prod-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,13 @@ jobs:
WEEKLY_PROMPT="$(printf '%s' "$WEEKLY_PROMPT_B64" | base64 -d | tr -d '\r')"
export WEEKLY_PROMPT

MONTHLY_PROMPT_B64="${{ secrets.MONTHLY_PROMPT_B64 }}"
MONTHLY_PROMPT="$(printf '%s' "$MONTHLY_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_PROMPT
MONTHLY_V1_PROMPT_B64="${{ secrets.MONTHLY_V1_PROMPT_B64 }}"
MONTHLY_V1_PROMPT="$(printf '%s' "$MONTHLY_V1_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V1_PROMPT

MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_BASELINE_PROMPT

EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ out/
node_modules

# 오늘의 리포트 프롬프트
/src/main/resources/secret/
/src/main/resources/secret/

AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportStartResponse;
import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.ReportListTypeV2;
import com.devkor.ifive.nadab.domain.monthlyreport.application.MonthlyReportQueryServiceV2;
import com.devkor.ifive.nadab.domain.monthlyreport.application.MonthlyReportService;
import com.devkor.ifive.nadab.domain.monthlyreport.application.MonthlyReportServiceV2;
import com.devkor.ifive.nadab.domain.weeklyreport.api.dto.response.CompletedCountResponse;
import com.devkor.ifive.nadab.global.core.response.ApiResponseDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity;
Expand All @@ -31,7 +31,7 @@
@RequiredArgsConstructor
public class MonthlyReportControllerV2 {

private final MonthlyReportService monthlyReportService;
private final MonthlyReportServiceV2 monthlyReportServiceV2;
private final MonthlyReportQueryServiceV2 monthlyReportQueryServiceV2;

@PostMapping("/start")
Expand Down Expand Up @@ -83,7 +83,7 @@ public class MonthlyReportControllerV2 {
public ResponseEntity<ApiResponseDto<MonthlyReportStartResponse>> startMonthlyReport(
@AuthenticationPrincipal UserPrincipal principal
) {
MonthlyReportStartResponse response = monthlyReportService.startMonthlyReport(principal.getId());
MonthlyReportStartResponse response = monthlyReportServiceV2.startMonthlyReport(principal.getId());
return ApiResponseEntity.ok(response);
}

Expand Down Expand Up @@ -181,6 +181,7 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportLookupResponseV2>> getMyMont
월간 리포트 V2를 id로 조회합니다. </br>
생성 대기 중인 경우 ```status = "PENDING"``` 으로 반환됩니다. </br>
생성 진행 중인 경우 ```status = "IN_PROGRESS"``` 로 반환됩니다. </br>
텍스트만 생성 완료한 경우 ```status = "TEXT_COMPLETED"```로 반환됩니다. </br>
생성에 성공한 경우 ```status = "COMPLETED"``` 로 반환됩니다. </br>
생성에 실패한 경우 ```status = "FAILED"``` 로 반환됩니다. 이때 크리스탈이 환불되기 때문에 잔액 조회를 해야합니다.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.devkor.ifive.nadab.domain.monthlyreport.application;

import com.devkor.ifive.nadab.domain.dailyreport.core.repository.DailyReportRepository;
import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportStartResponse;
import com.devkor.ifive.nadab.domain.monthlyreport.core.dto.MonthlyReserveResultDto;
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.domain.weeklyreport.api.dto.response.CompletedCountResponse;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import com.devkor.ifive.nadab.global.exception.report.MonthlyReportNotEligibleException;
import com.devkor.ifive.nadab.global.shared.util.MonthRangeCalculator;
import com.devkor.ifive.nadab.global.shared.util.dto.MonthRangeDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MonthlyReportServiceV2 {

private final UserRepository userRepository;
private final DailyReportRepository dailyReportRepository;

private final MonthlyReportTxServiceV2 monthlyReportTxServiceV2;

/**
* 비동기 시작 API: 즉시 reportId 반환
*/
public MonthlyReportStartResponse startMonthlyReport(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));

// 월간 리포트 작성 자격 확인 (저번 달에 15회 이상 완료)
MonthRangeDto range = MonthRangeCalculator.getLastMonthRange();

long completedCount = dailyReportRepository.countCompletedInMonth(userId, range.monthStartDate(), range.monthEndDate());
boolean eligible = completedCount >= 15;

if (!eligible) {
CompletedCountResponse response = new CompletedCountResponse(completedCount);
throw new MonthlyReportNotEligibleException(ErrorCode.MONTHLY_REPORT_NOT_ENOUGH_REPORTS, response);
}

// (Tx) Report(PENDING) + reserve consume + log(PENDING)
MonthlyReserveResultDto reserve = monthlyReportTxServiceV2.reserveMonthlyAndPublish(user);

return new MonthlyReportStartResponse(reserve.reportId(), "PENDING", reserve.balanceAfter());
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.devkor.ifive.nadab.domain.monthlyreport.application;


import com.devkor.ifive.nadab.domain.monthlyreport.core.content.InterestStatsContent;
import com.devkor.ifive.nadab.domain.monthlyreport.core.dto.MonthlyReportGenerationRequestedEventDto;
import com.devkor.ifive.nadab.domain.monthlyreport.core.dto.MonthlyReserveResultDto;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.*;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReport;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportStatus;
import com.devkor.ifive.nadab.domain.monthlyreport.core.repository.MonthlyReportRepository;
import com.devkor.ifive.nadab.domain.monthlyreport.core.repository.MonthlyReportV2Repository;
import com.devkor.ifive.nadab.domain.monthlyreport.core.service.PendingMonthlyReportService;
import com.devkor.ifive.nadab.domain.typereport.core.content.TypeEmotionStatsContent;
import com.devkor.ifive.nadab.domain.typereport.core.content.TypeTextContent;
import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.domain.wallet.core.entity.CrystalLog;
import com.devkor.ifive.nadab.domain.wallet.core.entity.CrystalLogReason;
Expand All @@ -20,6 +17,7 @@
import com.devkor.ifive.nadab.global.exception.NotEnoughCrystalException;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import com.devkor.ifive.nadab.global.exception.ai.AiResponseParseException;
import com.devkor.ifive.nadab.global.shared.reportcontent.ReportContent;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -34,7 +32,6 @@ public class MonthlyReportTxService {
private final PendingMonthlyReportService pendingMonthlyReportService;

private final MonthlyReportRepository monthlyReportRepository;
private final MonthlyReportV2Repository monthlyReportV2Repository;
private final UserWalletRepository userWalletRepository;
private final CrystalLogRepository crystalLogRepository;

Expand All @@ -48,10 +45,10 @@ public class MonthlyReportTxService {
* (Tx) MonthlyReport(PENDING) + reserve consume + CrystalLog(PENDING)
* 커밋되면 리포트 생성 작업을 시작할 준비가 완료됨
*/
public MonthlyReserveResultDto reserveMonthly(User user, boolean exists) {
public MonthlyReserveResultDto reserveMonthly(User user) {

// Report: 있으면 기존 사용, 없으면 새로 PENDING 생성
MonthlyReportV2 report = pendingMonthlyReportService.getOrCreatePendingMonthlyReport(user, exists);
MonthlyReport report = pendingMonthlyReportService.getOrCreatePendingMonthlyReport(user);

// 선차감(원자적) + balanceAfter 확보
int updated = userWalletRepository.tryConsume(user.getId(), MONTHLY_REPORT_COST);
Expand Down Expand Up @@ -80,79 +77,43 @@ public MonthlyReserveResultDto reserveMonthly(User user, boolean exists) {
}

public MonthlyReserveResultDto reserveMonthlyAndPublish(User user) {
MonthlyReserveResultDto reserve = this.reserveMonthly(user);

boolean exists = monthlyReportV2Repository.existsByUserIdAndStatus(user.getId(), MonthlyReportStatus.COMPLETED);

MonthlyReserveResultDto reserve = this.reserveMonthly(user, exists);

monthlyReportV2Repository.updateStatus(reserve.reportId(), MonthlyReportStatus.IN_PROGRESS);
monthlyReportRepository.updateStatus(reserve.reportId(), MonthlyReportStatus.IN_PROGRESS);

// 트랜잭션 안에서 publish (AFTER_COMMIT 트리거 보장)
eventPublisher.publishEvent(new MonthlyReportGenerationRequestedEventDto(
reserve.reportId(),
user.getId(),
reserve.crystalLogId(),
exists
reserve.crystalLogId()
));

return reserve;
}

public void confirmMonthly(Long reportId, Long logId, String imageKey) {
monthlyReportV2Repository.completeWithImage(
reportId,
imageKey,
MonthlyReportImageStatus.COMPLETED,
MonthlyReportStatus.COMPLETED
);
public void confirmMonthly(Long reportId, Long logId, ReportContent content) {
ReportContent normalized = content.normalized();

// log를 CONFIRMED로
crystalLogRepository.markConfirmed(logId);
}

public void confirmMonthlyText(
Long reportId,
MonthlyReportV2Content content,
TypeTextContent emotionSummaryContent,
TypeEmotionStatsContent emotionStats,
InterestStatsContent interestStats
) {
MonthlyReportV2Content contentNormalized = content.normalized();
TypeTextContent emotionSummaryContentNormalized = emotionSummaryContent.normalized();
InterestStatsContent interestStatsNormalized = interestStats.normalized();

String summary = contentNormalized.summary();
String commentSummary = contentNormalized.commentSummary();
String dominantKeyword = contentNormalized.dominantKeyword();
String summary = normalized.summary();
String discovered = normalized.discovered().plainText();
String improve = normalized.improve().plainText();

// report를 COMPLETED로
String contentJson;
String emotionSummaryContentJson;
String emotionStatsJson;
String interestStatsJson;

try {
contentJson = objectMapper.writeValueAsString(contentNormalized);
emotionSummaryContentJson = objectMapper.writeValueAsString(emotionSummaryContentNormalized);
emotionStatsJson = objectMapper.writeValueAsString(emotionStats.normalized());
interestStatsJson = objectMapper.writeValueAsString(interestStatsNormalized);
contentJson = objectMapper.writeValueAsString(normalized);
} catch (Exception e) {
throw new AiResponseParseException(ErrorCode.AI_RESPONSE_PARSE_FAILED);
}
monthlyReportV2Repository.updateContent(
reportId,
contentJson,
emotionSummaryContentJson,
summary,
commentSummary,
dominantKeyword,
emotionStatsJson,
interestStatsJson,
MonthlyReportStatus.TEXT_COMPLETED.name()
);
monthlyReportRepository.markCompleted(
reportId, MonthlyReportStatus.COMPLETED.name(), contentJson, discovered, improve, summary);

// log를 CONFIRMED로
crystalLogRepository.markConfirmed(logId);
}

public void failAndRefundMonthly(Long userId, Long reportId, Long logId) {
monthlyReportV2Repository.markFailed(reportId, MonthlyReportStatus.FAILED);
monthlyReportRepository.markFailed(reportId, MonthlyReportStatus.FAILED);

// 환불(+cost)
int updated = userWalletRepository.refund(userId, MONTHLY_REPORT_COST);
Expand All @@ -164,17 +125,4 @@ public void failAndRefundMonthly(Long userId, Long reportId, Long logId) {
// log를 REFUNDED로
crystalLogRepository.markRefunded(logId);
}

public void markMonthlyImageProcessing(Long reportId) {
monthlyReportV2Repository.updateImageStatus(reportId, MonthlyReportImageStatus.PROCESSING);
}

public void failMonthlyImage(Long reportId) {
monthlyReportV2Repository.updateImageStatus(reportId, MonthlyReportImageStatus.FAILED);
}

public void failAndRefundMonthlyWithImage(Long userId, Long reportId, Long logId) {
this.failAndRefundMonthly(userId, reportId, logId);
this.failMonthlyImage(reportId);
}
}
}
Loading
Loading