Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e4d0eef
fix: 소셜 중복 회원 생성 방지 로직 추가
bebeis May 29, 2025
295c71c
fix: GlobalExHandler 수정
bebeis May 29, 2025
941b5e3
refactor: 예외 처리 코드, 로직 개선
bebeis Jun 9, 2025
a2c27ba
refactor: 예외 처리 코드, 로직 개선
bebeis Jun 9, 2025
282dabc
fix: Group Api 응답 데이터 수정
bebeis Jun 9, 2025
5f9a1db
fix: 서비스 계층에서 불필요한 Dto 필드 제거
bebeis Jun 9, 2025
b123572
refactor: Api URI 및 Dto 클래스명 개선
bebeis Jun 9, 2025
3960006
feat: SchoolGroup CRUD 기능 구현
bebeis Jun 9, 2025
6dfa623
fix: 잘못 적용된 검증 애노테이션 수정
bebeis Jun 9, 2025
2a911b5
refactor: pattern variable 적용
bebeis Jun 9, 2025
eac9874
feat: SchoolGroup CRUD 기능 구현
bebeis Jun 9, 2025
04f1458
feat: 예산 정보 CRUD 기능 구현
bebeis Jun 11, 2025
d8f1e47
test: 예산 도메인 기능 및 비즈니스 로직 테스트
bebeis Jun 11, 2025
69431e9
refactor: 검증 로직 추가 및 불필요 메서드 제거
bebeis Jun 11, 2025
d340563
refactor: YearMonth Format 애노테이션 등록 및 적용
bebeis Jun 11, 2025
2e958eb
fix: Api 엔드포인트 경로 수정
bebeis Jun 11, 2025
2217fef
refactor: 예산 조회 정렬 기준 지정
bebeis Jun 11, 2025
8e69062
fix: 컨트롤러 메서드 파라미터 수정
bebeis Jun 11, 2025
9369b4a
feat: 예산 도메인 기능 및 비즈니스 로직 구현
bebeis Jun 11, 2025
fc3fd4e
refactor: 회원 프로필 API 코드 개선
bebeis Jun 11, 2025
40e6c68
refactor: 쿼리 최적화
bebeis Jun 11, 2025
386c45d
refactor: 불필요한 필드, 파라미터 제거
bebeis Jun 11, 2025
d49d338
refactor: Api Controller 클래스 구조 리팩토링
bebeis Jun 11, 2025
e1da497
fix: 쿼리 오타 수정
bebeis Jun 11, 2025
a3ca754
test: 리팩토링에 따른 테스트 코드 구조 수정
bebeis Jun 11, 2025
b4286f0
test: 해피/예외 케이스 테스트 추가
bebeis Jun 12, 2025
ff0ca7a
chore: Spring AI 의존성 추가
bebeis Jun 12, 2025
289b268
feat: 국민은행 메시지 파싱 기능 추가
bebeis Jun 12, 2025
797c8e8
feat: 신한은행 메시지 파싱 기능 추가
bebeis Jun 12, 2025
25c51e0
feat: 농협은행 메시지 파싱 기능 추가
bebeis Jun 12, 2025
c1f44f9
feat: AI 메시지 파싱 기능 추가
bebeis Jun 12, 2025
243124c
feat: 메시지 파싱 커맨드 객체 추가
bebeis Jun 12, 2025
ccf4323
test: 결제 메시지 파싱 테스트 구현
bebeis Jun 12, 2025
8c3b748
refactor: Presentation Layer 구조 개선
bebeis Jun 12, 2025
8570bae
feat: 결제 메시지 파싱 기능 추가
bebeis Jun 12, 2025
c2568e0
fix: 의존성 순환 문제 수정
bebeis Jun 12, 2025
f36c70a
refactor: 필드명 직관적으로 수정
bebeis Jun 12, 2025
3493d07
feat: 지출내역 CRUD 기능 구현
bebeis Jun 12, 2025
d4288cf
fix: 타입 오류 수정
bebeis Jun 12, 2025
34bde9a
test: 지출내역 CRUD 기능 테스트
bebeis Jun 12, 2025
5711da4
refactor: 지출내역 관련 코드 개선
bebeis Jun 12, 2025
6c627ff
feat: 지출내역 CRUD API 구현
bebeis Jun 12, 2025
2546db7
feat: 월별 조회 API 추가
bebeis Jun 12, 2025
99a6daa
test: Infra 계층 테스트 추가
bebeis Jun 12, 2025
8062078
test: Persistence Layer 테스트 작성
bebeis Jun 12, 2025
1b71d8d
fix: Api Request validation을 Controller에서도 수행하도록 변경
bebeis Jun 12, 2025
8a62235
test: Controller Validation 적용에 따른 테스트 변경
bebeis Jun 12, 2025
e0e140b
fix: Api URI 경로 변수 누락 수정
bebeis Jun 12, 2025
ecad356
test: 회원가입 테스트 API URI 오타 수정
bebeis Jun 12, 2025
4e14ffc
test: 주소변환 Api, Spring 커스텀 빈 테스트 코드 추가
bebeis Jun 12, 2025
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
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}

ext {
springAiVersion = "1.0.0"
}

group = 'com.stcom'
version = '0.0.1-SNAPSHOT'

Expand Down Expand Up @@ -33,6 +37,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
implementation 'org.springframework.ai:spring-ai-bom:1.0.0'
implementation 'org.springframework.ai:spring-ai-starter-model-vertex-ai-gemini'
implementation 'com.google.protobuf:protobuf-java:3.25.5'
Comment on lines +40 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation 'org.springframework.ai:spring-ai-bom:1.0.0'
implementation 'org.springframework.ai:spring-ai-starter-model-vertex-ai-gemini'
implementation 'com.google.protobuf:protobuf-java:3.25.5'
implementation 'org.springframework.ai:spring-ai-starter-model-vertex-ai-gemini'
implementation 'com.google.protobuf:protobuf-java:3.25.5'
🤖 Prompt for AI Agents
In build.gradle around lines 40 to 42, the spring-ai-bom dependency is
incorrectly added with implementation, causing the BOM POM JAR to be included at
runtime. Remove the line "implementation
'org.springframework.ai:spring-ai-bom:1.0.0'" or change it to use
"platform('org.springframework.ai:spring-ai-bom:1.0.0')" so it is declared
properly in dependency management without being included as a runtime
dependency.

runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화/역직렬화에 Jackson 사용
compileOnly 'org.projectlombok:lombok'
Expand All @@ -51,4 +58,10 @@ tasks.named('test') {

jar {
enabled = false
}

dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion"
}
}
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Map 대신 List/빈 주입 사용 권장

Map.of(...)로 직접 생성한 파서는 Spring 컨테이너가 관리하지 않아 AOP, 설정 주입 등이 불가능합니다.
또한 키를 사용하지 않고 순회만 하므로 다음과 같이 개선하면 확장성이 높아집니다.

- private final Map<String, CreditMessageParser> parsers = Map.of(
-     "KB", new KBCreditMessageParser(),
-     "NH", new NHCreditMessageParser(),
-     "SH", new SHCreditMessageParser()
- );
+ private final List<CreditMessageParser> parsers;

그리고 생성자 주입 시 @AutowiredList<CreditMessageParser>를 받아오면 새 파서를 추가해도 코드 변경이 필요 없습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/stcom/smartmealtable/component/creditmessage/CreditMessageManager.java
around lines 13 to 17, replace the direct Map.of(...) initialization of parsers
with Spring-managed beans by injecting a List<CreditMessageParser> via
constructor with @Autowired. This allows Spring to manage the parser instances,
enabling AOP and configuration injection, and improves extensibility by avoiding
hardcoded keys and manual map updates when adding new parsers.


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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ChatClient 매번 생성 → 싱글톤 재사용 고려

parse 호출마다 chatClientBuilder.build() 로 새 클라이언트를 생성하면 연결 초기화 비용이 큽니다.
빈 초기화 시점에 ChatClient 를 한 번 만들어 필드에 보관하고 재사용하도록 리팩터링하면 성능과 자원 사용이 개선됩니다.

Also applies to: 27-28

🤖 Prompt for AI Agents
In
src/main/java/com/stcom/smartmealtable/component/creditmessage/GeminiCreditMessageParser.java
around lines 16 to 18 and 27 to 28, the code creates a new ChatClient instance
on every parse call by invoking chatClientBuilder.build(), which is inefficient.
Refactor by initializing a single ChatClient instance once during the class or
bean initialization and store it in a private final field. Then reuse this
ChatClient instance in the parse method instead of building a new one each time
to improve performance and resource usage.


@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);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예산 한도 변경 메서드 검증 로직은 적절하지만 추가 validation 고려 필요

null·음수 체크는 좋습니다. 다만 limit.compareTo(spendAmount) < 0 인 경우(이미 초과 사용 중인데 한도를 더 낮추는 상황)도 예외로 막을지, 비즈니스 요건을 다시 확인해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java around lines
71 to 76, the changeLimit method currently validates that the new limit is not
null and not negative, but it does not check if the new limit is less than the
current spendAmount. To fix this, add a validation that throws an exception if
limit.compareTo(spendAmount) < 0, preventing setting a limit lower than the
already spent amount, unless business requirements specify otherwise.


public void subtractSpent(BigDecimal spent) {
this.spendAmount = this.spendAmount.subtract(spent);
}
Comment on lines +78 to +80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void subtractSpent(BigDecimal spent) {
this.spendAmount = this.spendAmount.subtract(spent);
}
public void subtractSpent(BigDecimal 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;
}
🤖 Prompt for AI Agents
In src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java around lines
78 to 80, the subtractSpent method can reduce spendAmount to a negative value
because it lacks validation. Add checks to ensure the spent parameter is not
null and non-negative, and verify that subtracting spent does not make
spendAmount negative. If it would, handle this case appropriately, such as
throwing an exception or preventing the subtraction.

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ public DailyBudget(MemberProfile memberProfile, BigDecimal limit,

@Column(name = "daily_budget_date")
private LocalDate date;

}
Loading