From 4aeac2ac0fb0354392c4318aff95344456e8cda0 Mon Sep 17 00:00:00 2001 From: sb Date: Sun, 5 Jul 2026 15:05:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=ED=86=B5=EA=B3=84=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20AI=20=EC=9E=AC=EB=AC=B4=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이번 달 집계(수입·지출·전월대비·예산·분류별)만 Claude Haiku에 보내 2~3개 코멘트 생성. POST /account/ai-comment(유료 게이트), 거래 원문 미전송. 미설정/실패 시 빈 코멘트→카드 숨김. Co-Authored-By: Claude Opus 4.8 --- .../account/controller/AccountController.java | 8 ++++ .../sb/web/account/dto/AiCommentRequest.java | 27 +++++++++++ .../sb/web/account/dto/AiCommentResponse.java | 16 +++++++ .../web/account/service/AccountService.java | 12 +++++ .../sb/web/account/service/AiEntryParser.java | 46 +++++++++++++++++++ .../com/sb/web/common/config/WebConfig.java | 1 + 6 files changed, 110 insertions(+) create mode 100644 src/main/java/com/sb/web/account/dto/AiCommentRequest.java create mode 100644 src/main/java/com/sb/web/account/dto/AiCommentResponse.java diff --git a/src/main/java/com/sb/web/account/controller/AccountController.java b/src/main/java/com/sb/web/account/controller/AccountController.java index 7893fd5..779fe95 100644 --- a/src/main/java/com/sb/web/account/controller/AccountController.java +++ b/src/main/java/com/sb/web/account/controller/AccountController.java @@ -250,6 +250,14 @@ public class AccountController { return accountService.parseText(req); } + /** 통계 화면 집계 → AI 재무 코멘트(유료). 집계만 전송, 거래 원문 미전송. */ + @PostMapping("/ai-comment") + public com.sb.web.account.dto.AiCommentResponse aiComment( + @RequestBody com.sb.web.account.dto.AiCommentRequest req, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + return accountService.aiComment(req); + } + /** 미확인 내역 확정 (분류/계좌 보정) */ @PostMapping("/entries/{id}/confirm") public AccountEntryResponse confirm( diff --git a/src/main/java/com/sb/web/account/dto/AiCommentRequest.java b/src/main/java/com/sb/web/account/dto/AiCommentRequest.java new file mode 100644 index 0000000..2e25922 --- /dev/null +++ b/src/main/java/com/sb/web/account/dto/AiCommentRequest.java @@ -0,0 +1,27 @@ +package com.sb.web.account.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 통계 화면의 이번 달 집계(원 단위) — AI 재무 코멘트 생성용. 거래 원문은 보내지 않고 집계만 전달. + */ +@Data +public class AiCommentRequest { + + private int year; + private int month; + private long income; // 이번 달 수입 합계 + private long expense; // 이번 달 지출 합계 + private long prevExpense; // 직전 달 지출 합계(대비용) + private long budget; // 이번 달 예산 합계 + private long spent; // 이번 달 예산 대비 소비액 + private List categories; // 분류별 지출 상위 + + @Data + public static class CategoryAmount { + private String category; + private long total; + } +} diff --git a/src/main/java/com/sb/web/account/dto/AiCommentResponse.java b/src/main/java/com/sb/web/account/dto/AiCommentResponse.java new file mode 100644 index 0000000..d2b54fd --- /dev/null +++ b/src/main/java/com/sb/web/account/dto/AiCommentResponse.java @@ -0,0 +1,16 @@ +package com.sb.web.account.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * AI 재무 코멘트 결과. bullets 가 비어 있으면 통계 화면에서 카드를 숨긴다. + */ +@Data +@Builder +public class AiCommentResponse { + + private List bullets; +} diff --git a/src/main/java/com/sb/web/account/service/AccountService.java b/src/main/java/com/sb/web/account/service/AccountService.java index f8473f7..7e78068 100644 --- a/src/main/java/com/sb/web/account/service/AccountService.java +++ b/src/main/java/com/sb/web/account/service/AccountService.java @@ -322,6 +322,18 @@ public class AccountService { .build(); } + /** 통계 화면 집계 → AI 재무 코멘트. 집계만 JSON 으로 직렬화해 전달(거래 원문 미전송). 미설정/실패 시 빈 코멘트. */ + public com.sb.web.account.dto.AiCommentResponse aiComment(com.sb.web.account.dto.AiCommentRequest req) { + try { + String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(req); + return com.sb.web.account.dto.AiCommentResponse.builder() + .bullets(aiEntryParser.financeComment(json)) + .build(); + } catch (Exception e) { + return com.sb.web.account.dto.AiCommentResponse.builder().bullets(java.util.List.of()).build(); + } + } + @Transactional public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) { CardNotificationParser.Parsed p = diff --git a/src/main/java/com/sb/web/account/service/AiEntryParser.java b/src/main/java/com/sb/web/account/service/AiEntryParser.java index d75c633..d531769 100644 --- a/src/main/java/com/sb/web/account/service/AiEntryParser.java +++ b/src/main/java/com/sb/web/account/service/AiEntryParser.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; @@ -107,6 +108,51 @@ public class AiEntryParser { return callClaude(system, content, 400, today, categoryHints, walletHints); } + /** 이번 달 집계(JSON) → 2~3개의 한국어 재무 코멘트. 미설정/실패 시 빈 리스트. */ + public List financeComment(String summaryJson) { + if (!enabled() || summaryJson == null || summaryJson.isBlank()) return List.of(); + try { + String system = """ + 너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고 + 도움이 되는 코멘트를 2~3개 만든다. 각 코멘트는 한 문장, 가능하면 구체적 숫자를 인용하고, + 잔소리·과장 없이 담백하게. 반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지): + {"bullets":["문장1","문장2"]} + 관점 예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율. + 데이터가 빈약하거나 코멘트할 게 없으면 bullets 를 빈 배열로. + """; + Map reqBody = Map.of( + "model", model, + "max_tokens", 400, + "system", system, + "messages", List.of(Map.of("role", "user", "content", summaryJson))); + JsonNode resp = rest.post() + .uri("/v1/messages") + .header("x-api-key", apiKey) + .header("anthropic-version", "2023-06-01") + .contentType(MediaType.APPLICATION_JSON) + .body(reqBody) + .retrieve() + .body(JsonNode.class); + String out = stripFences(resp.path("content").path(0).path("text").asText("")).trim(); + log.info("[ai-comment] 원문='{}'", out); + String json = extractJson(out); + if (json == null) return List.of(); + JsonNode arr = om.readTree(json).path("bullets"); + List bullets = new ArrayList<>(); + if (arr.isArray()) { + for (JsonNode n : arr) { + String s = n.asText("").trim(); + if (!s.isEmpty()) bullets.add(s); + } + } + log.info("[ai-comment] 결과 — {}개", bullets.size()); + return bullets; + } catch (Exception e) { + log.warn("[ai-comment] 실패: {}", e.toString()); + return List.of(); + } + } + /** 분류·계좌 후보와 오늘 날짜를 시스템 프롬프트 템플릿에 주입. */ private String withHints(String body, LocalDate today, List categoryHints, List walletHints) { String catList = (categoryHints == null || categoryHints.isEmpty()) ? "(없음)" : String.join(", ", categoryHints); diff --git a/src/main/java/com/sb/web/common/config/WebConfig.java b/src/main/java/com/sb/web/common/config/WebConfig.java index 7a456db..a2754ff 100644 --- a/src/main/java/com/sb/web/common/config/WebConfig.java +++ b/src/main/java/com/sb/web/common/config/WebConfig.java @@ -39,6 +39,7 @@ public class WebConfig implements WebMvcConfigurer { .addPathPatterns( "/api/account/stats", "/api/account/category-stats", + "/api/account/ai-comment", "/api/account/networth/trend", "/api/account/budgets", "/api/account/budgets/**", "/api/account/recurrings", "/api/account/recurrings/**",