diff --git a/common/src/main/java/com/ntropy/common/client/AccountTransactionAnalysisClient.java b/common/src/main/java/com/ntropy/common/client/AccountTransactionAnalysisClient.java new file mode 100644 index 0000000..fafebf9 --- /dev/null +++ b/common/src/main/java/com/ntropy/common/client/AccountTransactionAnalysisClient.java @@ -0,0 +1,31 @@ +package com.ntropy.common.client; + +import java.util.List; + +import com.ntropy.common.dto.account.ClassificationTargetTransaction; +import com.ntropy.common.dto.account.TransactionAnalysisSaveRequest; + +/** + * AI-service가 account-service의 거래 분석 기능을 호출하기 위한 내부 Client 인터페이스입니다. + */ +public interface AccountTransactionAnalysisClient { + + /** + * 특정 사용자와 연월 기준으로 AI 분류 대상 출금 거래를 조회합니다. + * + * @param userId 사용자 ID + * @param yearMonth 조회 대상 연월. 예: "2026-07" + * @return FastAPI 소비 분류 요청에 사용할 거래 목록 + */ + List findClassificationTargets( + Long userId, + String yearMonth + ); + + /** + * FastAPI 소비 분류 결과를 account-service의 TXN_ANALYSIS에 저장합니다. + * + * @param request 저장할 분류 결과 요청 + */ + void saveTransactionAnalyses(TransactionAnalysisSaveRequest request); +} \ No newline at end of file diff --git a/common/src/main/java/com/ntropy/common/dto/account/ClassificationTargetTransaction.java b/common/src/main/java/com/ntropy/common/dto/account/ClassificationTargetTransaction.java new file mode 100644 index 0000000..12f86e1 --- /dev/null +++ b/common/src/main/java/com/ntropy/common/dto/account/ClassificationTargetTransaction.java @@ -0,0 +1,22 @@ +package com.ntropy.common.dto.account; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * FastAPI 소비 분류 대상 거래 DTO입니다. + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ClassificationTargetTransaction { + + private Long transactionId; + private String merchantName; + private String description; + private Long amount; + private LocalDateTime transactionDate; +} \ No newline at end of file diff --git a/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveItem.java b/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveItem.java new file mode 100644 index 0000000..2bd575c --- /dev/null +++ b/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveItem.java @@ -0,0 +1,19 @@ +package com.ntropy.common.dto.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 거래 1건의 소비 분류 저장 DTO입니다. + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TransactionAnalysisSaveItem { + + private Long transactionId; + private Boolean isConsumption; + private String category; + private String expenseType; +} \ No newline at end of file diff --git a/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveRequest.java b/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveRequest.java new file mode 100644 index 0000000..87c954d --- /dev/null +++ b/common/src/main/java/com/ntropy/common/dto/account/TransactionAnalysisSaveRequest.java @@ -0,0 +1,20 @@ +package com.ntropy.common.dto.account; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 거래 분석 결과 저장 요청 DTO입니다. + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TransactionAnalysisSaveRequest { + + private Long userId; + private String yearMonth; + private List analyses; +} \ No newline at end of file diff --git a/services/account-service/src/main/java/com/ntropy/account/client/LocalAccountTransactionAnalysisClient.java b/services/account-service/src/main/java/com/ntropy/account/client/LocalAccountTransactionAnalysisClient.java new file mode 100644 index 0000000..3914b59 --- /dev/null +++ b/services/account-service/src/main/java/com/ntropy/account/client/LocalAccountTransactionAnalysisClient.java @@ -0,0 +1,38 @@ +package com.ntropy.account.client; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.ntropy.account.service.TxnAnalysisService; +import com.ntropy.common.client.AccountTransactionAnalysisClient; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; +import com.ntropy.common.dto.account.TransactionAnalysisSaveRequest; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class LocalAccountTransactionAnalysisClient + implements AccountTransactionAnalysisClient { + + private final TxnAnalysisService txnAnalysisService; + + @Override + public List findClassificationTargets( + Long userId, + String yearMonth + ) { + return txnAnalysisService.findClassificationTargets( + userId, + yearMonth + ); + } + + @Override + public void saveTransactionAnalyses( + TransactionAnalysisSaveRequest request + ) { + txnAnalysisService.saveAnalyses(request); + } +} \ No newline at end of file diff --git a/services/account-service/src/main/java/com/ntropy/account/domain/entity/TxnAnalysis.java b/services/account-service/src/main/java/com/ntropy/account/domain/entity/TxnAnalysis.java new file mode 100644 index 0000000..bc5ce6d --- /dev/null +++ b/services/account-service/src/main/java/com/ntropy/account/domain/entity/TxnAnalysis.java @@ -0,0 +1,24 @@ +package com.ntropy.account.domain.entity; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * TXN_ANALYSIS 테이블과 매핑되는 도메인 객체입니다. + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TxnAnalysis { + + private Long analysisId; + private Long accountTransactionId; + private Boolean isConsumption; + private String category; + private String expenseType; + private LocalDateTime classifiedAt; +} \ No newline at end of file diff --git a/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialDataQueryMapper.java b/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialDataQueryMapper.java index fd0ee9d..51dc5f2 100644 --- a/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialDataQueryMapper.java +++ b/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialDataQueryMapper.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.List; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -33,4 +34,9 @@ List findTransactionsByAccountIdAndUserId( @Param("limit") int limit, @Param("offset") int offset ); + + List findClassificationTargets( + @Param("userId") Long userId, + @Param("yearMonth") String yearMonth + ); } diff --git a/services/account-service/src/main/java/com/ntropy/account/mapper/TxnAnalysisMapper.java b/services/account-service/src/main/java/com/ntropy/account/mapper/TxnAnalysisMapper.java new file mode 100644 index 0000000..85a16e2 --- /dev/null +++ b/services/account-service/src/main/java/com/ntropy/account/mapper/TxnAnalysisMapper.java @@ -0,0 +1,29 @@ +package com.ntropy.account.mapper; + +import java.util.List; + +import com.ntropy.common.dto.account.TransactionAnalysisSaveRequest; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import com.ntropy.account.domain.entity.TxnAnalysis; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; + +@Mapper +public interface TxnAnalysisMapper { + + /** + * 특정 사용자와 연월 기준으로 분류 대상 출금 거래를 조회합니다. + */ + List findClassificationTargets( + @Param("userId") Long userId, + @Param("yearMonth") String yearMonth + ); + + /** + * 거래 분석 결과를 저장하거나 갱신합니다. + */ + int upsert(TxnAnalysis txnAnalysis); + + int upsertAnalyses(TransactionAnalysisSaveRequest request); +} \ No newline at end of file diff --git a/services/account-service/src/main/java/com/ntropy/account/service/TxnAnalysisService.java b/services/account-service/src/main/java/com/ntropy/account/service/TxnAnalysisService.java new file mode 100644 index 0000000..377c915 --- /dev/null +++ b/services/account-service/src/main/java/com/ntropy/account/service/TxnAnalysisService.java @@ -0,0 +1,44 @@ +package com.ntropy.account.service; + +import java.util.List; + +import com.ntropy.account.mapper.FinancialDataQueryMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ntropy.account.domain.entity.TxnAnalysis; +import com.ntropy.account.mapper.TxnAnalysisMapper; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; +import com.ntropy.common.dto.account.TransactionAnalysisSaveItem; +import com.ntropy.common.dto.account.TransactionAnalysisSaveRequest; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TxnAnalysisService { + + private final TxnAnalysisMapper txnAnalysisMapper; + private final FinancialDataQueryMapper financialDataQueryMapper; + + public List findClassificationTargets( + Long userId, + String yearMonth + ) { + return financialDataQueryMapper.findClassificationTargets( + userId, + yearMonth + ); + } + + @Transactional + public void saveAnalyses(TransactionAnalysisSaveRequest request) { + if (request == null + || request.getAnalyses() == null + || request.getAnalyses().isEmpty()) { + return; + } + + txnAnalysisMapper.upsertAnalyses(request); + } +} \ No newline at end of file diff --git a/services/account-service/src/main/resources/db/account-service-schema.sql b/services/account-service/src/main/resources/db/account-service-schema.sql index a76a8c0..ca8eef6 100644 --- a/services/account-service/src/main/resources/db/account-service-schema.sql +++ b/services/account-service/src/main/resources/db/account-service-schema.sql @@ -95,3 +95,22 @@ CREATE TABLE IF NOT EXISTS ACCOUNT_TRANSACTION CONSTRAINT fk_account_transaction_account FOREIGN KEY (account_id) REFERENCES ACCOUNT (account_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +CREATE TABLE IF NOT EXISTS TXN_ANALYSIS +( + txn_analysis_id BIGINT AUTO_INCREMENT PRIMARY KEY, + account_transaction_id BIGINT NOT NULL, + is_consumption BOOLEAN NOT NULL, + category VARCHAR(32) NULL, + expense_type VARCHAR(16) NULL, + classified_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + UNIQUE KEY uk_txn_analysis_transaction ( + account_transaction_id + ), + + CONSTRAINT fk_txn_analysis_transaction + FOREIGN KEY (account_transaction_id) + REFERENCES ACCOUNT_TRANSACTION (account_transaction_id) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; \ No newline at end of file diff --git a/services/account-service/src/main/resources/mapper/account/FinancialDataQueryMapper.xml b/services/account-service/src/main/resources/mapper/account/FinancialDataQueryMapper.xml index 0495709..1626ec7 100644 --- a/services/account-service/src/main/resources/mapper/account/FinancialDataQueryMapper.xml +++ b/services/account-service/src/main/resources/mapper/account/FinancialDataQueryMapper.xml @@ -93,4 +93,34 @@ LIMIT #{limit} OFFSET #{offset} + diff --git a/services/account-service/src/main/resources/mapper/account/TxnAnalysisMapper.xml b/services/account-service/src/main/resources/mapper/account/TxnAnalysisMapper.xml new file mode 100644 index 0000000..7200cd2 --- /dev/null +++ b/services/account-service/src/main/resources/mapper/account/TxnAnalysisMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + INSERT INTO TXN_ANALYSIS ( + account_transaction_id, + is_consumption, + category, + expense_type, + classified_at + ) VALUES ( + #{accountTransactionId}, + #{isConsumption}, + #{category}, + #{expenseType}, + NOW() + ) + ON DUPLICATE KEY UPDATE + is_consumption = VALUES(is_consumption), + category = VALUES(category), + expense_type = VALUES(expense_type), + classified_at = NOW() + + + + + INSERT INTO TXN_ANALYSIS ( + account_transaction_id, + is_consumption, + category, + expense_type, + classified_at + ) + VALUES + + ( + #{item.transactionId}, + #{item.isConsumption}, + #{item.category}, + #{item.expenseType}, + CURRENT_TIMESTAMP + ) + + ON DUPLICATE KEY UPDATE + is_consumption = VALUES(is_consumption), + category = VALUES(category), + expense_type = VALUES(expense_type), + classified_at = CURRENT_TIMESTAMP + + + \ No newline at end of file diff --git a/services/account-service/src/test/java/com/ntropy/account/client/LocalAccountQueryClientTest.java b/services/account-service/src/test/java/com/ntropy/account/client/LocalAccountQueryClientTest.java index 8feb18d..4571849 100644 --- a/services/account-service/src/test/java/com/ntropy/account/client/LocalAccountQueryClientTest.java +++ b/services/account-service/src/test/java/com/ntropy/account/client/LocalAccountQueryClientTest.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; import org.junit.jupiter.api.Test; import com.ntropy.account.domain.AccountGroup; @@ -231,5 +232,13 @@ public List findTransactionsByAccountIdAndUserId( int to = Math.min(from + limit, rows.size()); return rows.subList(from, to); } + + @Override + public List findClassificationTargets( + Long userId, + String yearMonth + ) { + return List.of(); + } } } diff --git a/services/ai-service/src/main/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationService.java b/services/ai-service/src/main/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationService.java index 06f12bc..1eebe3f 100644 --- a/services/ai-service/src/main/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationService.java +++ b/services/ai-service/src/main/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationService.java @@ -4,38 +4,35 @@ import java.time.ZoneId; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import com.ntropy.ai.client.fastapi.FastApiTransactionClassificationClient; import com.ntropy.ai.dto.fastapi.TransactionClassificationResponse; +import com.ntropy.ai.dto.fastapi.TransactionClassificationResult; import com.ntropy.ai.dto.fastapi.TransactionForClassification; +import com.ntropy.common.client.AccountTransactionAnalysisClient; +import com.ntropy.common.dto.account.ClassificationTargetTransaction; +import com.ntropy.common.dto.account.TransactionAnalysisSaveItem; +import com.ntropy.common.dto.account.TransactionAnalysisSaveRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -/** - * 월간 AI 리포트 생성 파이프라인 전체를 조정하는 서비스입니다. - * - * 현재는 account-service, diagnosis-service 연동 계약이 일부 확정되지 않았으므로 - * 배치 대상 사용자 조회와 거래 조회는 안전한 빈 목록으로 유지합니다. - * - * FastAPI 소비 분류 API 호출 구조만 먼저 연결해두고, - * 계약 확정 후 processSingleUser() 내부 TODO를 실제 Client 호출로 교체합니다. - */ @Slf4j @Service @RequiredArgsConstructor public class MonthlyAiReportOrchestrationService { - // FastAPI 소비 분류 API 호출 Client입니다. private final FastApiTransactionClassificationClient classificationClient; + private final AccountTransactionAnalysisClient + accountTransactionAnalysisClient; + /** - * 매월 1일 스케줄러에서 호출됩니다. - * - * 예를 들어 2026년 8월 1일에 실행되면 - * 2026년 7월 리포트를 생성 대상으로 처리합니다. + * 매월 1일 실행되며 지난달 데이터를 처리합니다. */ public void runLastMonthBatch() { YearMonth lastMonth = YearMonth.now( @@ -46,13 +43,15 @@ public void runLastMonthBatch() { } /** - * 지정한 연월의 AI 리포트 배치를 실행합니다. - * - * 테스트에서는 현재 날짜와 관계없이 원하는 연월을 직접 전달할 수 있습니다. - * - * @param targetYearMonth 리포트 생성 대상 연월 + * 지정한 연월의 배치를 실행합니다. */ public void runBatch(YearMonth targetYearMonth) { + if (targetYearMonth == null) { + throw new IllegalArgumentException( + "배치 대상 연월은 필수입니다." + ); + } + String yearMonth = targetYearMonth.toString(); long startedAt = System.currentTimeMillis(); @@ -61,24 +60,20 @@ public void runBatch(YearMonth targetYearMonth) { yearMonth ); - // 추후 user-service 또는 account-service Client를 통해 - // 배치 대상 사용자 목록을 조회하도록 교체합니다. List targetUserIds = findTargetUserIds(yearMonth); int successCount = 0; int failedCount = 0; - // 사용자 한 명의 실패가 전체 배치를 중단시키지 않도록 개별 예외를 분리합니다. for (Long userId : targetUserIds) { try { processSingleUser(userId, yearMonth); successCount++; - } catch (Exception exception) { failedCount++; log.error( - "[AI 리포트 배치] 사용자 리포트 생성 실패 - userId: {}, yearMonth: {}", + "[AI 리포트 배치] 사용자 처리 실패 - userId: {}, yearMonth: {}", userId, yearMonth, exception @@ -86,7 +81,8 @@ public void runBatch(YearMonth targetYearMonth) { } } - long elapsedMillis = System.currentTimeMillis() - startedAt; + long elapsedMillis = + System.currentTimeMillis() - startedAt; log.info( "[AI 리포트 배치] 완료 - yearMonth: {}, 대상: {}명, 성공: {}명, 실패: {}명, 실행 시간: {}ms", @@ -99,10 +95,7 @@ public void runBatch(YearMonth targetYearMonth) { } /** - * 배치 대상 사용자 ID 목록을 조회합니다. - * - * 아직 사용자 조회 Client 계약이 없으므로 빈 목록을 반환합니다. - * 따라서 현재 단계에서 실제 데이터 저장이나 외부 API 호출은 일어나지 않습니다. + * 현재는 사용자 조회 Client가 연결되지 않아 빈 목록을 반환합니다. */ private List findTargetUserIds(String yearMonth) { log.info( @@ -114,32 +107,28 @@ private List findTargetUserIds(String yearMonth) { } /** - * 사용자 한 명의 월간 AI 리포트를 생성합니다. - * - * 현재는 account-service 거래 조회 계약이 확정되지 않았으므로 - * 분류 대상 거래 목록은 빈 목록으로 둡니다. - * - * 이후 account-service 거래 조회 Client가 생기면 - * transactions 변수에 실제 분류 대상 출금 거래를 넣으면 됩니다. + * 한 사용자의 거래 조회 및 소비 분류 결과 저장을 처리합니다. */ private void processSingleUser( Long userId, String yearMonth ) { log.info( - "[AI 리포트 배치] 사용자 리포트 처리 시작 - userId: {}, yearMonth: {}", + "[AI 리포트 배치] 사용자 처리 시작 - userId: {}, yearMonth: {}", userId, yearMonth ); - /* - * TODO 1. account-service에서 분류 대상 거래 조회 - * - * 현재는 거래 조회 Client 계약이 없으므로 빈 목록으로 둡니다. - */ - List transactions = List.of(); + List + classificationTargets = + accountTransactionAnalysisClient + .findClassificationTargets( + userId, + yearMonth + ); - if (transactions.isEmpty()) { + if (classificationTargets == null + || classificationTargets.isEmpty()) { log.info( "[AI 리포트 배치] 분류 대상 거래 없음 - userId: {}, yearMonth: {}", userId, @@ -148,45 +137,105 @@ private void processSingleUser( return; } - /* - * TODO 2. FastAPI에 소비 내역 분류 요청 - */ - TransactionClassificationResponse classificationResponse = - classificationClient.classifyTransactions(transactions); - - if ( - classificationResponse == null - || !Boolean.TRUE.equals(classificationResponse.getSuccess()) - || classificationResponse.getData() == null - ) { + List transactions = + classificationTargets.stream() + .map(target -> new TransactionForClassification( + target.getTransactionId(), + target.getMerchantName(), + target.getDescription(), + target.getAmount(), + target.getTransactionDate() + )) + .toList(); + + // FastAPI 소비 분류 요청 + TransactionClassificationResponse + classificationResponse = + classificationClient.classifyTransactions( + transactions + ); + + if (classificationResponse == null + || !Boolean.TRUE.equals( + classificationResponse.getSuccess() + ) + || classificationResponse.getData() == null + || classificationResponse.getData() + .getResults() == null) { throw new IllegalStateException( "FastAPI 소비 분류 응답이 올바르지 않습니다." ); } + List results = + classificationResponse.getData().getResults(); + + // 입력 거래 수와 응답 결과 수 검증 + if (results.size() != transactions.size()) { + throw new IllegalStateException( + "FastAPI 분류 결과 수가 요청 거래 수와 일치하지 않습니다." + ); + } + + // transactionId 1:1 매핑 검증 + Set requestedTransactionIds = + transactions.stream() + .map(TransactionForClassification + ::getTransactionId) + .collect(Collectors.toSet()); + + Set responseTransactionIds = + results.stream() + .map(TransactionClassificationResult + ::getTransactionId) + .collect(Collectors.toSet()); + + if (!requestedTransactionIds.equals( + responseTransactionIds + )) { + throw new IllegalStateException( + "FastAPI 분류 결과의 transactionId가 요청 거래와 일치하지 않습니다." + ); + } + + // account-service 저장용 DTO로 변환 + List saveItems = + results.stream() + .map(result -> new TransactionAnalysisSaveItem( + result.getTransactionId(), + result.getIsConsumption(), + result.getCategory(), + result.getExpenseType() + )) + .toList(); + + // TXN_ANALYSIS upsert 저장 + accountTransactionAnalysisClient + .saveTransactionAnalyses( + new TransactionAnalysisSaveRequest( + userId, + yearMonth, + saveItems + ) + ); + log.info( - "[AI 리포트 배치] 소비 분류 완료 - userId: {}, yearMonth: {}, resultCount: {}", + "[AI 리포트 배치] TXN_ANALYSIS 저장 완료 - userId: {}, yearMonth: {}, savedCount: {}", userId, yearMonth, - classificationResponse.getData().getResults().size() + saveItems.size() ); /* - * TODO 3. account-service에 TXN_ANALYSIS 저장 요청 - * - * TODO 4. diagnosis-service에 재무진단 생성 요청 - * - * TODO 5. diagnosis-service에서 DIAGNOSIS_RESULT 조회 - * - * TODO 6. FastAPI에 금융상품 추천 및 리포트 문구 생성 요청 - * - * TODO 7. AiReportService.upsert()로 AI_REPORT 저장 또는 갱신 - * - * TODO 8. notification-service에 알림 발송 요청 + * TODO 1. diagnosis-service 진단 재계산 요청 + * TODO 2. DIAGNOSIS_RESULT 조회 + * TODO 3. FastAPI 종합 분석 및 추천 요청 + * TODO 4. AI_REPORT upsert 저장 + * TODO 5. 이메일·카카오톡 알림 발송 */ log.info( - "[AI 리포트 배치] 사용자 리포트 처리 뼈대 완료 - userId: {}, yearMonth: {}", + "[AI 리포트 배치] 사용자 처리 완료 - userId: {}, yearMonth: {}", userId, yearMonth ); diff --git a/services/ai-service/src/test/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationServiceTest.java b/services/ai-service/src/test/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationServiceTest.java index 4ae8b14..e0b1b34 100644 --- a/services/ai-service/src/test/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationServiceTest.java +++ b/services/ai-service/src/test/java/com/ntropy/ai/service/MonthlyAiReportOrchestrationServiceTest.java @@ -21,7 +21,7 @@ class MonthlyAiReportOrchestrationServiceTest { void runBatch_whenTargetUserListIsEmpty_completesWithoutException() { // 현재 오케스트레이터는 외부 Client 의존성이 없는 뼈대 상태입니다. MonthlyAiReportOrchestrationService orchestrationService = - new MonthlyAiReportOrchestrationService(null); + new MonthlyAiReportOrchestrationService(null, null); // 테스트에서 원하는 리포트 대상 연월을 직접 전달합니다. YearMonth targetYearMonth = YearMonth.of(2026, 7); diff --git a/services/bff-service/src/main/java/com/ntropy/bff/controller/ai/AiReportController.java b/services/bff-service/src/main/java/com/ntropy/bff/controller/ai/AiReportController.java index 44d4836..4fdeb94 100644 --- a/services/bff-service/src/main/java/com/ntropy/bff/controller/ai/AiReportController.java +++ b/services/bff-service/src/main/java/com/ntropy/bff/controller/ai/AiReportController.java @@ -33,14 +33,10 @@ public class AiReportController { private final AuthenticatedUserIdResolver authenticatedUserIdResolver; /** - * 특정 월의 AI 리포트를 조회합니다. + * 인증된 사용자의 특정 월 AI 리포트를 조회합니다. * * 요청 예시: - * GET /api/ai-reports/2026-07?userId=1 - * - * @param authentication 인증 정보. 사용자 ID를 추출하는 데 사용합니다. - * @param yearMonth 조회할 리포트 대상 연월. 예: "2026-07" - * @return 공통 응답 형식으로 감싼 AI 리포트 상세 데이터 + * GET /api/ai-reports/2026-07 */ @GetMapping("/{yearMonth}") public ResponseEntity> getAiReport( @@ -69,18 +65,20 @@ public ResponseEntity> getAiReport( } /** - * 특정 사용자의 전체 AI 리포트 목록을 최신 연월순으로 조회합니다. + * 인증된 사용자의 전체 AI 리포트 목록을 최신 연월순으로 조회합니다. * * 요청 예시: - * GET /api/ai-reports?userId=1 + * GET /api/ai-reports * - * @param userId 조회할 사용자 ID - * @return 공통 응답 형식으로 감싼 AI 리포트 목록 데이터 + * JWT의 사용자 ID를 사용하므로 userId 쿼리 파라미터는 받지 않습니다. */ @GetMapping public ResponseEntity> getAiReports( - @RequestParam Long userId + @ApiParam(hidden = true) Authentication authentication ) { + + Long userId = authenticatedUserIdResolver.resolve(authentication); + // BFF는 인터페이스를 통해 ai-service에 전체 AI 리포트 목록을 요청합니다. List summaries = aiReportQueryClient.findAllByUserId(userId);