-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 회원 데이터 CRUD API #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e4d0eef
295c71c
941b5e3
a2c27ba
282dabc
5f9a1db
b123572
3960006
6dfa623
2a911b5
eac9874
04f1458
d8f1e47
69431e9
d340563
2e958eb
2217fef
8e69062
9369b4a
fc3fd4e
40e6c68
386c45d
d49d338
e1da497
a3ca754
b4286f0
ff0ca7a
289b268
797c8e8
25c51e0
c1f44f9
243124c
ccf4323
8c3b748
8570bae
c2568e0
f36c70a
3493d07
d4288cf
34bde9a
5711da4
6c627ff
2546db7
99a6daa
8062078
1b71d8d
8a62235
e0e140b
ecad356
4e14ffc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import java.util.Map; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class CreditMessageManager { | ||
|
|
||
| private final GeminiCreditMessageParser geminiParser; | ||
|
|
||
| private final Map<String, CreditMessageParser> parsers = Map.of( | ||
| "KB", new KBCreditMessageParser(), | ||
| "NH", new NHCreditMessageParser(), | ||
| "SH", new SHCreditMessageParser() | ||
| ); | ||
|
Comment on lines
+13
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Map 대신 List/빈 주입 사용 권장
- private final Map<String, CreditMessageParser> parsers = Map.of(
- "KB", new KBCreditMessageParser(),
- "NH", new NHCreditMessageParser(),
- "SH", new SHCreditMessageParser()
- );
+ private final List<CreditMessageParser> parsers;그리고 생성자 주입 시
🤖 Prompt for AI Agents |
||
|
|
||
| public ExpenditureDto parseMessage(String message) { | ||
| if (message == null || message.isEmpty()) { | ||
| throw new IllegalArgumentException("메시지가 비어 있습니다."); | ||
| } | ||
|
|
||
| for (CreditMessageParser parser : parsers.values()) { | ||
| if (parser.checkVendor(message)) { | ||
| try { | ||
| return parser.parse(message); | ||
| } catch (Exception ignore) { | ||
| // 룰 기반 파싱 실패 – Gemini 로 fallback | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return geminiParser.parse(message); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| public interface CreditMessageParser { | ||
|
|
||
| boolean checkVendor(String message); | ||
|
|
||
| ExpenditureDto parse(String message); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Data; | ||
| import lombok.ToString; | ||
|
|
||
| @Data | ||
| @AllArgsConstructor | ||
| @ToString | ||
| public class ExpenditureDto { | ||
|
|
||
| private String vendor; | ||
| private LocalDateTime spentDate; | ||
| private Long amount; | ||
| private String tradeName; | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.time.LocalDateTime; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.chat.client.ChatClient.Builder; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class GeminiCreditMessageParser implements CreditMessageParser { | ||
|
|
||
| private final Builder chatClientBuilder; | ||
|
|
||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion ChatClient 매번 생성 → 싱글톤 재사용 고려
Also applies to: 27-28 🤖 Prompt for AI Agents |
||
|
|
||
| @Override | ||
| public boolean checkVendor(String message) { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public ExpenditureDto parse(String message) { | ||
| ChatClient chatClient = chatClientBuilder.build(); | ||
|
|
||
| String prompt = String.format(""" | ||
| 너는 대한민국의 신용카드 승인 문자(SMS)를 파싱해서 JSON 형태로 반환하는 전문가야. | ||
| 반드시 아래 형식의 JSON 만 출력해. 설명 문구나 코드 블록 표시(```)는 절대 포함하면 안 돼. | ||
|
|
||
| { | ||
| \"vendor\": \"<카드사 영문 약어, 예: KB, NH, SH, UNKNOWN>\", | ||
| \"dateTime\": \"<ISO-8601 형식 yyyy-MM-dd'T'HH:mm:ss>\", | ||
| \"amount\": <숫자형 원화 금액>, | ||
| \"tradeName\": \"<가맹점명>\" | ||
| } | ||
|
|
||
| 다음은 파싱 대상 SMS 원문이다: | ||
| %s | ||
| """, message); | ||
|
|
||
| String jsonResponse = chatClient.prompt() | ||
| .user(prompt) | ||
| .call() | ||
| .content(); | ||
| try { | ||
| JsonNode root = MAPPER.readTree(jsonResponse); | ||
| String vendor = root.path("vendor").asText("UNKNOWN"); | ||
| String dateTimeStr = root.path("dateTime").asText(); | ||
| long amount = root.path("amount").asLong(); | ||
| String tradeName = root.path("tradeName").asText(); | ||
|
|
||
| LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr); | ||
| return new ExpenditureDto(vendor, dateTime, amount, tradeName); | ||
| } catch (Exception e) { | ||
| throw new IllegalArgumentException("Gemini 파싱 실패: " + e.getMessage(), e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class KBCreditMessageParser implements CreditMessageParser { | ||
|
|
||
| private static final Pattern KB_PATTERN = Pattern.compile( | ||
| // 1: MM/dd, 2: HH:mm, 3: amount, 4: trade name | ||
| "\\[KB국민카드]\\s*(\\d{2}/\\d{2})\\s*(\\d{2}:\\d{2})\\s*승인\\s*([\\d,]+)원\\s*[가-힣A-Za-z]*\\s*(.+)" | ||
| ); | ||
|
|
||
| private static final DateTimeFormatter FORMATTER = | ||
| DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); | ||
|
|
||
| @Override | ||
| public boolean checkVendor(String message) { | ||
| // 메시지에 "KB"가 포함되어 있는지 확인 | ||
| return message != null && message.contains("KB"); | ||
| } | ||
|
|
||
| @Override | ||
| public ExpenditureDto parse(String message) { | ||
| if (message == null) { | ||
| throw new IllegalArgumentException("메시지가 비어있습니다."); | ||
| } | ||
|
|
||
| Matcher matcher = KB_PATTERN.matcher(message); | ||
| if (!matcher.find()) { | ||
| throw new IllegalArgumentException("올바르지 않은 메시지 포맷입니다. " + message); | ||
| } | ||
|
|
||
| String datePart = matcher.group(1); | ||
| String timePart = matcher.group(2); | ||
| String amountPart = matcher.group(3).replace(",", ""); // “11,000” → “11000” | ||
| String tradeName = matcher.group(4).trim(); | ||
|
|
||
| int currentYear = LocalDate.now().getYear(); | ||
| LocalDateTime dateTime = LocalDateTime.parse( | ||
| currentYear + "/" + datePart + " " + timePart, FORMATTER | ||
| ); | ||
|
|
||
| long amount = Long.parseLong(amountPart); | ||
|
|
||
| return new ExpenditureDto("KB", dateTime, amount, tradeName); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class NHCreditMessageParser implements CreditMessageParser { | ||
|
|
||
| private static final Pattern NH_PATTERN = Pattern.compile( | ||
| "NH(?:농협)?카드.*?승인.*?([\\d,]+)원.*?(\\d{2}/\\d{2})\\s*(\\d{2}:\\d{2})\\s+(.+?)(?:\\s+(?:총누적|잔여).*)?$" | ||
| ); | ||
|
|
||
| private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); | ||
|
|
||
| @Override | ||
| public boolean checkVendor(String message) { | ||
| return message != null && (message.contains("NH") || message.contains("농협")); | ||
| } | ||
|
|
||
| @Override | ||
| public ExpenditureDto parse(String message) { | ||
| if (message == null) { | ||
| throw new IllegalArgumentException("메시지가 비어 있습니다."); | ||
| } | ||
|
|
||
| Matcher m = NH_PATTERN.matcher(message); | ||
| if (!m.find()) { | ||
| throw new IllegalArgumentException("농협카드 SMS 형식을 인식하지 못했습니다: " + message); | ||
| } | ||
|
|
||
| long amount = Long.parseLong(m.group(1).replace(",", "")); | ||
| int thisYear = LocalDate.now().getYear(); | ||
| LocalDateTime dateTime = LocalDateTime.parse( | ||
| thisYear + "/" + m.group(2) + " " + m.group(3), FMT | ||
| ); | ||
|
|
||
| String trade = m.group(4).trim(); | ||
|
|
||
| return new ExpenditureDto("NH", dateTime, amount, trade); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.stcom.smartmealtable.component.creditmessage; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class SHCreditMessageParser implements CreditMessageParser { | ||
|
|
||
| private static final Pattern SH_PATTERN = Pattern.compile( | ||
| "신한카드.*?승인.*?([\\d,]+)원(?:\\([^)]*\\))?\\s*(\\d{2}/\\d{2})\\s*(\\d{2}:\\d{2})\\s+(.+?)(?:\\s+(?:누적|잔여).*)?$" | ||
| ); | ||
|
|
||
| private static final DateTimeFormatter FMT = | ||
| DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); | ||
|
|
||
| @Override | ||
| public boolean checkVendor(String message) { | ||
| return message != null && message.contains("신한카드"); | ||
| } | ||
|
|
||
| @Override | ||
| public ExpenditureDto parse(String message) { | ||
| if (message == null) { | ||
| throw new IllegalArgumentException("메시지가 비어 있습니다."); | ||
| } | ||
|
|
||
| Matcher m = SH_PATTERN.matcher(message); | ||
| if (!m.find()) { | ||
| throw new IllegalArgumentException("신한카드 SMS 형식을 인식하지 못했습니다: " + message); | ||
| } | ||
|
|
||
| long amount = Long.parseLong(m.group(1).replace(",", "")); | ||
| int year = LocalDate.now().getYear(); | ||
| LocalDateTime dateTime = LocalDateTime.parse( | ||
| year + "/" + m.group(2) + " " + m.group(3), FMT | ||
| ); | ||
|
|
||
| String tradeName = m.group(4).trim() | ||
| .replaceAll("\\s*(누적|잔여|잔액).*", "") // 혹시 남은 잔여표기가 끼어들면 제거 | ||
| .trim(); | ||
|
|
||
| return new ExpenditureDto("SH", dateTime, amount, tradeName); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,6 +38,7 @@ public abstract class Budget extends BaseTimeEntity { | |||||||||||||||||||||||||||
| @Column(name = "budget_limit") | ||||||||||||||||||||||||||||
| private BigDecimal limit; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| protected Budget(MemberProfile memberProfile, BigDecimal limit) { | ||||||||||||||||||||||||||||
| this.memberProfile = memberProfile; | ||||||||||||||||||||||||||||
| this.limit = limit; | ||||||||||||||||||||||||||||
|
|
@@ -66,4 +67,15 @@ public BigDecimal getAvailableAmount() { | |||||||||||||||||||||||||||
| public boolean isOverLimit() { | ||||||||||||||||||||||||||||
| return spendAmount.compareTo(limit) > 0; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public void changeLimit(BigDecimal limit) { | ||||||||||||||||||||||||||||
| if (limit == null || limit.signum() < 0) { | ||||||||||||||||||||||||||||
| throw new IllegalArgumentException("예산 한도는 0 이상이어야 합니다."); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| this.limit = limit; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+71
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 예산 한도 변경 메서드 검증 로직은 적절하지만 추가 validation 고려 필요
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public void subtractSpent(BigDecimal spent) { | ||||||||||||||||||||||||||||
| this.spendAmount = this.spendAmount.subtract(spent); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. subtractSpent()가 사용금액을 음수로 만들 수 있음 매개변수·결과값에 대한 검증이 없습니다. 다음과 같이 보호 로직을 추가하는 것을 권장합니다. public void subtractSpent(BigDecimal spent) {
- this.spendAmount = this.spendAmount.subtract(spent);
+ if (spent == null || spent.signum() < 0) {
+ throw new IllegalArgumentException("차감 금액은 0 이상이어야 합니다.");
+ }
+ BigDecimal updated = this.spendAmount.subtract(spent);
+ if (updated.signum() < 0) {
+ throw new IllegalStateException("차감 결과가 음수가 될 수 없습니다.");
+ }
+ this.spendAmount = updated;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BOM 의존성을
implementation에 추가하면 실행 시 POM JAR가 포함됩니다spring-ai-bom은 BOM 전용 아티팩트이므로dependencyManagement에서만 선언해야 합니다.implementation 'org.springframework.ai:spring-ai-bom:1.0.0'라인을 제거하거나platform으로 선언해 주세요.- implementation 'org.springframework.ai:spring-ai-bom:1.0.0'📝 Committable suggestion
🤖 Prompt for AI Agents