이번 달 집계(수입·지출·전월대비·예산·분류별)만 Claude Haiku에 보내 2~3개 코멘트 생성. POST /account/ai-comment(유료 게이트), 거래 원문 미전송. 미설정/실패 시 빈 코멘트→카드 숨김. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -250,6 +250,14 @@ public class AccountController {
|
|||||||
return accountService.parseText(req);
|
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")
|
@PostMapping("/entries/{id}/confirm")
|
||||||
public AccountEntryResponse confirm(
|
public AccountEntryResponse confirm(
|
||||||
|
|||||||
@@ -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<CategoryAmount> categories; // 분류별 지출 상위
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class CategoryAmount {
|
||||||
|
private String category;
|
||||||
|
private long total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> bullets;
|
||||||
|
}
|
||||||
@@ -322,6 +322,18 @@ public class AccountService {
|
|||||||
.build();
|
.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
|
@Transactional
|
||||||
public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) {
|
public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) {
|
||||||
CardNotificationParser.Parsed p =
|
CardNotificationParser.Parsed p =
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -107,6 +108,51 @@ public class AiEntryParser {
|
|||||||
return callClaude(system, content, 400, today, categoryHints, walletHints);
|
return callClaude(system, content, 400, today, categoryHints, walletHints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 이번 달 집계(JSON) → 2~3개의 한국어 재무 코멘트. 미설정/실패 시 빈 리스트. */
|
||||||
|
public List<String> financeComment(String summaryJson) {
|
||||||
|
if (!enabled() || summaryJson == null || summaryJson.isBlank()) return List.of();
|
||||||
|
try {
|
||||||
|
String system = """
|
||||||
|
너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고
|
||||||
|
도움이 되는 코멘트를 2~3개 만든다. 각 코멘트는 한 문장, 가능하면 구체적 숫자를 인용하고,
|
||||||
|
잔소리·과장 없이 담백하게. 반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지):
|
||||||
|
{"bullets":["문장1","문장2"]}
|
||||||
|
관점 예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율.
|
||||||
|
데이터가 빈약하거나 코멘트할 게 없으면 bullets 를 빈 배열로.
|
||||||
|
""";
|
||||||
|
Map<String, Object> 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<String> 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<String> categoryHints, List<String> walletHints) {
|
private String withHints(String body, LocalDate today, List<String> categoryHints, List<String> walletHints) {
|
||||||
String catList = (categoryHints == null || categoryHints.isEmpty()) ? "(없음)" : String.join(", ", categoryHints);
|
String catList = (categoryHints == null || categoryHints.isEmpty()) ? "(없음)" : String.join(", ", categoryHints);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.addPathPatterns(
|
.addPathPatterns(
|
||||||
"/api/account/stats",
|
"/api/account/stats",
|
||||||
"/api/account/category-stats",
|
"/api/account/category-stats",
|
||||||
|
"/api/account/ai-comment",
|
||||||
"/api/account/networth/trend",
|
"/api/account/networth/trend",
|
||||||
"/api/account/budgets", "/api/account/budgets/**",
|
"/api/account/budgets", "/api/account/budgets/**",
|
||||||
"/api/account/recurrings", "/api/account/recurrings/**",
|
"/api/account/recurrings", "/api/account/recurrings/**",
|
||||||
|
|||||||
Reference in New Issue
Block a user