From d0ebd485d18ba161ec3858339b65d86f2b1c07a3 Mon Sep 17 00:00:00 2001
From: dohee <152317074+seamooll@users.noreply.github.com>
Date: Mon, 2 Feb 2026 23:21:48 +0900
Subject: [PATCH 1/4] =?UTF-8?q?[REFACTOR/#64]=20=EC=9D=80=ED=96=89=20?=
=?UTF-8?q?=EA=B3=84=EC=A2=8C=20=EC=A1=B0=ED=9A=8C=20API=20=EB=AA=A9?=
=?UTF-8?q?=ED=91=9C=20=EC=A0=95=EB=B3=B4=20=ED=86=B5=ED=95=A9=20=EB=B0=8F?=
=?UTF-8?q?=20QueryDSL=20=EC=A0=84=ED=99=98=20(#65)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ Related Issue
- Closes #64
## ๐ Summary
- ์ํ๋ณ ๋ชฉํ ์กฐํ๋ฅผ ๋ณ๋ API๋ก ๋ถ๋ฆฌํ์ง ์๊ณ , ๊ณ์ข ์กฐํ API์ ์ฐ๊ฒฐ๋ ๋ชฉํ ์ ๋ณด๋ฅผ ํจ๊ป ์ ๊ณตํ๋๋ก ์์ ํ์์ต๋๋ค.
- JPQL ๊ธฐ๋ฐ ์กฐํ ๋ก์ง์ QueryDSL๋ก ๋ฆฌํฉํ ๋งํ์์ต๋๋ค.
## ๐ Changes
- [X] API ๋ณ๊ฒฝ (์ถ๊ฐ/์์ )
- [ ] ๋ฐ์ดํฐ ๋ฐ ๋๋ฉ์ธ ๋ณ๊ฒฝ (DB, ๋น์ฆ๋์ค ๋ก์ง)
- [ ] ์ค์ ๋๋ ์ธํ๋ผ ๊ด๋ จ ๋ณ๊ฒฝ
- [X] ๋ฆฌํฉํ ๋ง
## ๐ฌ Questions & Review Points
## ๐ธ API Test Results (Swagger)
## โ
Checklist
- [x] API ํ
์คํธ ์๋ฃ
- [x] ํ
์คํธ ๊ฒฐ๊ณผ ์ฌ์ง ์ฒจ๋ถ
- [x] ๋น๋ ์ฑ๊ณต ํ์ธ (./gradlew build)
---
.../asset/controller/AssetController.java | 6 +-
.../asset/controller/AssetControllerDocs.java | 102 +++++++++++++-----
.../asset/converter/AssetConverter.java | 55 +++++++++-
.../domain/asset/dto/res/BankResDTO.java | 74 +++++++++++++
.../domain/asset/entity/BankAccount.java | 4 +
.../bank/BankAccountRepository.java | 42 --------
.../bankAccount/BankAccountRepository.java | 7 ++
.../BankAccountRepositoryCustom.java | 16 +++
.../BankAccountRepositoryImpl.java | 59 ++++++++++
.../BankTransactionRepository.java | 2 +-
.../BankTransactionRepositoryCustom.java | 2 +-
.../BankTransactionRepositoryImpl.java | 2 +-
.../card/{ => card}/CardRepository.java | 2 +-
.../CardApprovalRepository.java | 2 +-
.../CardApprovalRepositoryCustom.java | 2 +-
.../CardApprovalRepositoryImpl.java | 2 +-
.../{ => command}/AssetSyncService.java | 10 +-
.../{ => query}/AssetQueryService.java | 12 +--
.../service/ConnectionEventListener.java | 2 +-
.../domain/goal/converter/GoalConverter.java | 14 ++-
.../umc/valuedi/domain/goal/entity/Goal.java | 7 +-
.../goal/repository/GoalRepository.java | 2 +-
.../goal/repository/GoalRepositoryCustom.java | 9 ++
.../goal/repository/GoalRepositoryImpl.java | 29 +++++
.../goal/service/query/GoalQueryService.java | 6 +-
.../service/command/LedgerSyncService.java | 4 +-
26 files changed, 374 insertions(+), 100 deletions(-)
delete mode 100644 src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankAccountRepository.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepository.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
rename src/main/java/org/umc/valuedi/domain/asset/repository/bank/{ => bankTransaction}/BankTransactionRepository.java (84%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/bank/{ => bankTransaction}/BankTransactionRepositoryCustom.java (73%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/bank/{ => bankTransaction}/BankTransactionRepositoryImpl.java (97%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/card/{ => card}/CardRepository.java (96%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/card/{ => cardApproval}/CardApprovalRepository.java (88%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/card/{ => cardApproval}/CardApprovalRepositoryCustom.java (73%)
rename src/main/java/org/umc/valuedi/domain/asset/repository/card/{ => cardApproval}/CardApprovalRepositoryImpl.java (98%)
rename src/main/java/org/umc/valuedi/domain/asset/service/{ => command}/AssetSyncService.java (95%)
rename src/main/java/org/umc/valuedi/domain/asset/service/{ => query}/AssetQueryService.java (82%)
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
diff --git a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
index 66613341..3275f628 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
@@ -9,7 +9,7 @@
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
import org.umc.valuedi.domain.asset.dto.res.BankResDTO;
import org.umc.valuedi.domain.asset.dto.res.CardResDTO;
-import org.umc.valuedi.domain.asset.service.AssetQueryService;
+import org.umc.valuedi.domain.asset.service.query.AssetQueryService;
import org.umc.valuedi.domain.connection.service.ConnectionQueryService;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.apiPayload.code.GeneralSuccessCode;
@@ -65,8 +65,8 @@ public ApiResponse getAllBankAccounts(
return ApiResponse.onSuccess(GeneralSuccessCode.OK, assetQueryService.getAllBankAccounts(memberId));
}
- @GetMapping("/banks/{bankCode}/accounts")
- public ApiResponse getAccountsByBank(
+ @GetMapping("/banks/{bankCode}")
+ public ApiResponse getAccountsByBank(
@PathVariable String bankCode,
@CurrentMember Long memberId
) {
diff --git a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
index 4c107a53..b76f3c05 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
@@ -189,13 +189,26 @@ ApiResponse getCardsByIssuer(
"result": {
"accountList": [
{
+ "accountId": 1,
"accountName": "KB๋๋ผ์ฌ๋์ฐ๋ํต์ฅ",
"balanceAmount": 150000,
"organization": "0004",
- "createdAt": "2024-05-20T10:00:00"
+ "createdAt": "2024-05-20T10:00:00",
+ "goalInfo": null
+ },
+ {
+ "accountId": 2,
+ "accountName": "KB๊ตญ๋ฏผONEํต์ฅ",
+ "balanceAmount": 300000,
+ "organization": "0004",
+ "createdAt": "2024-05-21T10:00:00",
+ "goalInfo": {
+ "goalId": 2,
+ "title": "์ฌํ"
+ }
}
],
- "totalCount": 1
+ "totalCount": 2
}
}
"""
@@ -205,38 +218,77 @@ ApiResponse getCardsByIssuer(
})
ApiResponse getAllBankAccounts(@CurrentMember Long memberId);
- @Operation(summary = "์ํ๋ณ ๊ณ์ข ๋ชฉ๋ก ์กฐํ", description = "ํน์ ์ํ์ ์ฐ๋๋ ๊ณ์ข ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.")
+ @Operation(summary = "์ํ๋ณ ๊ณ์ข ๋ฐ ๋ชฉํ ๋ชฉ๋ก ์กฐํ", description = "ํน์ ์ํ์ ์ฐ๋๋ ๊ณ์ข์ ๋ชฉํ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "์ฑ๊ณต - ์ํ๋ณ ๊ณ์ข ๋ชฉ๋ก ๋ฐํ",
content = @Content(
schema = @Schema(implementation = ApiResponse.class),
- examples = @ExampleObject(
- name = "์ฑ๊ณต ์์",
- value = """
- {
- "isSuccess": true,
- "code": "COMMON200",
- "message": "์ฑ๊ณต์
๋๋ค.",
- "result": {
- "accountList": [
- {
- "accountName": "KB๋๋ผ์ฌ๋์ฐ๋ํต์ฅ",
- "balanceAmount": 150000,
- "organization": "0004",
- "createdAt": "2024-05-20T10:00:00"
- }
- ],
- "totalCount": 1
- }
- }
- """
- )
+ examples = {
+ @ExampleObject(
+ name = "๋ชฉํ๊ฐ ์๋ ๊ฒฝ์ฐ",
+ value = """
+ {
+ "isSuccess": true,
+ "code": "COMMON200",
+ "message": "์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋์์ต๋๋ค.",
+ "result": {
+ "bankName": "์ฐ๋ฆฌ์ํ",
+ "totalBalance": 240732,
+ "accountList": [
+ {
+ "accountId": 2,
+ "accountName": "์ ์ถ์๊ธ",
+ "balanceAmount": 220732,
+ "connectedGoalId": null
+ },
+ {
+ "accountId": 3,
+ "accountName": "์ฒญ์ฝ์ ์ถ",
+ "balanceAmount": 20000,
+ "connectedGoalId": 101
+ }
+ ],
+ "goalList": [
+ {
+ "goalId": 2,
+ "title": "์ฌํํ",
+ "linkedAccountId": 2
+ }
+ ]
+ }
+ }
+ """
+ ),
+ @ExampleObject(
+ name = "๋ชฉํ๊ฐ ์๋ ๊ฒฝ์ฐ",
+ value = """
+ {
+ "isSuccess": true,
+ "code": "COMMON200",
+ "message": "์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋์์ต๋๋ค.",
+ "result": {
+ "bankName": "์ฐ๋ฆฌ์ํ",
+ "totalBalance": 100000,
+ "accountList": [
+ {
+ "accountId": 2,
+ "accountName": "์ ์ถ์๊ธ",
+ "balanceAmount": 100000,
+ "connectedGoalId": null
+ }
+ ],
+ "goalList": []
+ }
+ }
+ """
+ )
+ }
)
)
})
- ApiResponse getAccountsByBank(
+ ApiResponse getAccountsByBank(
@Parameter(description = "์ํ ์ฝ๋ (์: 0020)") String organization,
@CurrentMember Long memberId
);
diff --git a/src/main/java/org/umc/valuedi/domain/asset/converter/AssetConverter.java b/src/main/java/org/umc/valuedi/domain/asset/converter/AssetConverter.java
index 208db8f7..6c272265 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/converter/AssetConverter.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/converter/AssetConverter.java
@@ -1,25 +1,38 @@
package org.umc.valuedi.domain.asset.converter;
-import org.springframework.stereotype.Component;
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
import org.umc.valuedi.domain.asset.dto.res.BankResDTO;
import org.umc.valuedi.domain.asset.dto.res.CardResDTO;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.Card;
+import org.umc.valuedi.domain.connection.enums.Organization;
+import org.umc.valuedi.domain.goal.entity.Goal;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-@Component
public class AssetConverter {
// ๊ฐ๋ณ BankAccount ์ํฐํฐ -> BankAccountInfo ๋ณํ
public static BankResDTO.BankAccountInfo toBankAccountInfo(BankAccount account) {
+ BankResDTO.GoalInfo goalInfo = null;
+ if (account.getGoal() != null) {
+ Goal goal = account.getGoal();
+
+ goalInfo = BankResDTO.GoalInfo.builder()
+ .goalId(goal.getId())
+ .title(goal.getTitle())
+ .build();
+ }
+
return BankResDTO.BankAccountInfo.builder()
+ .accountId(account.getId())
.accountName(account.getAccountName())
.balanceAmount(account.getBalanceAmount())
.organization(account.getCodefConnection().getOrganization()) // ๊ธฐ๊ด์ฝ๋ ์ถ์ถ
.createdAt(account.getCreatedAt())
+ .goalInfo(goalInfo)
.build();
}
@@ -66,4 +79,42 @@ public static AssetResDTO.AssetSummaryCountDTO toAssetSummaryCountDTO(long accou
.totalAssetCount(accountCount + cardCount)
.build();
}
+
+ // ํน์ ์ํ์ ์์ฐ ํํฉ ์๋ต DTO ๋ณํ
+ public static BankResDTO.BankAssetResponse toBankAssetResponse(String organizationCode, List accounts) {
+ String bankName = Organization.getNameByCode(organizationCode);
+
+ long totalBalance = 0L;
+ List accountList = new ArrayList<>(accounts.size());
+ List goalList = new ArrayList<>();
+
+ for (BankAccount account : accounts) {
+ totalBalance += (account.getBalanceAmount() != null ? account.getBalanceAmount() : 0L);
+
+ Goal goal = account.getGoal();
+ Long connectedGoalId = (goal != null) ? goal.getId() : null;
+
+ accountList.add(BankResDTO.AccountInfo.builder()
+ .accountId(account.getId())
+ .accountName(account.getAccountName())
+ .balanceAmount(account.getBalanceAmount())
+ .connectedGoalId(connectedGoalId)
+ .build());
+
+ if (goal != null) {
+ goalList.add(BankResDTO.GoalSimpleInfo.builder()
+ .goalId(goal.getId())
+ .title(goal.getTitle())
+ .linkedAccountId(account.getId())
+ .build());
+ }
+ }
+
+ return BankResDTO.BankAssetResponse.builder()
+ .bankName(bankName)
+ .totalBalance(totalBalance)
+ .accountList(accountList)
+ .goalList(goalList)
+ .build();
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/dto/res/BankResDTO.java b/src/main/java/org/umc/valuedi/domain/asset/dto/res/BankResDTO.java
index 69a9020f..25f79d24 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/dto/res/BankResDTO.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/dto/res/BankResDTO.java
@@ -35,6 +35,19 @@ public static class BankConnection {
private ConnectionStatus status;
}
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Schema(description = "๋ชฉํ ์ ๋ณด")
+ public static class GoalInfo {
+ @Schema(description = "๋ชฉํ ID", example = "1")
+ private Long goalId;
+
+ @Schema(description = "๋ชฉํ๋ช
", example = "ํธ๊พธ์ฅ์ฌํ๊ฐ๊ณ ์ถ์ด์")
+ private String title;
+ }
+
@Getter
@Builder
@NoArgsConstructor
@@ -42,6 +55,9 @@ public static class BankConnection {
@Schema(description = "๊ฐ๋ณ ๊ณ์ข ์ ๋ณด")
public static class BankAccountInfo {
+ @Schema(description = "๊ณ์ข ID", example = "1")
+ private Long accountId;
+
@Schema(description = "๊ณ์ข๋ช
", example = "KB๊ตญ๋ฏผONEํต์ฅ")
private String accountName;
@@ -53,6 +69,9 @@ public static class BankAccountInfo {
@Schema(description = "๊ณ์ข ๋ฑ๋ก์ผ์ (์ ๋ ฌ ๊ธฐ์ค ํ์ธ์ฉ)", example = "2026-01-19T10:00:00")
private LocalDateTime createdAt;
+
+ @Schema(description = "์ฐ๊ฒฐ๋ ๋ชฉํ ์ ๋ณด (์์ผ๋ฉด null)")
+ private GoalInfo goalInfo;
}
@Getter
@@ -68,4 +87,59 @@ public static class BankAccountListDTO {
@Schema(description = "์ด ๊ณ์ข ๊ฐ์", example = "1")
private Integer totalCount;
}
+
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Schema(description = "์ํ๋ณ ์์ฐ ์กฐํ ์๋ต")
+ public static class BankAssetResponse {
+ @Schema(description = "์ํ๋ช
", example = "์ฐ๋ฆฌ์ํ")
+ private String bankName;
+
+ @Schema(description = "์ด ์์ก", example = "240732")
+ private Long totalBalance;
+
+ @Schema(description = "๊ณ์ข ๋ชฉ๋ก")
+ private List accountList;
+
+ @Schema(description = "๋ชฉํ ๋ชฉ๋ก")
+ private List goalList;
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Schema(description = "๊ฐ๋ตํ ๊ณ์ข ์ ๋ณด")
+ public static class AccountInfo {
+ @Schema(description = "๊ณ์ข ID", example = "1")
+ private Long accountId;
+
+ @Schema(description = "๊ณ์ข๋ช
", example = "์ ์ถ์๊ธ")
+ private String accountName;
+
+ @Schema(description = "์์ก", example = "220732")
+ private Long balanceAmount;
+
+ @Schema(description = "์ฐ๊ฒฐ๋ ๋ชฉํ ID (์์ผ๋ฉด null)", example = "1")
+ private Long connectedGoalId;
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Schema(description = "๊ฐ๋ตํ ๋ชฉํ ์ ๋ณด")
+ public static class GoalSimpleInfo {
+ @Schema(description = "๋ชฉํ ID", example = "1")
+ private Long goalId;
+
+ @Schema(description = "๋ชฉํ๋ช
", example = "์ฌํ")
+ private String title;
+
+ @Schema(description = "์ฐ๊ฒฐ๋ ๊ณ์ข ID", example = "1")
+ private Long linkedAccountId;
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/entity/BankAccount.java b/src/main/java/org/umc/valuedi/domain/asset/entity/BankAccount.java
index 752c301d..0c6cbdc0 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/entity/BankAccount.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/entity/BankAccount.java
@@ -5,6 +5,7 @@
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import org.umc.valuedi.domain.asset.enums.AccountGroup;
+import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.global.entity.BaseEntity;
import org.umc.valuedi.domain.connection.entity.CodefConnection;
@@ -85,6 +86,9 @@ public class BankAccount extends BaseEntity {
@JoinColumn(name = "codef_connection_id", nullable = false)
private CodefConnection codefConnection;
+ @OneToOne(mappedBy = "bankAccount", fetch = FetchType.LAZY)
+ private Goal goal;
+
public void assignConnection(CodefConnection connection) {
this.codefConnection = connection;
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankAccountRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankAccountRepository.java
deleted file mode 100644
index e605f902..00000000
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankAccountRepository.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.umc.valuedi.domain.asset.repository.bank;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-import org.umc.valuedi.domain.asset.entity.BankAccount;
-
-import java.util.List;
-
-public interface BankAccountRepository extends JpaRepository {
-
- /**
- * ์ ์ฒด ํ์ฑ ๊ณ์ข ๋ชฉ๋ก ์กฐํ (์ต์ ์)
- */
- @Query("SELECT ba FROM BankAccount ba " +
- "JOIN FETCH ba.codefConnection cc " +
- "WHERE cc.member.id = :memberId " +
- "AND ba.isActive = true " +
- "ORDER BY ba.createdAt DESC")
- List findAllByMemberId(@Param("memberId") Long memberId);
-
- /**
- * ํน์ ์ํ๋ณ ํ์ฑ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
- */
- @Query("SELECT ba FROM BankAccount ba " +
- "JOIN FETCH ba.codefConnection cc " +
- "WHERE cc.member.id = :memberId " +
- "AND cc.organization = :organization " +
- "AND ba.isActive = true")
- List findAllByMemberIdAndOrganization(
- @Param("memberId") Long memberId,
- @Param("organization") String organization
- );
-
- /**
- * ์ด ํ์ฑ ๊ณ์ข ์ ์นด์ดํธ
- */
- @Query("SELECT COUNT(ba) FROM BankAccount ba " +
- "WHERE ba.codefConnection.member.id = :memberId " +
- "AND ba.isActive = true")
- long countByMemberId(@Param("memberId") Long memberId);
-}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepository.java
new file mode 100644
index 00000000..d817e779
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepository.java
@@ -0,0 +1,7 @@
+package org.umc.valuedi.domain.asset.repository.bank.bankAccount;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+
+public interface BankAccountRepository extends JpaRepository, BankAccountRepositoryCustom {
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
new file mode 100644
index 00000000..35ad42d3
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
@@ -0,0 +1,16 @@
+package org.umc.valuedi.domain.asset.repository.bank.bankAccount;
+
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+
+import java.util.List;
+
+public interface BankAccountRepositoryCustom {
+ // ํน์ ์ํ๋ณ ํ์ฑ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
+ List findAllByMemberIdAndOrganization(Long memberId, String organization);
+
+ // ์ ์ฒด ํ์ฑ ๊ณ์ข ๋ชฉ๋ก ์กฐํ (์ต์ ์)
+ List findAllByMemberId(Long memberId);
+
+ // ์ด ํ์ฑ ๊ณ์ข ์ ์นด์ดํธ
+ long countByMemberId(Long memberId);
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
new file mode 100644
index 00000000..51cd559d
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
@@ -0,0 +1,59 @@
+package org.umc.valuedi.domain.asset.repository.bank.bankAccount;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+
+import java.util.List;
+
+import static org.umc.valuedi.domain.asset.entity.QBankAccount.bankAccount;
+import static org.umc.valuedi.domain.connection.entity.QCodefConnection.codefConnection;
+import static org.umc.valuedi.domain.goal.entity.QGoal.goal;
+
+@RequiredArgsConstructor
+public class BankAccountRepositoryImpl implements BankAccountRepositoryCustom {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public List findAllByMemberIdAndOrganization(Long memberId, String organization) {
+ return queryFactory
+ .selectFrom(bankAccount)
+ .join(bankAccount.codefConnection, codefConnection).fetchJoin()
+ .leftJoin(bankAccount.goal, goal).fetchJoin()
+ .where(
+ codefConnection.member.id.eq(memberId),
+ codefConnection.organization.eq(organization),
+ bankAccount.isActive.isTrue()
+ )
+ .fetch();
+ }
+
+ @Override
+ public List findAllByMemberId(Long memberId) {
+ return queryFactory
+ .selectFrom(bankAccount)
+ .join(bankAccount.codefConnection, codefConnection).fetchJoin()
+ .leftJoin(bankAccount.goal, goal).fetchJoin()
+ .where(
+ codefConnection.member.id.eq(memberId),
+ bankAccount.isActive.isTrue()
+ )
+ .orderBy(bankAccount.createdAt.desc())
+ .fetch();
+ }
+
+ @Override
+ public long countByMemberId(Long memberId) {
+ Long count = queryFactory
+ .select(bankAccount.count())
+ .from(bankAccount)
+ .join(bankAccount.codefConnection, codefConnection)
+ .where(
+ codefConnection.member.id.eq(memberId),
+ bankAccount.isActive.isTrue()
+ )
+ .fetchOne();
+ return count != null ? count : 0;
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
similarity index 84%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepository.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
index 7ac97e80..02d0f3cb 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.bank;
+package org.umc.valuedi.domain.asset.repository.bank.bankTransaction;
import org.springframework.data.jpa.repository.JpaRepository;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryCustom.java
similarity index 73%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryCustom.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryCustom.java
index f4048d43..f48a55b1 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryCustom.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryCustom.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.bank;
+package org.umc.valuedi.domain.asset.repository.bank.bankTransaction;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import java.util.List;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
similarity index 97%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryImpl.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
index fc3ae7c8..f73d09bf 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/BankTransactionRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.bank;
+package org.umc.valuedi.domain.asset.repository.bank.bankTransaction;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
similarity index 96%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/card/CardRepository.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
index 0a531ca3..fcd3d092 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.card;
+package org.umc.valuedi.domain.asset.repository.card.card;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
similarity index 88%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepository.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
index 6fe497b4..be58af96 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.card;
+package org.umc.valuedi.domain.asset.repository.card.cardApproval;
import org.springframework.data.jpa.repository.JpaRepository;
import org.umc.valuedi.domain.asset.entity.CardApproval;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryCustom.java
similarity index 73%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryCustom.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryCustom.java
index b8f52ada..aa60d96f 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryCustom.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryCustom.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.card;
+package org.umc.valuedi.domain.asset.repository.card.cardApproval;
import org.umc.valuedi.domain.asset.entity.CardApproval;
import java.util.List;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
similarity index 98%
rename from src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryImpl.java
rename to src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
index 8b30932a..18c68887 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/CardApprovalRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.repository.card;
+package org.umc.valuedi.domain.asset.repository.card.cardApproval;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/AssetSyncService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
similarity index 95%
rename from src/main/java/org/umc/valuedi/domain/asset/service/AssetSyncService.java
rename to src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
index 002f2f97..bb847003 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetSyncService.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.service;
+package org.umc.valuedi.domain.asset.service.command;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
@@ -12,10 +12,10 @@
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import org.umc.valuedi.domain.asset.entity.Card;
import org.umc.valuedi.domain.asset.entity.CardApproval;
-import org.umc.valuedi.domain.asset.repository.bank.BankAccountRepository;
-import org.umc.valuedi.domain.asset.repository.bank.BankTransactionRepository;
-import org.umc.valuedi.domain.asset.repository.card.CardApprovalRepository;
-import org.umc.valuedi.domain.asset.repository.card.CardRepository;
+import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
+import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
+import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
+import org.umc.valuedi.domain.asset.repository.card.card.CardRepository;
import org.umc.valuedi.domain.connection.entity.CodefConnection;
import org.umc.valuedi.domain.connection.enums.BusinessType;
import org.umc.valuedi.global.external.codef.service.CodefAssetService;
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/AssetQueryService.java b/src/main/java/org/umc/valuedi/domain/asset/service/query/AssetQueryService.java
similarity index 82%
rename from src/main/java/org/umc/valuedi/domain/asset/service/AssetQueryService.java
rename to src/main/java/org/umc/valuedi/domain/asset/service/query/AssetQueryService.java
index 19b810ae..fdd5a32d 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetQueryService.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/query/AssetQueryService.java
@@ -1,4 +1,4 @@
-package org.umc.valuedi.domain.asset.service;
+package org.umc.valuedi.domain.asset.service.query;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -9,8 +9,8 @@
import org.umc.valuedi.domain.asset.dto.res.CardResDTO;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.Card;
-import org.umc.valuedi.domain.asset.repository.bank.BankAccountRepository;
-import org.umc.valuedi.domain.asset.repository.card.CardRepository;
+import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
+import org.umc.valuedi.domain.asset.repository.card.card.CardRepository;
import java.util.List;
@@ -32,11 +32,11 @@ public BankResDTO.BankAccountListDTO getAllBankAccounts(Long memberId) {
}
/**
- * ์ํ๋ณ ์ฐ๋๋ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
+ * ์ํ๋ณ ์ฐ๋๋ ๊ณ์ข ๋ฐ ๋ชฉํ ๋ชฉ๋ก ์กฐํ
*/
- public BankResDTO.BankAccountListDTO getBankAccountsByOrganization(Long memberId, String organization) {
+ public BankResDTO.BankAssetResponse getBankAccountsByOrganization(Long memberId, String organization) {
List accounts = bankAccountRepository.findAllByMemberIdAndOrganization(memberId, organization);
- return AssetConverter.toBankAccountListDTO(accounts);
+ return AssetConverter.toBankAssetResponse(organization, accounts);
}
/**
diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
index 82032a02..e21d2f08 100644
--- a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
+++ b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
@@ -6,7 +6,7 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
-import org.umc.valuedi.domain.asset.service.AssetSyncService;
+import org.umc.valuedi.domain.asset.service.command.AssetSyncService;
import org.umc.valuedi.domain.connection.dto.event.ConnectionSuccessEvent;
@Slf4j
diff --git a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
index 86822407..d37e9933 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
@@ -19,7 +19,7 @@ private GoalConverter() {}
public static Goal toEntity(Member member, GoalCreateRequestDto req) {
return Goal.builder()
.member(member)
- .accountId(null) // ๊ณ์ข ์ฐ๋ ๋๋ฉด ์ฑ์ฐ๊ธฐ
+ .bankAccount(null) // ๊ณ์ข ์ฐ๋ ๋๋ฉด ์ฑ์ฐ๊ธฐ
.title(req.title())
.startDate(req.startDate())
.endDate(req.endDate())
@@ -66,7 +66,7 @@ public static GoalListResponseDto.GoalSummaryDto toSummaryDto(
remainingAmount,
remainingDays,
achievementRate,
- null, // ๊ณ์ข ์ฐ๋ ๋๋ฉด ์ฑ์ฐ๊ธฐ
+ goal.getBankAccount() != null ? goal.getBankAccount().getAccountName() : null,
goal.getStatus(),
goal.getColor(),
goal.getIcon()
@@ -80,6 +80,14 @@ public static GoalDetailResponseDto toDetailDto(
) {
long remainingDays = calcRemainingDays(goal.getEndDate());
+ GoalDetailResponseDto.AccountDto accountDto = null;
+ if (goal.getBankAccount() != null) {
+ accountDto = new GoalDetailResponseDto.AccountDto(
+ goal.getBankAccount().getCodefConnection().getOrganization(), // ์ํ๋ช
(๊ธฐ๊ด๋ช
)
+ goal.getBankAccount().getAccountDisplay() // ๊ณ์ข๋ฒํธ
+ );
+ }
+
return new GoalDetailResponseDto(
goal.getId(),
goal.getTitle(),
@@ -87,7 +95,7 @@ public static GoalDetailResponseDto toDetailDto(
goal.getTargetAmount(),
remainingDays,
achievementRate,
- null, // ๊ณ์ข ์ฐ๋ ๋๋ฉด ์ฑ์ฐ๊ธฐ
+ accountDto,
goal.getStatus(),
goal.getColor(),
goal.getIcon()
diff --git a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
index bf2d9626..916a2841 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
@@ -4,6 +4,7 @@
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.umc.valuedi.domain.member.entity.Member;
import org.umc.valuedi.global.entity.BaseEntity;
@@ -29,8 +30,9 @@ public class Goal extends BaseEntity {
@JoinColumn(name = "member_id", nullable = false)
private Member member;
- @Column(name = "id2", nullable = true)
- private Long accountId;
+ @OneToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "bank_account_id")
+ private BankAccount bankAccount;
@Column(name = "title", nullable = false, length = 20)
private String title;
@@ -97,4 +99,5 @@ public void Fail() {
this.completedAt = LocalDateTime.now();
}
+ public void setBankAccount(BankAccount bankAccount) { this.bankAccount = bankAccount;}
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
index eba06c00..6685ced0 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
@@ -8,7 +8,7 @@
import java.time.LocalDate;
import java.util.List;
-public interface GoalRepository extends JpaRepository {
+public interface GoalRepository extends JpaRepository, GoalRepositoryCustom {
List findAllByMember_Id( Long memberId);
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
new file mode 100644
index 00000000..0634b2b5
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
@@ -0,0 +1,9 @@
+package org.umc.valuedi.domain.goal.repository;
+
+import org.umc.valuedi.domain.goal.entity.Goal;
+
+import java.util.Optional;
+
+public interface GoalRepositoryCustom {
+ Optional findByIdWithDetails(Long goalId);
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
new file mode 100644
index 00000000..b5faa2cf
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
@@ -0,0 +1,29 @@
+package org.umc.valuedi.domain.goal.repository;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.umc.valuedi.domain.goal.entity.Goal;
+
+import java.util.Optional;
+
+import static org.umc.valuedi.domain.asset.entity.QBankAccount.bankAccount;
+import static org.umc.valuedi.domain.connection.entity.QCodefConnection.codefConnection;
+import static org.umc.valuedi.domain.goal.entity.QGoal.goal;
+
+@RequiredArgsConstructor
+public class GoalRepositoryImpl implements GoalRepositoryCustom {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public Optional findByIdWithDetails(Long goalId) {
+ Goal result = queryFactory
+ .selectFrom(goal)
+ .leftJoin(goal.bankAccount, bankAccount).fetchJoin()
+ .leftJoin(bankAccount.codefConnection, codefConnection).fetchJoin()
+ .where(goal.id.eq(goalId))
+ .fetchOne();
+
+ return Optional.ofNullable(result);
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
index 2b6ea6ed..64991c18 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
@@ -27,10 +27,14 @@ public class GoalQueryService {
// ๋ชฉํ ์์ธ ์กฐํ
public GoalDetailResponseDto getGoalDetail(Long goalId) {
- Goal goal = goalRepository.findById(goalId)
+ Goal goal = goalRepository.findByIdWithDetails(goalId)
.orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
long savedAmount = 0; // ๊ณ์ข ์ฐ๋ ํ ์์
+ if (goal.getBankAccount() != null && goal.getBankAccount().getBalanceAmount() != null) {
+ savedAmount = goal.getBankAccount().getBalanceAmount();
+ }
+
int rate = achievementRateService.calculateRate(savedAmount, goal.getTargetAmount());
return GoalConverter.toDetailDto(goal, savedAmount, rate);
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
index 4c35823a..de790c2a 100644
--- a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
+++ b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
@@ -9,8 +9,8 @@
import org.umc.valuedi.domain.asset.entity.CardApproval;
import org.umc.valuedi.domain.asset.enums.CancelStatus;
import org.umc.valuedi.domain.asset.enums.TransactionDirection;
-import org.umc.valuedi.domain.asset.repository.bank.BankTransactionRepository;
-import org.umc.valuedi.domain.asset.repository.card.CardApprovalRepository;
+import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
+import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
import org.umc.valuedi.domain.ledger.dto.request.LedgerSyncRequest;
import org.umc.valuedi.domain.ledger.entity.Category;
import org.umc.valuedi.domain.ledger.entity.CategoryKeyword;
From e5636415ef4267cd818928019fea99d161cebcb8 Mon Sep 17 00:00:00 2001
From: dohee <152317074+seamooll@users.noreply.github.com>
Date: Tue, 3 Feb 2026 09:15:26 +0900
Subject: [PATCH 2/4] =?UTF-8?q?[FEATURE/#68]=20=EC=9D=80=ED=96=89=20?=
=?UTF-8?q?=EA=B8=88=EC=9C=B5=EC=82=AC=20=EC=97=B0=EB=8F=99=20=EC=82=AD?=
=?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20=EC=97=B0=EA=B2=B0=EB=90=9C=20=EB=AA=A9?=
=?UTF-8?q?=ED=91=9C=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EB=B3=B4?=
=?UTF-8?q?=EC=99=84=20(#69)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ Related Issue
- Closes #68
## ๐ Summary
์ํ ๊ธ์ต์ฌ ์ฐ๋ ์ญ์ ์ ๊ณ์ข์ ์ฐ๊ฒฐ๋ ๋ชฉํ๋ ์ญ์ ๋๋๋ก ๋ณด์ํ์ต๋๋ค.
## ๐ Changes
- [X] API ๋ณ๊ฒฝ (์ถ๊ฐ/์์ )
- [ ] ๋ฐ์ดํฐ ๋ฐ ๋๋ฉ์ธ ๋ณ๊ฒฝ (DB, ๋น์ฆ๋์ค ๋ก์ง)
- [ ] ์ค์ ๋๋ ์ธํ๋ผ ๊ด๋ จ ๋ณ๊ฒฝ
- [ ] ๋ฆฌํฉํ ๋ง
## ๐ฌ Questions & Review Points
## ๐ธ API Test Results (Swagger)
## โ
Checklist
- [x] API ํ
์คํธ ์๋ฃ
- [ ] ํ
์คํธ ๊ฒฐ๊ณผ ์ฌ์ง ์ฒจ๋ถ
- [x] ๋น๋ ์ฑ๊ณต ํ์ธ (./gradlew build)
---
.../connection/service/ConnectionCommandService.java | 11 ++++++++---
.../org/umc/valuedi/domain/goal/entity/Goal.java | 1 -
.../domain/goal/repository/GoalRepository.java | 12 ++++++++++++
3 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionCommandService.java b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionCommandService.java
index f317da3a..07fb52e8 100644
--- a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionCommandService.java
+++ b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionCommandService.java
@@ -6,9 +6,11 @@
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.connection.dto.req.ConnectionReqDTO;
import org.umc.valuedi.domain.connection.entity.CodefConnection;
+import org.umc.valuedi.domain.connection.enums.BusinessType;
import org.umc.valuedi.domain.connection.exception.ConnectionException;
import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode;
import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository;
+import org.umc.valuedi.domain.goal.repository.GoalRepository;
import org.umc.valuedi.global.external.codef.service.CodefAccountService;
@Slf4j
@@ -19,6 +21,7 @@ public class ConnectionCommandService {
private final CodefAccountService codefAccountService;
private final CodefConnectionRepository codefConnectionRepository;
+ private final GoalRepository goalRepository;
/**
* ๊ธ์ต์ฌ ๊ณ์ ์ฐ๋
@@ -45,10 +48,12 @@ public void disconnect(Long memberId, Long connectionId) {
connection.getBusinessType()
);
- // connection Soft Delete (Cascade์ ์ํด ํ์ ๊ณ์ข/์นด๋๋ Soft Delete ๋จ)
+ //ํ์ ๊ณ์ข/์นด๋๋ Soft Delete
codefConnectionRepository.delete(connection);
- // TODO: [์ถํ ๊ตฌํ] ์ํ ๊ณ์ข์ ์ฐ๊ฒฐ๋ ๋ชฉํ(Goal) Soft Delete ์ฒ๋ฆฌ ๋ก์ง ์ถ๊ฐ ํ์
- // if (connection.getBusinessType() == BusinessType.BANK) { ... }
+ if (connection.getBusinessType() == BusinessType.BK) {
+ // ์ํ ๊ณ์ข์ ์ฐ๊ฒฐ๋ ๋ชฉํ Soft Delete ์ฒ๋ฆฌ (์๋ธ์ฟผ๋ฆฌ ์ฌ์ฉ)
+ goalRepository.softDeleteGoalsByConnectionId(connection.getId());
+ }
}
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
index 916a2841..ffb5b10f 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
@@ -99,5 +99,4 @@ public void Fail() {
this.completedAt = LocalDateTime.now();
}
- public void setBankAccount(BankAccount bankAccount) { this.bankAccount = bankAccount;}
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
index 6685ced0..5ccd72b4 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
@@ -1,6 +1,9 @@
package org.umc.valuedi.domain.goal.repository;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.springframework.data.domain.Pageable;
@@ -21,4 +24,13 @@ public interface GoalRepository extends JpaRepository, GoalRepositor
List findAllByMember_IdAndStatus(Long memberId, GoalStatus status, Pageable pageable);
List findAllByMember_IdAndStatusIn(Long memberId, List statuses, Pageable pageable);
+ List findAllByBankAccountId(Long bankAccountId);
+
+ @Modifying(clearAutomatically = true)
+ @Query("UPDATE Goal g SET g.deletedAt = CURRENT_TIMESTAMP " +
+ "WHERE g.bankAccount.id IN (" +
+ " SELECT ba.id FROM BankAccount ba " +
+ " WHERE ba.codefConnection.id = :connectionId" +
+ ")")
+ void softDeleteGoalsByConnectionId(@Param("connectionId") Long connectionId);
}
From fc7ee8653de9f7a868dfeb364cdc03fbb8c7c9f3 Mon Sep 17 00:00:00 2001
From: Lee GaEun <137785327+kkeunii@users.noreply.github.com>
Date: Tue, 3 Feb 2026 21:35:18 +0900
Subject: [PATCH 3/4] =?UTF-8?q?[FEATURE/#56]=20=EB=AA=A9=ED=91=9C=20?=
=?UTF-8?q?=EB=8B=B9=20=EA=B3=84=EC=A2=8C=20=EC=97=B0=EA=B2=B0=20api=20?=
=?UTF-8?q?=EA=B5=AC=ED=98=84=20(#57)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ Related Issue
- Closes #56
## ๐ Summary
### Goal ์์ฑ ์ ๊ณ์ข ์ฐ๋ ๋ฐ ๊ณ์ข ๋ณ๊ฒฝ API ์ถ๊ฐ
- ๋ชฉํ(Goal) ์์ฑ ์ ๊ณ์ข(account) ํ์ ์ฐ๋ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ
- Goal โ BankAccount 1:1 ๊ด๊ณ ์ ์ฉ
- ํ์ฌ ๊ณ์ข๋ค ์ค ์ฐ๊ฒฐ ๋ ๋ชฉํ๊ฐ ์๋ ๊ณ์ข ์กฐํํ๋ API ์ถ๊ฐ
- ๋ชฉํ์ ์ฐ๊ฒฐ๋ ๊ณ์ข๋ฅผ PUT ๋ฐฉ์์ผ๋ก ์ค์ /๊ต์ฒดํ ์ ์๋ API ์ถ๊ฐ
- Goal ์์ธ/์์ฑ ์๋ต์ ์ฐ๊ฒฐ๋ ๊ณ์ข ์ ๋ณด ํฌํจ
### Primary ๋ชฉํ ์กฐํ API ์ถ๊ฐ
- ํํ๋ฉด์์ ์งํ ์ค(ACTIVE) ๋ชฉํ๋ฅผ ์ต์ ์์ผ๋ก ์ ์ฒด ์กฐํํ๋ /api/goals/primary API ์ถ๊ฐ
## ๐ Changes
- [x] API ๋ณ๊ฒฝ (์ถ๊ฐ/์์ )
- [ ] ๋ฐ์ดํฐ ๋ฐ ๋๋ฉ์ธ ๋ณ๊ฒฝ (DB, ๋น์ฆ๋์ค ๋ก์ง)
- [ ] ์ค์ ๋๋ ์ธํ๋ผ ๊ด๋ จ ๋ณ๊ฒฝ
- [ ] ๋ฆฌํฉํ ๋ง
## ๐ฌ Questions & Review Points
์ผ๋จ ์์ง ๊ฑฐ๋ ๋ด์ญ ๋ถ๋ฌ์ค๋ api ์ ์ธ ๊ณ์ข ์ฐ๋ ๊ด๋ จ api ๋ชจ๋ ์์ฑํ์ต๋๋ค.
๊ณ์ข๋ ๋ชฉํ ์์ฑ ์์ ํ์๋ก id๋ฅผ ์
๋ ฅํ๊ฒ ๋ณ๊ฒฝํ๊ณ , ๊ณ์ข ์ฐ๋์ด ํด์ง๋ ๋ ๋ค๋ฅธ ๊ณ์ข๋ก ๊ณ์ข๋ง ์ฌ์ฐ๊ฒฐ ํด์ผํ๋๊ฑธ ๋๋น ํด
/api/goals/{goalId}/linked-accounts ๋ฅผ put์ผ๋ก ๊ตฌํํ์ต๋๋ค!
## ๐ธ API Test Results (Swagger)
### ๋ชฉํ ์์ฑ ์ ์ ํํ ๊ณ์ข๊ฐ ์ด๋ฏธ ๋ชฉํ๋ ์ฐ๋ ๋ ๊ณ์ข์ผ ๋
### ๋ชฉํ ์์ฑ ์
### /api/goals/accounts -> ๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
### /api/goals/{goalId}/ledgers -> ๋ชฉํ ๋ณ ๊ฑฐ๋๋ด์ญ ์กฐํ
### /api/goals/primary -> ํํ๋ฉด ๋ชฉํ ๋ชฉ๋ก ์กฐํ
## โ
Checklist
- [x] API ํ
์คํธ ์๋ฃ
- [x] ํ
์คํธ ๊ฒฐ๊ณผ ์ฌ์ง ์ฒจ๋ถ
- [x] ๋น๋ ์ฑ๊ณต ํ์ธ (./gradlew build)
---------
Co-authored-by: dohee <152317074+seamooll@users.noreply.github.com>
---
.../BankAccountRepositoryCustom.java | 7 +
.../BankAccountRepositoryImpl.java | 39 +++++
.../goal/controller/GoalController.java | 82 +++++++++--
.../goal/controller/GoalControllerDocs.java | 133 ++++++++++++++----
.../goal/converter/GoalAccountConverter.java | 21 +++
.../domain/goal/converter/GoalConverter.java | 79 ++++++++---
.../dto/request/GoalCreateRequestDto.java | 9 +-
.../request/GoalLinkAccountRequestDto.java | 12 ++
.../dto/request/GoalUpdateRequestDto.java | 2 +-
.../goal/dto/response/GoalAccountResDto.java | 29 ++++
.../dto/response/GoalCreateResponseDto.java | 46 +++++-
.../dto/response/GoalDetailResponseDto.java | 28 ++++
.../dto/response/GoalListResponseDto.java | 26 +++-
.../response/GoalPrimaryListResponseDto.java | 30 ++++
.../umc/valuedi/domain/goal/entity/Goal.java | 16 ++-
.../goal/exception/code/GoalErrorCode.java | 9 +-
.../goal/exception/code/GoalSuccessCode.java | 5 +-
.../goal/repository/GoalRepository.java | 9 +-
.../goal/repository/GoalRepositoryCustom.java | 8 ++
.../goal/repository/GoalRepositoryImpl.java | 39 +++++
.../command/GoalAccountCommandService.java | 47 +++++++
.../service/command/GoalCommandService.java | 34 +++--
.../query/GoalAccountQueryService.java | 32 +++++
.../service/query/GoalLedgerQueryService.java | 54 +++++++
.../service/query/GoalListQueryService.java | 19 ++-
.../goal/service/query/GoalQueryService.java | 25 +++-
.../repository/LedgerQueryRepository.java | 38 +++++
stop | 1 -
28 files changed, 783 insertions(+), 96 deletions(-)
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/converter/GoalAccountConverter.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalLinkAccountRequestDto.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalAccountResDto.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalPrimaryListResponseDto.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/service/command/GoalAccountCommandService.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/service/query/GoalAccountQueryService.java
create mode 100644 src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java
delete mode 100644 stop
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
index 35ad42d3..d5a699be 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryCustom.java
@@ -3,6 +3,7 @@
import org.umc.valuedi.domain.asset.entity.BankAccount;
import java.util.List;
+import java.util.Optional;
public interface BankAccountRepositoryCustom {
// ํน์ ์ํ๋ณ ํ์ฑ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
@@ -13,4 +14,10 @@ public interface BankAccountRepositoryCustom {
// ์ด ํ์ฑ ๊ณ์ข ์ ์นด์ดํธ
long countByMemberId(Long memberId);
+
+ //๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก ์กฐํ
+ List findUnlinkedByMemberId(Long memberId, List excludeIds);
+
+ //ํน์ ๊ณ์ข(accountId)๊ฐ ํด๋น ํ์(memberId)์ ์์ ์ด๋ฉฐ ํ์ฑ ์ํ์ธ์ง ์กฐํ
+ Optional findByIdAndMemberId(Long accountId, Long memberId);
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
index 51cd559d..9e57b2e8 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankAccount/BankAccountRepositoryImpl.java
@@ -5,6 +5,7 @@
import org.umc.valuedi.domain.asset.entity.BankAccount;
import java.util.List;
+import java.util.Optional;
import static org.umc.valuedi.domain.asset.entity.QBankAccount.bankAccount;
import static org.umc.valuedi.domain.connection.entity.QCodefConnection.codefConnection;
@@ -56,4 +57,42 @@ public long countByMemberId(Long memberId) {
.fetchOne();
return count != null ? count : 0;
}
+
+ @Override
+ public List findUnlinkedByMemberId(Long memberId, List excludeIds) {
+ boolean hasExcludeIds = (excludeIds != null && !excludeIds.isEmpty());
+
+ var base = queryFactory
+ .selectFrom(bankAccount)
+ .join(bankAccount.codefConnection, codefConnection).fetchJoin()
+ .leftJoin(bankAccount.goal, goal).fetchJoin()
+ .where(
+ codefConnection.member.id.eq(memberId),
+ bankAccount.isActive.isTrue()
+ );
+
+ if (hasExcludeIds) {
+ base.where(bankAccount.id.notIn(excludeIds));
+ }
+
+ return base
+ .orderBy(bankAccount.id.desc())
+ .fetch();
+ }
+
+ @Override
+ public Optional findByIdAndMemberId(Long accountId, Long memberId) {
+ BankAccount result = queryFactory
+ .selectFrom(bankAccount)
+ .join(bankAccount.codefConnection, codefConnection).fetchJoin()
+ .leftJoin(bankAccount.goal, goal).fetchJoin()
+ .where(
+ bankAccount.id.eq(accountId),
+ codefConnection.member.id.eq(memberId),
+ bankAccount.isActive.isTrue()
+ )
+ .fetchOne();
+
+ return Optional.ofNullable(result);
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java
index c29b0505..685a18db 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java
@@ -5,18 +5,21 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.umc.valuedi.domain.goal.dto.request.GoalCreateRequestDto;
+import org.umc.valuedi.domain.goal.dto.request.GoalLinkAccountRequestDto;
import org.umc.valuedi.domain.goal.dto.request.GoalUpdateRequestDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalCreateResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalDetailResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalListResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalActiveCountResponseDto;
+import org.umc.valuedi.domain.goal.dto.response.*;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.umc.valuedi.domain.goal.enums.GoalSort;
import org.umc.valuedi.domain.goal.exception.code.GoalSuccessCode;
+import org.umc.valuedi.domain.goal.service.command.GoalAccountCommandService;
import org.umc.valuedi.domain.goal.service.command.GoalCommandService;
+import org.umc.valuedi.domain.goal.service.query.GoalAccountQueryService;
+import org.umc.valuedi.domain.goal.service.query.GoalLedgerQueryService;
import org.umc.valuedi.domain.goal.service.query.GoalListQueryService;
import org.umc.valuedi.domain.goal.service.query.GoalQueryService;
+import org.umc.valuedi.domain.ledger.dto.response.LedgerListResponse;
import org.umc.valuedi.global.apiPayload.ApiResponse;
+import org.umc.valuedi.global.security.annotation.CurrentMember;
@RestController
@RequiredArgsConstructor
@@ -26,23 +29,27 @@ public class GoalController implements GoalControllerDocs{
private final GoalCommandService goalService;
private final GoalQueryService goalQueryService;
private final GoalListQueryService goalListQueryService;
+ private final GoalAccountQueryService goalAccountQueryService;
+ private final GoalAccountCommandService goalAccountCommandService;
+ private final GoalLedgerQueryService goalLedgerQueryService;
// ๋ชฉํ ์ถ๊ฐ
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse createGoal(
+ @CurrentMember Long memberId,
@RequestBody @Valid GoalCreateRequestDto req
) {
return ApiResponse.onSuccess(
GoalSuccessCode.GOAL_CREATED,
- goalService.createGoal(req)
+ goalService.createGoal(memberId, req)
);
}
// ์ ์ฒด ๋ชฉํ ์กฐํ (์งํ/์๋ฃ/์ทจ์ ๋ถ๋ฆฌ)
@GetMapping
public ApiResponse getGoals(
- @RequestParam Long memberId,
+ @CurrentMember Long memberId,
@RequestParam(defaultValue = "ACTIVE") GoalStatus status,
@RequestParam(defaultValue = "TIME_DESC") GoalSort sort,
@RequestParam(required = false) Integer limit
@@ -56,21 +63,23 @@ public ApiResponse getGoals(
// ๋ชฉํ ์์ธ ์กฐํ
@GetMapping("/{goalId}")
public ApiResponse getGoalDetail(
+ @CurrentMember Long memberId,
@PathVariable Long goalId
) {
return ApiResponse.onSuccess(
GoalSuccessCode.GOAL_DETAIL_FETCHED,
- goalQueryService.getGoalDetail(goalId)
+ goalQueryService.getGoalDetail(memberId, goalId)
);
}
// ๋ชฉํ ์์
@PatchMapping("/{goalId}")
public ApiResponse updateGoal(
+ @CurrentMember Long memberId,
@PathVariable Long goalId,
@RequestBody @Valid GoalUpdateRequestDto req
) {
- goalService.updateGoal(goalId, req);
+ goalService.updateGoal(memberId, goalId, req);
return ApiResponse.onSuccess(
GoalSuccessCode.GOAL_UPDATED,
null
@@ -80,9 +89,10 @@ public ApiResponse updateGoal(
// ๋ชฉํ ์ญ์
@DeleteMapping("/{goalId}")
public ApiResponse deleteGoal(
+ @CurrentMember Long memberId,
@PathVariable Long goalId
) {
- goalService.deleteGoal(goalId);
+ goalService.deleteGoal(memberId, goalId);
return ApiResponse.onSuccess(
GoalSuccessCode.GOAL_DELETED,
null
@@ -92,11 +102,63 @@ public ApiResponse deleteGoal(
// ๋ชฉํ ๊ฐ์ ์กฐํ
@GetMapping("/count")
public ApiResponse getActiveGoalCount(
- @RequestParam Long memberId
+ @CurrentMember Long memberId
) {
return ApiResponse.onSuccess(
GoalSuccessCode.GOAL_ACTIVE_COUNT_FETCHED,
goalQueryService.getActiveGoalCount(memberId)
);
}
+
+
+ // ๋ชฉํ ์๋ ๊ณ์ข ์กฐํ
+ @GetMapping("/accounts")
+ public ApiResponse getUnlinkedAccounts(
+ @CurrentMember Long memberId
+
+ ) {
+ return ApiResponse.onSuccess(
+ GoalSuccessCode.GOAL_UNLINKED_ACCOUNTS_FETCHED,
+ goalAccountQueryService.getUnlinkedAccounts(memberId)
+ );
+ }
+
+ // ๋ชฉํ์ ๊ณ์ข ์ฌ์ฐ๊ฒฐ
+ @PutMapping("/{goalId}/linked-accounts")
+ public ApiResponse linkAccountToGoal(
+ @CurrentMember Long memberId,
+ @PathVariable Long goalId,
+ @RequestBody @Valid GoalLinkAccountRequestDto req
+ ) {
+ goalAccountCommandService.setLinkedAccount(memberId, goalId, req.accountId());
+ return ApiResponse.onSuccess(
+ GoalSuccessCode.GOAL_ACCOUNT_LINKED,
+ null
+ );
+ }
+
+ // ์ฃผ์ ๋ชฉํ ์กฐํ(ํํ๋ฉด)
+ @GetMapping("/primary")
+ public ApiResponse getPrimaryGoals(
+ @CurrentMember Long memberId
+ ) {
+ return ApiResponse.onSuccess(
+ GoalSuccessCode.GOAL_LIST_FETCHED,
+ goalQueryService.getPrimaryGoals(memberId)
+ );
+ }
+
+ //๋ชฉํ ๊ฑฐ๋๋ด์ญ ์กฐํ
+ @GetMapping("/{goalId}/ledgers")
+ public ApiResponse getGoalLedgers(
+ @CurrentMember Long memberId,
+ @PathVariable Long goalId,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "20") int size
+ ) {
+ return ApiResponse.onSuccess(
+ GoalSuccessCode.GOAL_LEDGER_LIST_FETCHED,
+ goalLedgerQueryService.getGoalLedgerTransactions(memberId, goalId, page, size)
+ );
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalControllerDocs.java b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalControllerDocs.java
index 810da999..b11b7445 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalControllerDocs.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalControllerDocs.java
@@ -6,62 +6,62 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.umc.valuedi.domain.goal.dto.request.GoalCreateRequestDto;
+import org.umc.valuedi.domain.goal.dto.request.GoalLinkAccountRequestDto;
import org.umc.valuedi.domain.goal.dto.request.GoalUpdateRequestDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalActiveCountResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalCreateResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalDetailResponseDto;
-import org.umc.valuedi.domain.goal.dto.response.GoalListResponseDto;
+import org.umc.valuedi.domain.goal.dto.response.*;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.umc.valuedi.domain.goal.enums.GoalSort;
+import org.umc.valuedi.domain.ledger.dto.response.LedgerListResponse;
+import org.umc.valuedi.global.security.annotation.CurrentMember;
@Tag(name = "Goal", description = "๋ชฉํ(Goal) ์์ฑ/์กฐํ/์์ /์ญ์ API")
public interface GoalControllerDocs {
@Operation(
summary = "๋ชฉํ ์ถ๊ฐ API",
- description = "๋ชฉํ ์ด๋ฆ, ์์์ผ, ์ข
๋ฃ์ผ, ๋ชฉํ ๊ธ์ก์ ์
๋ ฅ๋ฐ์ ๋ชฉํ๋ฅผ ์์ฑํฉ๋๋ค. (๊ณ์ข ์ฐ๋์ ์ถํ ์ถ๊ฐ)"
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ๋ชฉํ๋ฅผ ์์ฑํฉ๋๋ค."
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "์์ฑ ์ฑ๊ณต"),
@ApiResponse(responseCode = "400", description = "๊ฒ์ฆ ์คํจ (๋ ์ง ๋ฒ์ ์ค๋ฅ, targetAmount ๋ฒ์ ์ค๋ฅ ๋ฑ)"),
- @ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "404", description = "ํ์ ๋๋ ๊ณ์ข๊ฐ ์กด์ฌํ์ง ์์"),
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse createGoal(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
@Valid @RequestBody GoalCreateRequestDto req
);
@Operation(
summary = "๋ชฉํ ๋ชฉ๋ก ์กฐํ API",
- description = "์ฌ์ฉ์์ ์ ์ฒด ๋ชฉํ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค."
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๋ชฉํ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
- @ApiResponse(responseCode = "400", description = "status ํ๋ผ๋ฏธํฐ ํ์
์ค๋ฅ"),
+ @ApiResponse(responseCode = "400", description = "status/sort ํ๋ผ๋ฏธํฐ ์ค๋ฅ"),
@ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse getGoals(
- @Parameter(description = "ํ์ ID", example = "1", required = true)
- @RequestParam Long memberId,
-
- @Parameter(description = "๋ชฉํ ์ํ", example = "ACTIVE", required = true)
- @RequestParam GoalStatus status,
+ @Parameter(hidden = true) @CurrentMember Long memberId,
- @Parameter(description = "๋ชฉํ ์ ๋ ฌ", example = "TIME_DESC", required = true)
- @RequestParam GoalSort sort,
+ @Parameter(description = "๋ชฉํ ์ํ", example = "ACTIVE")
+ @RequestParam(defaultValue = "ACTIVE") GoalStatus status,
- @Parameter(description = "ํ์ํ ๋ชฉํ ์", example = "3", required = true)
- @RequestParam Integer limit
+ @Parameter(description = "๋ชฉํ ์ ๋ ฌ", example = "TIME_DESC")
+ @RequestParam(defaultValue = "TIME_DESC") GoalSort sort,
+ @Parameter(description = "ํ์ํ ๋ชฉํ ์(์์ผ๋ฉด ์ ์ฒด)", example = "3")
+ @RequestParam(required = false) Integer limit
);
@Operation(
summary = "๋ชฉํ ์์ธ ์กฐํ API",
- description = "goalId์ ํด๋นํ๋ ๋ชฉํ์ ์์ธ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค."
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํน์ ๋ชฉํ(goalId)์ ๋ํ ์์ธ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
@@ -69,13 +69,15 @@ org.umc.valuedi.global.apiPayload.ApiResponse getGoals(
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse getGoalDetail(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
+
@Parameter(description = "๋ชฉํ ID", example = "10", required = true)
- Long goalId
+ @PathVariable Long goalId
);
@Operation(
summary = "๋ชฉํ ์์ API",
- description = "goalId์ ํด๋นํ๋ ๋ชฉํ์ ์ ๋ชฉ/๊ธฐ๊ฐ/๊ธ์ก์ ์์ ํฉ๋๋ค."
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํน์ ๋ชฉํ(goalId)์ ์ ๋ชฉ/๊ธฐ๊ฐ/๊ธ์ก ๋ฑ์ ์์ ํฉ๋๋ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์์ ์ฑ๊ณต"),
@@ -84,15 +86,17 @@ org.umc.valuedi.global.apiPayload.ApiResponse getGoalDeta
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse updateGoal(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
+
@Parameter(description = "๋ชฉํ ID", example = "10", required = true)
- Long goalId,
+ @PathVariable Long goalId,
@Valid @RequestBody GoalUpdateRequestDto req
);
@Operation(
summary = "๋ชฉํ ์ญ์ API",
- description = "goalId์ ํด๋นํ๋ ๋ชฉํ๋ฅผ ์ทจ์ ์ฒ๋ฆฌํฉ๋๋ค."
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํน์ ๋ชฉํ(goalId)๋ฅผ ์ญ์ (soft delete) ์ฒ๋ฆฌํฉ๋๋ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์ญ์ ์ฑ๊ณต"),
@@ -100,21 +104,90 @@ org.umc.valuedi.global.apiPayload.ApiResponse updateGoal(
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse deleteGoal(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
+
@Parameter(description = "๋ชฉํ ID", example = "10", required = true)
- Long goalId
+ @PathVariable Long goalId
);
@Operation(
summary = "์งํ ์ค์ธ ๋ชฉํ ๊ฐ์ ์กฐํ API",
- description = "์ฌ์ฉ์์ ํ์ฌ ์งํ ์ค(ACTIVE) ๋ชฉํ์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค."
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํ์ฌ ์งํ ์ค(ACTIVE) ๋ชฉํ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค."
)
- @io.swagger.v3.oas.annotations.responses.ApiResponses({
- @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
- @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
- @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
+ @ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
org.umc.valuedi.global.apiPayload.ApiResponse getActiveGoalCount(
- @Parameter(description = "ํ์ ID", example = "1", required = true)
- @RequestParam Long memberId
+ @Parameter(hidden = true) @CurrentMember Long memberId
+ );
+
+ @Operation(
+ summary = "๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก ์กฐํ API",
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ณ์ข ์ค ์์ง ์ด๋ค ๋ชฉํ์๋ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค."
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
+ @ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
+ })
+ org.umc.valuedi.global.apiPayload.ApiResponse getUnlinkedAccounts(
+ @Parameter(hidden = true) @CurrentMember Long memberId
+ );
+
+ @Operation(
+ summary = "๋ชฉํ-๊ณ์ข ์ฐ๊ฒฐ API",
+ description = "ํน์ ๋ชฉํ(goalId)์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ณ์ข(accountId)๋ฅผ 1:1๋ก ์ฐ๊ฒฐํฉ๋๋ค."
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "์ฐ๊ฒฐ ์ฑ๊ณต"),
+ @ApiResponse(responseCode = "400", description = "์ด๋ฏธ ์ฐ๊ฒฐ๋ ๋ชฉํ/๊ณ์ข ๋ฑ ์์ฒญ ์ค๋ฅ"),
+ @ApiResponse(responseCode = "404", description = "๋ชฉํ ๋๋ ๊ณ์ข๊ฐ ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
+ })
+ org.umc.valuedi.global.apiPayload.ApiResponse linkAccountToGoal(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
+
+ @Parameter(description = "๋ชฉํ ID", example = "10", required = true)
+ @PathVariable Long goalId,
+
+ @Valid @RequestBody GoalLinkAccountRequestDto req
+ );
+
+ @Operation(
+ summary = "ํํ๋ฉด ๋ชฉํ ๋ชฉ๋ก ์กฐํ API",
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์งํ ์ค(ACTIVE) ๋ชฉํ๋ฅผ ์์ฑ์ผ ์ต์ ์์ผ๋ก ์กฐํํฉ๋๋ค."
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
+ @ApiResponse(responseCode = "404", description = "ํ์์ด ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
+ })
+ org.umc.valuedi.global.apiPayload.ApiResponse getPrimaryGoals(
+ @Parameter(hidden = true) @CurrentMember Long memberId
+ );
+
+ @Operation(
+ summary = "๋ชฉํ ๊ฑฐ๋๋ด์ญ ์กฐํ API",
+ description = "๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ํน์ ๋ชฉํ(goalId)์ ์ฐ๊ฒฐ๋ ๊ณ์ข์ ๊ฑฐ๋๋ด์ญ์ ํ์ด์ง๋ค์ด์
์ผ๋ก ์กฐํํฉ๋๋ค."
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "์กฐํ ์ฑ๊ณต"),
+ @ApiResponse(responseCode = "400", description = "page/size ํ๋ผ๋ฏธํฐ ์ค๋ฅ"),
+ @ApiResponse(responseCode = "404", description = "๋ชฉํ๊ฐ ์กด์ฌํ์ง ์์"),
+ @ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
+ })
+ org.umc.valuedi.global.apiPayload.ApiResponse getGoalLedgers(
+ @Parameter(hidden = true) @CurrentMember Long memberId,
+
+ @Parameter(description = "๋ชฉํ ID", example = "10", required = true)
+ @PathVariable Long goalId,
+
+ @Parameter(description = "ํ์ด์ง ๋ฒํธ(0๋ถํฐ ์์)", example = "0")
+ @RequestParam(defaultValue = "0") int page,
+
+ @Parameter(description = "ํ์ด์ง ํฌ๊ธฐ", example = "20")
+ @RequestParam(defaultValue = "20") int size
);
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalAccountConverter.java b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalAccountConverter.java
new file mode 100644
index 00000000..52cd27bf
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalAccountConverter.java
@@ -0,0 +1,21 @@
+package org.umc.valuedi.domain.goal.converter;
+
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.goal.dto.response.GoalAccountResDto;
+
+import java.util.List;
+
+public class GoalAccountConverter {
+
+ public static GoalAccountResDto.UnlinkedBankAccountListDTO toUnlinkedListDTO(List accounts) {
+ List items = accounts.stream()
+ .map(a -> new GoalAccountResDto.UnlinkedBankAccountDTO(
+ a.getId(),
+ a.getAccountName(),
+ a.getAccountDisplay()
+ ))
+ .toList();
+
+ return new GoalAccountResDto.UnlinkedBankAccountListDTO(items);
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
index d37e9933..dd3791e2 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java
@@ -1,29 +1,32 @@
package org.umc.valuedi.domain.goal.converter;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.goal.constant.GoalStyleCatalog;
import org.umc.valuedi.domain.goal.dto.request.GoalCreateRequestDto;
import org.umc.valuedi.domain.goal.dto.request.GoalUpdateRequestDto;
+import org.umc.valuedi.domain.goal.dto.response.GoalCreateResponseDto;
import org.umc.valuedi.domain.goal.dto.response.GoalDetailResponseDto;
import org.umc.valuedi.domain.goal.dto.response.GoalListResponseDto;
+import org.umc.valuedi.domain.goal.dto.response.GoalPrimaryListResponseDto;
import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.umc.valuedi.domain.member.entity.Member;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
+import java.util.List;
public class GoalConverter {
- private GoalConverter() {}
-
- public static Goal toEntity(Member member, GoalCreateRequestDto req) {
+ public static Goal toEntity(Member member,BankAccount bankAccount, GoalCreateRequestDto req, Long startAmount) {
return Goal.builder()
.member(member)
- .bankAccount(null) // ๊ณ์ข ์ฐ๋ ๋๋ฉด ์ฑ์ฐ๊ธฐ
+ .bankAccount(bankAccount)
.title(req.title())
.startDate(req.startDate())
.endDate(req.endDate())
.targetAmount(req.targetAmount())
+ .startAmount(startAmount)
.status(GoalStatus.ACTIVE)
.completedAt(null)
.color(GoalStyleCatalog.normalizeColor(req.colorCode()))
@@ -52,40 +55,68 @@ public static void applyPatch(Goal goal, GoalUpdateRequestDto req) {
goal.changeIcon(req.iconId());
}
+ private record AccountInfo(String bankName, String accountNumber) {}
+
+ private static AccountInfo extractAccountInfo(BankAccount bankAccount) {
+ if (bankAccount == null) return null;
+
+ String bankName = null;
+ if (bankAccount.getCodefConnection() != null) {
+ bankName = bankAccount.getCodefConnection().getOrganization();
+ }
+ String accountNumber = bankAccount.getAccountDisplay();
+ return new AccountInfo(bankName, accountNumber);
+ }
+
+
+ public static GoalCreateResponseDto toCreateDto(Goal goal) {
+ long remainingDays = calcRemainingDays(goal.getEndDate());
+
+ GoalCreateResponseDto.AccountDto accountDto = null;
+ var info = extractAccountInfo(goal.getBankAccount());
+ if (info != null) {
+ accountDto = new GoalCreateResponseDto.AccountDto(info.bankName(), info.accountNumber());
+ }
+
+ return new GoalCreateResponseDto(
+ goal.getId(),
+ goal.getTitle(),
+ goal.getTargetAmount(),
+ goal.getStartAmount(),
+ goal.getStartDate(),
+ goal.getEndDate(),
+ remainingDays,
+ accountDto,
+ goal.getIcon()
+ );
+ }
+
public static GoalListResponseDto.GoalSummaryDto toSummaryDto(
Goal goal,
Long savedAmount,
int achievementRate
) {
- Long remainingAmount = Math.max(goal.getTargetAmount() - savedAmount, 0);
Long remainingDays = calcRemainingDays(goal.getEndDate());
return new GoalListResponseDto.GoalSummaryDto(
goal.getId(),
goal.getTitle(),
- remainingAmount,
+ savedAmount,
remainingDays,
achievementRate,
- goal.getBankAccount() != null ? goal.getBankAccount().getAccountName() : null,
goal.getStatus(),
goal.getColor(),
goal.getIcon()
);
}
- public static GoalDetailResponseDto toDetailDto(
- Goal goal,
- Long savedAmount,
- int achievementRate
- ) {
+ public static GoalDetailResponseDto toDetailDto(Goal goal, Long savedAmount, int achievementRate) {
long remainingDays = calcRemainingDays(goal.getEndDate());
GoalDetailResponseDto.AccountDto accountDto = null;
- if (goal.getBankAccount() != null) {
- accountDto = new GoalDetailResponseDto.AccountDto(
- goal.getBankAccount().getCodefConnection().getOrganization(), // ์ํ๋ช
(๊ธฐ๊ด๋ช
)
- goal.getBankAccount().getAccountDisplay() // ๊ณ์ข๋ฒํธ
- );
+ var info = extractAccountInfo(goal.getBankAccount());
+ if (info != null) {
+ accountDto = new GoalDetailResponseDto.AccountDto(info.bankName(), info.accountNumber());
}
return new GoalDetailResponseDto(
@@ -107,4 +138,18 @@ private static long calcRemainingDays(LocalDate endDate) {
long days = ChronoUnit.DAYS.between(today, endDate);
return Math.max(days, 0);
}
+
+ public static GoalPrimaryListResponseDto toPrimaryListResponse(List goals) {
+ return new GoalPrimaryListResponseDto(
+ goals.stream()
+ .map(goal -> new GoalPrimaryListResponseDto.GoalPrimarySummaryDto(
+ goal.getId(), // goalId
+ goal.getTitle(),
+ goal.getTargetAmount(),
+ goal.getIcon()
+ ))
+ .toList()
+ );
+ }
+
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalCreateRequestDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalCreateRequestDto.java
index 5706f743..2083669d 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalCreateRequestDto.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalCreateRequestDto.java
@@ -11,8 +11,9 @@
@Schema(description = "๋ชฉํ ์ถ๊ฐ")
public record GoalCreateRequestDto(
- @Schema(description = "ํ์ ID", example = "1")
- @NotNull Long memberId,
+ @Schema(description = "์ฐ๊ฒฐํ ๊ณ์ข ID", example = "1")
+ @NotNull
+ Long bankAccountId,
@Schema(description = "๋ชฉํ ์ด๋ฆ(์ต๋ 12์)", example = "์ ๋ฝ ์ฌํ ์๊ธ")
@NotNull
@@ -26,10 +27,12 @@ public record GoalCreateRequestDto(
@Schema(description = "๋ชฉํ ์ข
๋ฃ์ผ (YYYY-MM-DD)", example = "2026-08-31")
@NotNull LocalDate endDate,
+
@Schema(description = "๋ชฉํ ๊ธ์ก(์ ๋จ์, 1 ์ด์)", example = "3000000", minimum = "1")
@NotNull(message = "targetAmount๋ ํ์์
๋๋ค.")
@Min(value = 1, message = "targetAmount๋ 1 ์ด์์ด์ด์ผ ํฉ๋๋ค.") Long targetAmount,
+
@Schema(description = "์์ ์ฝ๋(HEX)", example = "FF6363")
@NotBlank(message = "colorCode๋ ํ์์
๋๋ค.")
String colorCode,
@@ -38,4 +41,4 @@ public record GoalCreateRequestDto(
@NotNull(message = "iconId๋ ํ์์
๋๋ค.")
Integer iconId
-) {}
+) {}
\ No newline at end of file
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalLinkAccountRequestDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalLinkAccountRequestDto.java
new file mode 100644
index 00000000..6340274d
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalLinkAccountRequestDto.java
@@ -0,0 +1,12 @@
+package org.umc.valuedi.domain.goal.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+@Schema(description = "๋ชฉํ-๊ณ์ข ์ฐ๊ฒฐ ์์ฒญ")
+public record GoalLinkAccountRequestDto(
+
+ @Schema(description = "์ฐ๊ฒฐํ ๊ณ์ข ID", example = "12")
+ @NotNull(message = "accountId๋ ํ์์
๋๋ค.")
+ Long accountId
+) {}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalUpdateRequestDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalUpdateRequestDto.java
index e1416a2e..d7cad906 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalUpdateRequestDto.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/request/GoalUpdateRequestDto.java
@@ -35,4 +35,4 @@ public record GoalUpdateRequestDto(
@NotNull(message = "iconId๋ ํ์์
๋๋ค.")
Integer iconId
-) {}
+) {}
\ No newline at end of file
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalAccountResDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalAccountResDto.java
new file mode 100644
index 00000000..f526b16a
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalAccountResDto.java
@@ -0,0 +1,29 @@
+package org.umc.valuedi.domain.goal.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+
+@Schema(description = "๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก ์กฐํ ์๋ต")
+public class GoalAccountResDto {
+
+ @Schema(description = "๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๋จ์ผ ๊ณ์ข ์ ๋ณด")
+ public record UnlinkedBankAccountDTO(
+
+ @Schema(description = "๊ณ์ข ID", example = "12")
+ Long accountId,
+
+ @Schema(description = "๊ณ์ข๋ช
", example = "์
์ถ๊ธํต์ฅ")
+ String accountName,
+
+ @Schema(description = "ํ์์ฉ ๊ณ์ข๋ฒํธ(๋ง์คํน ํฌํจ ๊ฐ๋ฅ)", example = "110-****-1234")
+ String accountDisplay
+ ) {}
+
+ @Schema(description = "๋ชฉํ์ ์ฐ๊ฒฐ๋์ง ์์ ๊ณ์ข ๋ชฉ๋ก")
+ public record UnlinkedBankAccountListDTO(
+
+ @Schema(description = "๊ณ์ข ๋ชฉ๋ก")
+ List accounts
+ ) {}
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java
index 2b529053..d25040b6 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java
@@ -1,5 +1,47 @@
package org.umc.valuedi.domain.goal.dto.response;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDate;
+
+@Schema(description = "๋ชฉํ ์์ฑ ์๋ต")
public record GoalCreateResponseDto(
- Long goalId
-) {}
+
+ @Schema(description = "์์ฑ๋ ๋ชฉํ ID", example = "10")
+ Long goalId,
+
+ @Schema(description = "๋ชฉํ ์ ๋ชฉ", example = "์ฌํ ์๊ธ ๋ชจ์ผ๊ธฐ")
+ String title,
+
+ @Schema(description = "๋ชฉํ ๊ธ์ก", example = "1000000")
+ Long targetAmount,
+
+ @Schema(description = "์์ ๊ธ์ก", example = "1000")
+ Long startAmount,
+
+ @Schema(description = "์์์ผ", example = "2026-01-01")
+ LocalDate startDate,
+
+ @Schema(description = "์ข
๋ฃ์ผ", example = "2026-03-01")
+ LocalDate endDate,
+
+ @Schema(description = "๋จ์ ์ผ์", example = "30")
+ Long remainingDays,
+
+ @Schema(description = "์ฐ๊ฒฐ ๊ณ์ข ์ ๋ณด")
+ AccountDto account,
+
+ @Schema(description = "์์ด์ฝ ID", example = "3")
+ Integer iconId
+
+) {
+ @Schema(description = "๋ชฉํ์ ์ฐ๊ฒฐ๋ ๊ณ์ข ์ ๋ณด")
+ public record AccountDto(
+
+ @Schema(description = "์ํ๋ช
", example = "์ ํ์ํ")
+ String bankName,
+
+ @Schema(description = "๊ณ์ข๋ฒํธ", example = "110-123-456789")
+ String accountNumber
+ ) {}
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java
index 2d274ab1..363f6643 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java
@@ -1,21 +1,49 @@
package org.umc.valuedi.domain.goal.dto.response;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
+@Schema(description = "๋ชฉํ ์์ธ ์กฐํ ์๋ต")
public record GoalDetailResponseDto(
+
+ @Schema(description = "๋ชฉํ ID", example = "10")
Long goalId,
+
+ @Schema(description = "๋ชฉํ ์ ๋ชฉ", example = "์ฌํ ์๊ธ ๋ชจ์ผ๊ธฐ")
String title,
+
+ @Schema(description = "ํ์ฌ๊น์ง ๋ชจ์ ๊ธ์ก", example = "300000")
Long savedAmount,
+
+ @Schema(description = "๋ชฉํ ๊ธ์ก", example = "1000000")
Long targetAmount,
+
+ @Schema(description = "๋จ์ ์ผ์", example = "30")
Long remainingDays,
+
+ @Schema(description = "๋ฌ์ฑ๋ฅ (0~100)", example = "30")
Integer achievementRate, // 0~100
+
+ @Schema(description = "์ฐ๊ฒฐ ๊ณ์ข ์ ๋ณด")
AccountDto account,
+
+ @Schema(description = "๋ชฉํ ์ํ", example = "ACTIVE")
GoalStatus status,
+
+ @Schema(description = "์์ ์ฝ๋(# ์ ์ธ)", example = "FF6363")
String colorCode,
+
+ @Schema(description = "์์ด์ฝ ID", example = "3")
Integer iconId
+
) {
+ @Schema(description = "๋ชฉํ์ ์ฐ๊ฒฐ๋ ๊ณ์ข ์ ๋ณด")
public record AccountDto(
+
+ @Schema(description = "์ํ๋ช
", example = "์ ํ์ํ")
String bankName,
+
+ @Schema(description = "๊ณ์ข๋ฒํธ", example = "110-123-456789")
String accountNumber
) {}
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java
index 9ef18349..8bcefa5d 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java
@@ -1,22 +1,42 @@
package org.umc.valuedi.domain.goal.dto.response;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import java.util.List;
+@Schema(description = "๋ชฉํ ๋ชฉ๋ก ์กฐํ ์๋ต")
public record GoalListResponseDto(
+
+ @Schema(description = "๋ชฉํ ์์ฝ ๋ชฉ๋ก")
List goals
+
) {
+ @Schema(description = "๋ชฉํ ์์ฝ ์ ๋ณด")
public record GoalSummaryDto(
+
+ @Schema(description = "๋ชฉํ ID", example = "10")
Long goalId,
+
+ @Schema(description = "๋ชฉํ ์ ๋ชฉ", example = "์ฌํ ์๊ธ ๋ชจ์ผ๊ธฐ")
String title,
- Long remainingAmount,
+
+ @Schema(description = "๋ชจ์ ๊ธ์ก", example = "700000")
+ Long savedAmount,
+
+ @Schema(description = "๋จ์ ์ผ์", example = "30")
Long remainingDays,
- Integer achievementRate, // 0~100
- String bankName, // ๊ณ์ข ์ฐ๋ ๋๋ฉด ๊ตฌํ ํ ์์ ์
๋น
+ @Schema(description = "๋ฌ์ฑ๋ฅ (0~100)", example = "30")
+ Integer achievementRate,
+
+ @Schema(description = "๋ชฉํ ์ํ", example = "ACTIVE")
GoalStatus status,
+
+ @Schema(description = "์์ ์ฝ๋(# ์ ์ธ)", example = "FF6363")
String colorCode,
+
+ @Schema(description = "์์ด์ฝ ID", example = "3")
Integer iconId
) {}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalPrimaryListResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalPrimaryListResponseDto.java
new file mode 100644
index 00000000..e7f8f5d3
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalPrimaryListResponseDto.java
@@ -0,0 +1,30 @@
+package org.umc.valuedi.domain.goal.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+
+@Schema(description = "Primary ๋ชฉํ ๋ชฉ๋ก ์กฐํ ์๋ต (ACTIVE ์ต์ ์, ๊ฐ๋จ ์กฐํ)")
+public record GoalPrimaryListResponseDto(
+
+ @Schema(description = "Primary ๋ชฉํ ๋ชฉ๋ก")
+ List goals
+
+) {
+ @Schema(description = "Primary ๋ชฉํ ์์ฝ ์ ๋ณด")
+ public record GoalPrimarySummaryDto(
+
+ @Schema(description = "๋ชฉํ ID", example = "10")
+ Long goalId,
+
+ @Schema(description = "๋ชฉํ ์ ๋ชฉ", example = "์ฌํ ์๊ธ ๋ชจ์ผ๊ธฐ")
+ String title,
+
+ @Schema(description = "๋ชฉํ ๊ธ์ก", example = "1000000")
+ Long targetAmount,
+
+ @Schema(description = "์์ด์ฝ ID", example = "3")
+ Integer iconId
+
+ ) {}
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
index ffb5b10f..ae735b2e 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java
@@ -19,7 +19,7 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@SQLRestriction("deleted_at IS NULL")
-@SQLDelete(sql = "UPDATE goal SET deleted_at = NOW() WHERE id = ?")
+@SQLDelete(sql = "UPDATE goal SET deleted_at = NOW(), bank_account_id = NULL WHERE id = ?")
public class Goal extends BaseEntity {
@Id
@@ -31,7 +31,7 @@ public class Goal extends BaseEntity {
private Member member;
@OneToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "bank_account_id")
+ @JoinColumn(name = "bank_account_id", unique = true)
private BankAccount bankAccount;
@Column(name = "title", nullable = false, length = 20)
@@ -46,6 +46,9 @@ public class Goal extends BaseEntity {
@Column(name = "target_amount", nullable = false)
private Long targetAmount;
+ @Column(name = "start_amount", nullable = false)
+ private Long startAmount;
+
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private GoalStatus status;
@@ -77,6 +80,7 @@ public void changeEndDate(LocalDate endDate) {
public void changeTargetAmount(Long targetAmount) {
this.targetAmount = targetAmount;
}
+ public void changeStartAmount(Long startAmount) { this.startAmount = startAmount; }
public void changeColor(String color) { this.color = color; }
@@ -99,4 +103,12 @@ public void Fail() {
this.completedAt = LocalDateTime.now();
}
+ public void linkBankAccount(BankAccount bankAccount) {
+ this.bankAccount = bankAccount;
+ }
+
+ public void unlinkBankAccount() {
+ this.bankAccount = null;
+ }
+
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalErrorCode.java b/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalErrorCode.java
index 35ee5145..34fab75f 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalErrorCode.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalErrorCode.java
@@ -14,8 +14,15 @@ public enum GoalErrorCode implements BaseErrorCode {
GOAL_STATUS_INVALID(HttpStatus.NOT_FOUND, "GOAL404_3", "์ด๋ฏธ ์๋ฃ๋์๊ฑฐ๋ ์คํจ ์ฒ๋ฆฌ๋ ๋ชฉํ์
๋๋ค."),
GOAL_COLOR_INVALID(HttpStatus.NOT_FOUND, "GOAL404_4", "์กด์ฌํ์ง ์๋ ๋ชฉํ ์์์
๋๋ค."),
GOAL_ICON_INVALID(HttpStatus.NOT_FOUND, "GOAL404_5", "์กด์ฌํ์ง ์๋ ๋ชฉํ ์์ด์ฝ์
๋๋ค."),
+ ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "GOAL404_6", "๊ณ์ข๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."),
INVALID_DATE_RANGE(HttpStatus.BAD_REQUEST, "GOAL400_1", "์์์ผ์ ์ข
๋ฃ์ผ๋ณด๋ค ๋ฆ์ ์ ์์ต๋๋ค."),
- INVALID_GOAL_LIST_STATUS(HttpStatus.BAD_REQUEST, "GOAL400_2", "๋ชฉํ ๋ชฉ๋ก ์กฐํ status๋ ACTIVE ๋๋ COMPLETE๋ง ๊ฐ๋ฅํฉ๋๋ค.");
+ INVALID_GOAL_LIST_STATUS(HttpStatus.BAD_REQUEST, "GOAL400_2", "๋ชฉํ ๋ชฉ๋ก ์กฐํ status๋ ACTIVE ๋๋ COMPLETE๋ง ๊ฐ๋ฅํฉ๋๋ค."),
+ GOAL_ALREADY_LINKED_ACCOUNT(HttpStatus.BAD_REQUEST, "GOAL400_3", "ํด๋น ๋ชฉํ๋ ์ด๋ฏธ ๊ณ์ข๊ฐ ์ฐ๊ฒฐ๋์ด ์์ต๋๋ค."),
+ ACCOUNT_ALREADY_LINKED_TO_GOAL(HttpStatus.BAD_REQUEST, "GOAL400_4", "ํด๋น ๊ณ์ข๋ ์ด๋ฏธ ๋ค๋ฅธ ๋ชฉํ์ ์ฐ๊ฒฐ๋์ด ์์ต๋๋ค."),
+ GOAL_FORBIDDEN(HttpStatus.FORBIDDEN, "GOAL403_1", "ํด๋น ๋ชฉํ์ ๋ํ ๊ถํ์ด ์์ต๋๋ค."),
+ GOAL_ACCOUNT_INACTIVE(HttpStatus.GONE, "GOAL_410_1", "์ฐ๊ฒฐ๋ ๊ณ์ข๊ฐ ๋นํ์ฑํ(์ญ์ )๋์ด ๋ชฉํ ์์ธ๋ฅผ ์กฐํํ ์ ์์ต๋๋ค."),
+ GOAL_BANK_ACCOUNT_NOT_LINKED(HttpStatus.GONE, "GOAL_400_5", "ํด๋น ๋ชฉํ์ ์ฐ๊ฒฐ ๋ ๊ณ์ข๊ฐ ์์ต๋๋ค.");
+
private final HttpStatus status;
diff --git a/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalSuccessCode.java b/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalSuccessCode.java
index cd514ae3..ad5117d4 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalSuccessCode.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/exception/code/GoalSuccessCode.java
@@ -14,7 +14,10 @@ public enum GoalSuccessCode implements BaseSuccessCode {
GOAL_DETAIL_FETCHED(HttpStatus.OK, "GOAL200_2", "๋ชฉํ ์์ธ ์กฐํ ์ฑ๊ณต"),
GOAL_UPDATED(HttpStatus.OK, "GOAL200_3", "๋ชฉํ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์์ ๋์์ต๋๋ค."),
GOAL_DELETED(HttpStatus.OK, "GOAL204_1", "๋ชฉํ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ๋์์ต๋๋ค."),
- GOAL_ACTIVE_COUNT_FETCHED(HttpStatus.OK, "GOAL200_5", "์งํ ์ค์ธ ๋ชฉํ ๊ฐ์ ์กฐํ ์ฑ๊ณต");
+ GOAL_ACTIVE_COUNT_FETCHED(HttpStatus.OK, "GOAL200_5", "์งํ ์ค์ธ ๋ชฉํ ๊ฐ์ ์กฐํ ์ฑ๊ณต"),
+ GOAL_UNLINKED_ACCOUNTS_FETCHED(HttpStatus.OK, "GOAL200_6", "์ฐ๊ฒฐ ๋ชฉํ๊ฐ ์๋ ๊ณ์ข ์กฐํ ์ฑ๊ณต"),
+ GOAL_ACCOUNT_LINKED(HttpStatus.OK, "GOAL200_7", "๊ณ์ข๊ฐ ๋ชฉํ์ ์ฑ๊ณต์ ์ผ๋ก ์ฐ๊ฒฐ ๋์์ต๋๋ค."),
+ GOAL_LEDGER_LIST_FETCHED(HttpStatus.OK, "GOAL200_8", "๋ชฉํ ๊ฐ๊ณ๋ถ ๊ฑฐ๋๋ด์ญ ์กฐํ๊ฐ ์๋ฃ๋์์ต๋๋ค.");
private final HttpStatus status;
private final String code;
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
index 5ccd72b4..8aaab912 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepository.java
@@ -1,30 +1,29 @@
package org.umc.valuedi.domain.goal.repository;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
-import org.springframework.data.domain.Pageable;
import java.time.LocalDate;
import java.util.List;
public interface GoalRepository extends JpaRepository, GoalRepositoryCustom {
- List findAllByMember_Id( Long memberId);
-
+ List findAllByMember_Id(Long memberId);
List findAllByMember_IdAndStatus(Long memberId, GoalStatus status);
List findAllByMember_IdAndStatusIn(Long memberId, List statuses);
long countByMember_IdAndStatus(Long memberId, GoalStatus status);
List findAllByStatus(GoalStatus status);
- List findAllByStatusAndEndDateLessThanEqual(GoalStatus status, LocalDate date);
+
List findAllByMember_IdAndStatus(Long memberId, GoalStatus status, Pageable pageable);
List findAllByMember_IdAndStatusIn(Long memberId, List statuses, Pageable pageable);
- List findAllByBankAccountId(Long bankAccountId);
+ boolean existsByBankAccount_Id(Long bankAccountId);
@Modifying(clearAutomatically = true)
@Query("UPDATE Goal g SET g.deletedAt = CURRENT_TIMESTAMP " +
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
index 0634b2b5..d8aa73d7 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryCustom.java
@@ -1,9 +1,17 @@
package org.umc.valuedi.domain.goal.repository;
import org.umc.valuedi.domain.goal.entity.Goal;
+import org.umc.valuedi.domain.goal.enums.GoalStatus;
+import java.util.List;
import java.util.Optional;
public interface GoalRepositoryCustom {
Optional findByIdWithDetails(Long goalId);
+
+ List findAllByMemberIdAndStatusOrderByCreatedAtDesc(Long memberId, GoalStatus status);
+
+ List findLinkedBankAccountIdsByMemberId(Long memberId);
+
+ Optional findByIdAndMemberId(Long goalId, Long memberId);
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
index b5faa2cf..a9f81798 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/repository/GoalRepositoryImpl.java
@@ -3,7 +3,9 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.umc.valuedi.domain.goal.entity.Goal;
+import org.umc.valuedi.domain.goal.enums.GoalStatus;
+import java.util.List;
import java.util.Optional;
import static org.umc.valuedi.domain.asset.entity.QBankAccount.bankAccount;
@@ -26,4 +28,41 @@ public Optional findByIdWithDetails(Long goalId) {
return Optional.ofNullable(result);
}
+
+ @Override
+ public List findAllByMemberIdAndStatusOrderByCreatedAtDesc(Long memberId, GoalStatus status) {
+ return queryFactory
+ .selectFrom(goal)
+ .where(
+ goal.member.id.eq(memberId),
+ goal.status.eq(status)
+ )
+ .orderBy(goal.createdAt.desc())
+ .fetch();
+ }
+
+ @Override
+ public List findLinkedBankAccountIdsByMemberId(Long memberId) {
+ return queryFactory
+ .select(goal.bankAccount.id)
+ .from(goal)
+ .where(
+ goal.member.id.eq(memberId),
+ goal.bankAccount.isNotNull()
+ )
+ .fetch();
+ }
+
+ @Override
+ public Optional findByIdAndMemberId(Long goalId, Long memberId) {
+ Goal result = queryFactory
+ .selectFrom(goal)
+ .where(
+ goal.id.eq(goalId),
+ goal.member.id.eq(memberId)
+ )
+ .fetchOne();
+
+ return Optional.ofNullable(result);
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalAccountCommandService.java b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalAccountCommandService.java
new file mode 100644
index 00000000..7fd8120d
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalAccountCommandService.java
@@ -0,0 +1,47 @@
+package org.umc.valuedi.domain.goal.service.command;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
+import org.umc.valuedi.domain.goal.entity.Goal;
+import org.umc.valuedi.domain.goal.exception.GoalException;
+import org.umc.valuedi.domain.goal.exception.code.GoalErrorCode;
+import org.umc.valuedi.domain.goal.repository.GoalRepository;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class GoalAccountCommandService {
+
+ private final GoalRepository goalRepository;
+ private final BankAccountRepository bankAccountRepository;
+
+ public void setLinkedAccount(Long memberId, Long goalId, Long accountId) {
+
+ Goal goal = goalRepository.findByIdAndMemberId(goalId, memberId)
+ .orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
+
+ BankAccount newAccount = bankAccountRepository.findByIdAndMemberId(accountId, memberId)
+ .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND));
+
+ // ์ด๋ฏธ ๊ฐ์ ๊ณ์ข๊ฐ ์ฐ๊ฒฐ๋ผ ์์ผ๋ฉด ๊ทธ๋ฅ ์ฑ๊ณต
+ if (goal.getBankAccount() != null && goal.getBankAccount().getId().equals(accountId)) {
+ return;
+ }
+
+ // ๋ค๋ฅธ Goal์ด ์ด ๊ณ์ข๋ฅผ ์ฐ๊ณ ์์ผ๋ฉด ์คํจ
+ if (goalRepository.existsByBankAccount_Id(accountId)) {
+ throw new GoalException(GoalErrorCode.ACCOUNT_ALREADY_LINKED_TO_GOAL);
+ }
+
+ try {
+ // ๊ต์ฒด(๊ธฐ์กด ์ฐ๊ฒฐ ๋ฎ์ด์ฐ๊ธฐ)
+ goal.linkBankAccount(newAccount);
+ } catch (DataIntegrityViolationException e) {
+ throw new GoalException(GoalErrorCode.ACCOUNT_ALREADY_LINKED_TO_GOAL);
+ }
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java
index bcd6d95d..fbcd9e96 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java
@@ -3,6 +3,8 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
import org.umc.valuedi.domain.goal.converter.GoalConverter;
import org.umc.valuedi.domain.goal.dto.request.GoalCreateRequestDto;
import org.umc.valuedi.domain.goal.dto.request.GoalUpdateRequestDto;
@@ -25,24 +27,38 @@ public class GoalCommandService {
private final GoalRepository goalRepository;
private final MemberRepository memberRepository;
+ private final BankAccountRepository bankAccountRepository;
// ๋ชฉํ ์์ฑ
- public GoalCreateResponseDto createGoal(GoalCreateRequestDto req) {
- Member member = memberRepository.findById(req.memberId())
- .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
+ public GoalCreateResponseDto createGoal(Long memberId, GoalCreateRequestDto req) {
GoalValidator.validateDateRange(req.startDate(), req.endDate());
GoalValidator.validateStyle(req.colorCode(), req.iconId());
- Goal goal = GoalConverter.toEntity(member, req);
+ Member member = memberRepository.findById(memberId)
+ .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
+
+ // ๋ด ๊ณ์ข + ํ์ฑ ์ํ ๊ฒ์ฆ
+ BankAccount account = bankAccountRepository.findByIdAndMemberId(req.bankAccountId(), memberId)
+ .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND));
+
+ // ์ด๋ฏธ ๋ค๋ฅธ ๋ชฉํ๊ฐ ์ด ๊ณ์ข๋ฅผ ์ฐ๊ณ ์๋์ง ๊ฒ์ฆ
+ if (goalRepository.existsByBankAccount_Id(account.getId())) {
+ throw new GoalException(GoalErrorCode.ACCOUNT_ALREADY_LINKED_TO_GOAL);
+ }
+
+ Long startAmount = account.getBalanceAmount();
+
+ // Goal ์ํฐํฐ ์์ฑ ์ bankAccount ํฌํจ
+ Goal goal = GoalConverter.toEntity(member, account, req, startAmount);
Goal saved = goalRepository.save(goal);
- return new GoalCreateResponseDto(saved.getId());
+ return GoalConverter.toCreateDto(saved);
}
// ๋ชฉํ ์์
- public void updateGoal(Long goalId, GoalUpdateRequestDto req) {
- Goal goal = goalRepository.findById(goalId)
+ public void updateGoal(Long memberId, Long goalId, GoalUpdateRequestDto req) {
+ Goal goal = goalRepository.findByIdAndMemberId(goalId, memberId)
.orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
if (goal.getStatus() != GoalStatus.ACTIVE) {
@@ -62,8 +78,8 @@ public void updateGoal(Long goalId, GoalUpdateRequestDto req) {
}
// ๋ชฉํ ์ญ์
- public void deleteGoal(Long goalId) {
- Goal goal = goalRepository.findById(goalId)
+ public void deleteGoal(Long memberId, Long goalId) {
+ Goal goal = goalRepository.findByIdAndMemberId(goalId, memberId)
.orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
goalRepository.delete(goal);
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalAccountQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalAccountQueryService.java
new file mode 100644
index 00000000..b307e2b7
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalAccountQueryService.java
@@ -0,0 +1,32 @@
+package org.umc.valuedi.domain.goal.service.query;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
+import org.umc.valuedi.domain.goal.converter.GoalAccountConverter;
+import org.umc.valuedi.domain.goal.dto.response.GoalAccountResDto;
+import org.umc.valuedi.domain.goal.repository.GoalRepository;
+
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class GoalAccountQueryService {
+
+ private final GoalRepository goalRepository;
+ private final BankAccountRepository bankAccountRepository;
+
+ public GoalAccountResDto.UnlinkedBankAccountListDTO getUnlinkedAccounts(Long memberId) {
+ List linkedAccountIds = goalRepository.findLinkedBankAccountIdsByMemberId(memberId);
+
+ List unlinked = bankAccountRepository.findUnlinkedByMemberId(
+ memberId,
+ linkedAccountIds
+ );
+
+ return GoalAccountConverter.toUnlinkedListDTO(unlinked);
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java
new file mode 100644
index 00000000..872b1f34
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java
@@ -0,0 +1,54 @@
+package org.umc.valuedi.domain.goal.service.query;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.goal.entity.Goal;
+import org.umc.valuedi.domain.goal.exception.GoalException;
+import org.umc.valuedi.domain.goal.exception.code.GoalErrorCode;
+import org.umc.valuedi.domain.goal.repository.GoalRepository;
+import org.umc.valuedi.domain.ledger.converter.LedgerConverter;
+import org.umc.valuedi.domain.ledger.dto.response.LedgerListResponse;
+import org.umc.valuedi.domain.ledger.entity.LedgerEntry;
+import org.umc.valuedi.domain.ledger.repository.LedgerQueryRepository;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class GoalLedgerQueryService {
+
+ private final GoalRepository goalRepository;
+ private final LedgerQueryRepository ledgerQueryRepository;
+
+ public LedgerListResponse getGoalLedgerTransactions(Long memberId, Long goalId, int page, int size) {
+ Goal goal = goalRepository.findById(goalId)
+ .orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
+
+ if (!goal.getMember().getId().equals(memberId)) {
+ throw new GoalException(GoalErrorCode.GOAL_FORBIDDEN);
+ }
+
+ LocalDateTime from = goal.getStartDate().atStartOfDay();
+ LocalDateTime to = goal.getEndDate().atTime(LocalTime.MAX);
+
+ Page result = ledgerQueryRepository.searchByPeriodLatest(
+ memberId,
+ from,
+ to,
+ PageRequest.of(page, size)
+ );
+
+ return LedgerConverter.toLedgerListResponse(
+ result.getContent(),
+ page,
+ size,
+ result.getTotalElements()
+ );
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java
index a63d0da1..adc696ac 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java
@@ -6,6 +6,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.goal.converter.GoalConverter;
import org.umc.valuedi.domain.goal.dto.response.GoalListResponseDto;
import org.umc.valuedi.domain.goal.entity.Goal;
@@ -71,14 +72,20 @@ private List findGoals(Long memberId, GoalStatus status) {
}
private List toSummaryDtos(List goals) {
- long savedAmount = 0; // ๊ณ์ข ์ฐ๋ ํ ์์
return goals.stream()
- .map(g -> GoalConverter.toSummaryDto(
- g,
- savedAmount,
- achievementRateService.calculateRate(savedAmount, g.getTargetAmount())
- ))
+ .map(g -> {
+ BankAccount account = g.getBankAccount();
+
+ if (!account.getIsActive()) {
+ throw new GoalException(GoalErrorCode.GOAL_ACCOUNT_INACTIVE);
+ }
+
+ long savedAmount = account.getBalanceAmount() - g.getStartAmount();
+ int rate = achievementRateService.calculateRate(savedAmount, g.getTargetAmount());
+
+ return GoalConverter.toSummaryDto(g, savedAmount, rate);
+ })
.toList();
}
diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
index 64991c18..355d7f5b 100644
--- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
+++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java
@@ -3,9 +3,11 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.goal.converter.GoalConverter;
import org.umc.valuedi.domain.goal.dto.response.GoalActiveCountResponseDto;
import org.umc.valuedi.domain.goal.dto.response.GoalDetailResponseDto;
+import org.umc.valuedi.domain.goal.dto.response.GoalPrimaryListResponseDto;
import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.domain.goal.enums.GoalStatus;
import org.umc.valuedi.domain.goal.exception.GoalException;
@@ -16,6 +18,8 @@
import org.umc.valuedi.domain.member.exception.code.MemberErrorCode;
import org.umc.valuedi.domain.member.repository.MemberRepository;
+import java.util.List;
+
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@@ -26,15 +30,17 @@ public class GoalQueryService {
private final GoalAchievementRateService achievementRateService;
// ๋ชฉํ ์์ธ ์กฐํ
- public GoalDetailResponseDto getGoalDetail(Long goalId) {
- Goal goal = goalRepository.findByIdWithDetails(goalId)
+ public GoalDetailResponseDto getGoalDetail(Long memberId, Long goalId) {
+ Goal goal = goalRepository.findByIdAndMemberId(goalId, memberId)
.orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND));
- long savedAmount = 0; // ๊ณ์ข ์ฐ๋ ํ ์์
- if (goal.getBankAccount() != null && goal.getBankAccount().getBalanceAmount() != null) {
- savedAmount = goal.getBankAccount().getBalanceAmount();
+ BankAccount account = goal.getBankAccount();
+
+ if (!account.getIsActive()) {
+ throw new GoalException(GoalErrorCode.GOAL_ACCOUNT_INACTIVE);
}
+ long savedAmount = account.getBalanceAmount() - goal.getStartAmount();
int rate = achievementRateService.calculateRate(savedAmount, goal.getTargetAmount());
return GoalConverter.toDetailDto(goal, savedAmount, rate);
@@ -49,4 +55,13 @@ public GoalActiveCountResponseDto getActiveGoalCount(Long memberId) {
long count = goalRepository.countByMember_IdAndStatus(memberId, GoalStatus.ACTIVE);
return new GoalActiveCountResponseDto((int) count);
}
+
+
+ // ์ฃผ์ ๋ชฉํ ์กฐํ
+ public GoalPrimaryListResponseDto getPrimaryGoals(Long memberId) {
+ var goals = goalRepository.findAllByMemberIdAndStatusOrderByCreatedAtDesc(
+ memberId, GoalStatus.ACTIVE
+ );
+ return GoalConverter.toPrimaryListResponse(goals);
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerQueryRepository.java b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerQueryRepository.java
index 4a6eaf78..4c031a1c 100644
--- a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerQueryRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerQueryRepository.java
@@ -248,6 +248,44 @@ private OrderSpecifier> getOrderSpecifier(LedgerSortType sort) {
return ledgerEntry.transactionAt.desc();
}
}
+
+ public Page searchByPeriodLatest(
+ Long memberId,
+ LocalDateTime from,
+ LocalDateTime to,
+ Pageable pageable
+ ) {
+ List content = queryFactory
+ .selectFrom(ledgerEntry)
+ .leftJoin(ledgerEntry.category, category).fetchJoin()
+ .leftJoin(ledgerEntry.bankTransaction, bankTransaction).fetchJoin()
+ .leftJoin(ledgerEntry.cardApproval, cardApproval).fetchJoin()
+ .where(
+ ledgerEntry.member.id.eq(memberId),
+ periodBetween(from, to)
+ )
+ .orderBy(ledgerEntry.transactionAt.desc())
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .fetch();
+
+ Long total = queryFactory
+ .select(ledgerEntry.count())
+ .from(ledgerEntry)
+ .where(
+ ledgerEntry.member.id.eq(memberId),
+ periodBetween(from, to)
+ )
+ .fetchOne();
+
+ return new PageImpl<>(content, pageable, total != null ? total : 0L);
+ }
+
+ private BooleanExpression periodBetween(LocalDateTime from, LocalDateTime to) {
+ if (from == null || to == null) return null;
+ return ledgerEntry.transactionAt.between(from, to);
+ }
+
}
diff --git a/stop b/stop
deleted file mode 100644
index b833c99c..00000000
--- a/stop
+++ /dev/null
@@ -1 +0,0 @@
-MySQL80
From 293db2b4a9cf5aae7c231693e48b133dba0143f2 Mon Sep 17 00:00:00 2001
From: dohee <152317074+seamooll@users.noreply.github.com>
Date: Tue, 3 Feb 2026 22:48:57 +0900
Subject: [PATCH 4/4] =?UTF-8?q?[FEATURE/#54]=20=EC=A0=84=EC=B2=B4=20?=
=?UTF-8?q?=EC=9E=90=EC=82=B0=20=EB=8F=99=EA=B8=B0=ED=99=94=20API=20?=
=?UTF-8?q?=EA=B5=AC=ED=98=84=20=20(#70)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ Related Issue
- closes #54
## ๐ Summary
์ฌ์ฉ์์ ๊ธ์ต ์์ฐ(์ํ ๋ด์ญ, ์นด๋ ๋ด์ญ)์ ํ ๋ฒ์ ๊ฐ์ ธ์ค๋ ์ ์ฒด ๋๊ธฐํ API๋ฅผ ๊ตฌํํ์์ต๋๋ค.
- ์ ์ฒด ์์ฐ ๋๊ธฐํ API ๊ตฌํ ๋ฐ ์ฑ๋ฅ ๊ฐ์
- ์ํ/์นด๋ ๊ฑฐ๋ ๋ด์ญ์ ์ธ๋ถ API๋ก ์กฐํ ํ DB์ ์ ์ฅํ๋ ์ ์ฒด ๋๊ธฐํ ๋ก์ง ๊ตฌํ
- JPA saveAll() ๋์ JdbcTemplate ๊ธฐ๋ฐ Bulk Insert ๋ฐฉ์ ์ ์ฉ (๋๋ Insert ์ ๋ฐ์ํ๋
์ฑ๋ฅ ์ ํ ๋ฌธ์ ๊ฐ์ )
- ํธ๋ฌ๋ธ์ํ
๋ฐ ์์ ํ
## ๐ Changes
- [X] API ๋ณ๊ฒฝ (์ถ๊ฐ/์์ )
- [ ] ๋ฐ์ดํฐ ๋ฐ ๋๋ฉ์ธ ๋ณ๊ฒฝ (DB, ๋น์ฆ๋์ค ๋ก์ง)
- [ ] ์ค์ ๋๋ ์ธํ๋ผ ๊ด๋ จ ๋ณ๊ฒฝ
- [X] ๋ฆฌํฉํ ๋ง
## ๐ฌ Questions & Review Points
## ๐ธ API Test Results (Swagger)
## โ
Checklist
- [X] API ํ
์คํธ ์๋ฃ
- [X] ํ
์คํธ ๊ฒฐ๊ณผ ์ฌ์ง ์ฒจ๋ถ
- [X] ๋น๋ ์ฑ๊ณต ํ์ธ (./gradlew build)
---
.../asset/controller/AssetController.java | 12 +
.../asset/controller/AssetControllerDocs.java | 48 ++++
.../domain/asset/dto/res/AssetResDTO.java | 29 +-
.../domain/asset/entity/BankTransaction.java | 10 +-
.../asset/exception/AssetException.java | 10 +
.../asset/exception/code/AssetErrorCode.java | 18 ++
.../exception/code/AssetSuccessCode.java | 19 ++
.../BankTransactionRepository.java | 10 +
.../BankTransactionRepositoryImpl.java | 4 +-
.../repository/card/card/CardRepository.java | 2 +
.../cardApproval/CardApprovalRepository.java | 11 +
.../CardApprovalRepositoryImpl.java | 4 +-
.../service/command/AssetFetchService.java | 147 ++++++++++
.../command/AssetSyncFacadeService.java | 49 ++++
.../service/command/AssetSyncProcessor.java | 50 ++++
.../service/command/AssetSyncService.java | 70 +----
.../command/worker/AssetFetchWorker.java | 108 ++++++++
.../service/ConnectionEventListener.java | 2 +-
.../domain/ledger/entity/LedgerEntry.java | 3 +-
.../repository/LedgerEntryRepository.java | 13 +-
.../LedgerEntryRepositoryCustom.java | 8 +
.../repository/LedgerEntryRepositoryImpl.java | 42 +++
.../service/command/LedgerSyncService.java | 252 ++++++------------
.../domain/mbti/entity/MbtiTypeInfo.java | 2 +-
.../valuedi/domain/member/entity/Member.java | 7 +
.../valuedi/global/config/AsyncConfig.java | 12 +-
.../codef/config/CodefFeignConfig.java | 2 +
.../codef/converter/CodefAssetConverter.java | 53 +++-
.../codef/service/CodefAssetService.java | 35 ++-
29 files changed, 776 insertions(+), 256 deletions(-)
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/exception/AssetException.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetErrorCode.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetSuccessCode.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncFacadeService.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java
create mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java
create mode 100644 src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryCustom.java
create mode 100644 src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryImpl.java
diff --git a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
index 3275f628..b786ae49 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java
@@ -4,12 +4,15 @@
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.umc.valuedi.domain.asset.exception.code.AssetSuccessCode;
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
import org.umc.valuedi.domain.asset.dto.res.BankResDTO;
import org.umc.valuedi.domain.asset.dto.res.CardResDTO;
import org.umc.valuedi.domain.asset.service.query.AssetQueryService;
+import org.umc.valuedi.domain.asset.service.command.AssetSyncFacadeService;
import org.umc.valuedi.domain.connection.service.ConnectionQueryService;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.apiPayload.code.GeneralSuccessCode;
@@ -25,6 +28,7 @@ public class AssetController implements AssetControllerDocs {
private final ConnectionQueryService connectionQueryService;
private final AssetQueryService assetQueryService;
+ private final AssetSyncFacadeService assetSyncFacadeService;
@GetMapping("/cards")
@@ -79,4 +83,12 @@ public ApiResponse getAssetCount(
) {
return ApiResponse.onSuccess(GeneralSuccessCode.OK, assetQueryService.getAssetSummaryCount(memberId));
}
+
+ @PostMapping("/sync/refresh")
+ public ApiResponse refreshAssetSync(
+ @CurrentMember Long memberId
+ ) {
+ assetSyncFacadeService.refreshAssetSync(memberId);
+ return ApiResponse.onSuccess(AssetSuccessCode.SYNC_REQUEST_SUCCESS, null);
+ }
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
index b76f3c05..6d9382a3 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java
@@ -319,4 +319,52 @@ ApiResponse getAccountsByBank(
)
})
ApiResponse getAssetCount(@CurrentMember Long memberId);
+
+ @Operation(
+ summary = "์ ์ฒด ์์ฐ ์๋ก๊ณ ์นจ(๋๊ธฐํ) ์์ฒญ API",
+ description = """
+ ์ฌ์ฉ์๊ฐ ์ฐ๋ํ ๋ชจ๋ ๊ธ์ต์ฌ์ ์ต์ ๊ฑฐ๋๋ด์ญ ์์ง์ **๋ฐฑ๊ทธ๋ผ์ด๋์์ ์์**ํ๋๋ก ์์ฒญํฉ๋๋ค.
+ - **์ฆ์ ์๋ต**: API๋ ๋๊ธฐํ ์์
์ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๋๊ธฐ๊ณ ์ฆ์ '์์ฒญ ์ฑ๊ณต' ์๋ต์ ๋ฐํํฉ๋๋ค.
+ - **๊ฒฐ๊ณผ ํ์ธ**: ์ค์ ๋๊ธฐํ ๊ฒฐ๊ณผ๋ ์ ์ ํ ์์ฐ ๊ด๋ จ ๋ค๋ฅธ API๋ฅผ ํตํด ํ์ธํด์ผ ํฉ๋๋ค.
+ - **10๋ถ ์ฟจํ์**: ๋ง์ง๋ง ๋๊ธฐํ ์๊ฐ์ผ๋ก๋ถํฐ 10๋ถ ์ด๋ด์๋ ์ฌํธ์ถํ ์ ์์ต๋๋ค.
+ """
+ )
+ @ApiResponses({
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(
+ responseCode = "200",
+ description = "์ฑ๊ณต - ๋๊ธฐํ ์์
์ด ์ฑ๊ณต์ ์ผ๋ก ์์๋จ",
+ content = @Content(
+ schema = @Schema(implementation = ApiResponse.class),
+ examples = @ExampleObject(
+ name = "์ฑ๊ณต ์์",
+ value = """
+ {
+ "isSuccess": true,
+ "code": "ASSET200_1",
+ "message": "์์ฐ ๋๊ธฐํ ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ ์๋์์ต๋๋ค. ์ ์ ํ ๋ฐ์ดํฐ๋ฅผ ํ์ธํด์ฃผ์ธ์.",
+ "result": null
+ }
+ """
+ )
+ )
+ ),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(
+ responseCode = "429",
+ description = "์คํจ - 10๋ถ ์ฟจํ์ ์ ํ",
+ content = @Content(
+ schema = @Schema(implementation = ApiResponse.class),
+ examples = @ExampleObject(
+ name = "์ฟจํ์ ์คํจ ์์",
+ value = """
+ {
+ "isSuccess": false,
+ "code": "ASSET429_1",
+ "message": "์์ฐ ๋๊ธฐํ๋ 10๋ถ์ ํ ๋ฒ๋ง ์์ฒญํ ์ ์์ต๋๋ค."
+ }
+ """
+ )
+ )
+ )
+ })
+ ApiResponse refreshAssetSync(@Parameter(hidden = true) @CurrentMember Long memberId);
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java
index a1cdc956..ace3564c 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java
@@ -6,6 +6,9 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
+import java.time.LocalDate;
+import java.util.List;
+
@Schema(description = "์์ฐ ํตํฉ ์๋ต DTO")
public class AssetResDTO {
@@ -24,4 +27,28 @@ public static class AssetSummaryCountDTO {
@Schema(description = "์ด ์ฐ๋ ์์ฐ ์ (๊ณ์ข + ์นด๋)", example = "8")
private Long totalAssetCount;
}
-}
\ No newline at end of file
+
+ @Getter
+ @AllArgsConstructor
+ @Builder
+ @Schema(description = "AssetFetchService์ ๋ด๋ถ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ DTO (API ์๋ต์ผ๋ก ์ง์ ์ฌ์ฉ๋์ง ์์)")
+ public static class AssetSyncResult {
+ @Schema(description = "์๋ก ์์ง๋ ์ํ ๊ฑฐ๋๋ด์ญ ์")
+ private int newBankTransactionCount;
+
+ @Schema(description = "์๋ก ์์ง๋ ์นด๋ ์น์ธ๋ด์ญ ์")
+ private int newCardApprovalCount;
+
+ @Schema(description = "๋ฐ์ดํฐ ์์ง์ ์ฑ๊ณตํ ๊ธฐ๊ด ๋ชฉ๋ก")
+ private List successOrganizations;
+
+ @Schema(description = "๋ฐ์ดํฐ ์์ง์ ์คํจํ ๊ธฐ๊ด ๋ชฉ๋ก")
+ private List failureOrganizations;
+
+ @Schema(description = "๊ฐ๊ณ๋ถ ๋๊ธฐํ์ ์ฌ์ฉ๋ ์กฐํ ์์์ผ")
+ private LocalDate fromDate;
+
+ @Schema(description = "๊ฐ๊ณ๋ถ ๋๊ธฐํ์ ์ฌ์ฉ๋ ์กฐํ ์ข
๋ฃ์ผ")
+ private LocalDate toDate;
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/entity/BankTransaction.java b/src/main/java/org/umc/valuedi/domain/asset/entity/BankTransaction.java
index 328c794e..8e8181e7 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/entity/BankTransaction.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/entity/BankTransaction.java
@@ -17,7 +17,15 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
-@Table(name = "bank_transaction")
+@Table(
+ name = "bank_transaction",
+ uniqueConstraints = {
+ @UniqueConstraint(
+ name = "uk_bank_transaction_identity",
+ columnNames = {"bank_account_id", "tr_datetime", "in_amount", "out_amount", "desc3"}
+ )
+ }
+)
public class BankTransaction extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
diff --git a/src/main/java/org/umc/valuedi/domain/asset/exception/AssetException.java b/src/main/java/org/umc/valuedi/domain/asset/exception/AssetException.java
new file mode 100644
index 00000000..7051857e
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/exception/AssetException.java
@@ -0,0 +1,10 @@
+package org.umc.valuedi.domain.asset.exception;
+
+import org.umc.valuedi.global.apiPayload.code.BaseErrorCode;
+import org.umc.valuedi.global.apiPayload.exception.GeneralException;
+
+public class AssetException extends GeneralException {
+ public AssetException(BaseErrorCode code) {
+ super(code);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetErrorCode.java b/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetErrorCode.java
new file mode 100644
index 00000000..4bae5fa9
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetErrorCode.java
@@ -0,0 +1,18 @@
+package org.umc.valuedi.domain.asset.exception.code;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import org.umc.valuedi.global.apiPayload.code.BaseErrorCode;
+
+@Getter
+@AllArgsConstructor
+public enum AssetErrorCode implements BaseErrorCode {
+
+ SYNC_COOL_DOWN(HttpStatus.TOO_MANY_REQUESTS, "ASSET429_1", "์ ์ฒด ๋๊ธฐํ๋ 10๋ถ์ ํ ๋ฒ๋ง ์์ฒญํ ์ ์์ต๋๋ค."),
+ ;
+
+ private final HttpStatus status;
+ private final String code;
+ private final String message;
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetSuccessCode.java b/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetSuccessCode.java
new file mode 100644
index 00000000..6b466093
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/exception/code/AssetSuccessCode.java
@@ -0,0 +1,19 @@
+package org.umc.valuedi.domain.asset.exception.code;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import org.umc.valuedi.global.apiPayload.code.BaseSuccessCode;
+
+
+@Getter
+@AllArgsConstructor
+public enum AssetSuccessCode implements BaseSuccessCode {
+
+ SYNC_REQUEST_SUCCESS(HttpStatus.OK, "ASSET200_1", "์์ฐ ๋๊ธฐํ ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ ์๋์์ต๋๋ค. ์ ์ ํ ๋ฐ์ดํฐ๋ฅผ ํ์ธํด์ฃผ์ธ์."),
+ ;
+
+ private final HttpStatus status;
+ private final String code;
+ private final String message;
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
index 02d0f3cb..2a4e28ef 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java
@@ -1,11 +1,21 @@
package org.umc.valuedi.domain.asset.repository.bank.bankTransaction;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
+import java.util.Optional;
public interface BankTransactionRepository extends JpaRepository, BankTransactionRepositoryCustom {
List findByTrDateBetween(LocalDate fromDate, LocalDate toDate);
+
+ @Query("SELECT MAX(bt.trDate) FROM BankTransaction bt WHERE bt.bankAccount = :account")
+ Optional findLatestTransactionDateByAccount(@Param("account") BankAccount account);
+
+ List findByBankAccountInAndTrDatetimeAfter(List accounts, LocalDateTime startTime);
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
index f73d09bf..f183c577 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java
@@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import java.sql.PreparedStatement;
@@ -10,6 +11,7 @@
import java.sql.Timestamp;
import java.util.List;
+@Repository
@RequiredArgsConstructor
public class BankTransactionRepositoryImpl implements BankTransactionRepositoryCustom {
@@ -17,7 +19,7 @@ public class BankTransactionRepositoryImpl implements BankTransactionRepositoryC
@Override
public void bulkInsert(List transactions) {
- if (transactions.isEmpty()) {
+ if (transactions == null || transactions.isEmpty()) {
return;
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
index fcd3d092..06a643a8 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java
@@ -32,4 +32,6 @@ public interface CardRepository extends JpaRepository {
"AND c.isActive = true " +
"ORDER BY c.createdAt DESC")
List findAllByMemberId(@Param("memberId") Long memberId);
+
+ List findAllByCodefConnection(CodefConnection connection);
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
index be58af96..2a52d005 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java
@@ -1,13 +1,24 @@
package org.umc.valuedi.domain.asset.repository.card.cardApproval;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.umc.valuedi.domain.asset.entity.Card;
import org.umc.valuedi.domain.asset.entity.CardApproval;
+import org.umc.valuedi.domain.member.entity.Member;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.Optional;
+import java.util.Set;
public interface CardApprovalRepository extends JpaRepository, CardApprovalRepositoryCustom {
List findByUsedDateBetween(LocalDate fromDate, LocalDate toDate);
boolean existsByUsedAmountAndUsedDatetimeBetween(Long amount, LocalDateTime start, LocalDateTime end);
+
+ @Query("SELECT MAX(ca.usedDate) FROM CardApproval ca JOIN ca.card c WHERE c.codefConnection.member = :member")
+ Optional findLatestApprovalDateByMember(@Param("member") Member member);
+
+ List findByCardInAndApprovalNoIn(List cards, List approvalNos);
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
index 18c68887..64877888 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepositoryImpl.java
@@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
import org.umc.valuedi.domain.asset.entity.CardApproval;
import java.sql.PreparedStatement;
@@ -10,6 +11,7 @@
import java.sql.Timestamp;
import java.util.List;
+@Repository
@RequiredArgsConstructor
public class CardApprovalRepositoryImpl implements CardApprovalRepositoryCustom {
@@ -17,7 +19,7 @@ public class CardApprovalRepositoryImpl implements CardApprovalRepositoryCustom
@Override
public void bulkInsert(List approvals) {
- if (approvals.isEmpty()) {
+ if (approvals == null || approvals.isEmpty()) {
return;
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java
new file mode 100644
index 00000000..692491a0
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java
@@ -0,0 +1,147 @@
+package org.umc.valuedi.domain.asset.service.command;
+
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.asset.entity.BankTransaction;
+import org.umc.valuedi.domain.asset.entity.Card;
+import org.umc.valuedi.domain.asset.entity.CardApproval;
+import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
+import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
+import org.umc.valuedi.domain.asset.service.command.worker.AssetFetchWorker;
+import org.umc.valuedi.domain.connection.entity.CodefConnection;
+import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository;
+import org.umc.valuedi.domain.member.entity.Member;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AssetFetchService {
+
+ private final EntityManager entityManager;
+ private final CodefConnectionRepository codefConnectionRepository;
+ private final BankTransactionRepository bankTransactionRepository;
+ private final CardApprovalRepository cardApprovalRepository;
+ private final AssetFetchWorker assetFetchWorker;
+
+ private record BankTransactionKey(LocalDateTime trDatetime, Long inAmount, Long outAmount, String desc3) {}
+ private record CardApprovalKey(Card card, String approvalNo) {}
+
+ @Transactional
+ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
+ List connections = codefConnectionRepository.findByMemberId(member.getId());
+ LocalDate today = LocalDate.now();
+
+ // ๊ฐ ๊ธฐ๊ด๋ณ๋ก ๋น๋๊ธฐ API ํธ์ถ ์คํ
+ List> futures = connections.stream()
+ .map(connection -> assetFetchWorker.fetchAndConvertData(connection, member))
+ .collect(Collectors.toList());
+
+ // ๋ชจ๋ ๋น๋๊ธฐ ์์
์ด ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํ๊ณ ๊ฒฐ๊ณผ ์ทจํฉ
+ List fetchResults = futures.stream()
+ .map(CompletableFuture::join)
+ .collect(Collectors.toList());
+
+ // ๋ชจ๋ ๊ฑฐ๋๋ด์ญ์ ํ๋ฒ์ ์กฐํํ๊ธฐ ์ํ ์ค๋น
+ List allFetchedBankTransactions = new ArrayList<>();
+ List allFetchedCardApprovals = new ArrayList<>();
+
+ List successOrganizations = new ArrayList<>();
+ List failureOrganizations = new ArrayList<>();
+ LocalDate overallMinDate = today;
+
+ // ์ทจํฉ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก DB ์ ์ฅ ๋ฐ ์ค๋ณต ์ ๊ฑฐ ๋ก์ง ์คํ
+ for (AssetFetchWorker.FetchResult result : fetchResults) {
+ if (result.isSuccess()) {
+ successOrganizations.add(result.connection().getOrganization());
+ allFetchedBankTransactions.addAll(result.bankTransactions());
+ allFetchedCardApprovals.addAll(result.cardApprovals());
+ if (result.startDate().isBefore(overallMinDate)) {
+ overallMinDate = result.startDate();
+ }
+ } else {
+ failureOrganizations.add(result.connection().getOrganization());
+ log.error("[ASSET-FETCH] ์์ฐ ๋ฐ์ดํฐ ์์ง ๋น๋๊ธฐ ์์
์คํจ. ๊ธฐ๊ด: {}, ํ์ ID: {}", result.connection().getOrganization(), member.getId());
+ }
+ }
+
+ // ์๋ก์ด ๋ฐ์ดํฐ ํํฐ๋ง ๋ฐ ์ ์ฅ
+ int totalNewBankTransactions = 0;
+ if (!allFetchedBankTransactions.isEmpty()) {
+ List newBankTransactions = filterNewBankTransactions(allFetchedBankTransactions);
+ if (!newBankTransactions.isEmpty()) {
+ bankTransactionRepository.bulkInsert(newBankTransactions);
+ totalNewBankTransactions = newBankTransactions.size();
+ }
+ }
+
+ int totalNewCardApprovals = 0;
+ if (!allFetchedCardApprovals.isEmpty()) {
+ List newCardApprovals = filterNewCardApprovals(allFetchedCardApprovals);
+ if (!newCardApprovals.isEmpty()) {
+ cardApprovalRepository.bulkInsert(newCardApprovals);
+ totalNewCardApprovals = newCardApprovals.size();
+ }
+ }
+
+ // JdbcTemplate ์ฌ์ฉ ํ ์์์ฑ ์ปจํ
์คํธ ์ด๊ธฐํ
+ entityManager.flush();
+ entityManager.clear();
+
+ return AssetResDTO.AssetSyncResult.builder()
+ .newBankTransactionCount(totalNewBankTransactions)
+ .newCardApprovalCount(totalNewCardApprovals)
+ .successOrganizations(successOrganizations)
+ .failureOrganizations(failureOrganizations)
+ .fromDate(overallMinDate)
+ .toDate(today)
+ .build();
+ }
+
+ private List filterNewBankTransactions(List allFetched) {
+ if (allFetched.isEmpty()) return List.of();
+
+ LocalDate minDate = allFetched.stream().map(BankTransaction::getTrDate).min(LocalDate::compareTo).orElse(LocalDate.now());
+ List accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().collect(Collectors.toList());
+
+ List existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay());
+
+ Set existingKeys = existingTransactions.stream()
+ .map(tx -> new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), "")))
+ .collect(Collectors.toSet());
+
+ return allFetched.stream()
+ .filter(tx -> !existingKeys.contains(new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), ""))))
+ .collect(Collectors.toList());
+ }
+
+ private List filterNewCardApprovals(List allFetched) {
+ if (allFetched.isEmpty()) return List.of();
+
+ List cards = allFetched.stream().map(CardApproval::getCard).distinct().collect(Collectors.toList());
+ List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().collect(Collectors.toList());
+
+ List existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos);
+
+ Set existingKeys = existingApprovals.stream()
+ .map(ca -> new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))
+ .collect(Collectors.toSet());
+
+ return allFetched.stream()
+ .filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo())))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncFacadeService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncFacadeService.java
new file mode 100644
index 00000000..8cbb15e6
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncFacadeService.java
@@ -0,0 +1,49 @@
+package org.umc.valuedi.domain.asset.service.command;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.umc.valuedi.domain.asset.exception.AssetException;
+import org.umc.valuedi.domain.asset.exception.code.AssetErrorCode;
+import org.umc.valuedi.domain.member.entity.Member;
+import org.umc.valuedi.domain.member.exception.MemberException;
+import org.umc.valuedi.domain.member.exception.code.MemberErrorCode;
+import org.umc.valuedi.domain.member.repository.MemberRepository;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class AssetSyncFacadeService {
+
+ private final MemberRepository memberRepository;
+ private final AssetSyncProcessor assetSyncProcessor; // ์ค์ ๋น๋๊ธฐ ์์
์ ์ํํ ์๋น์ค ์ฃผ์
+
+ private static final long SYNC_COOL_DOWN_MINUTES = 10;
+
+ /**
+ * ๋๊ธฐํ ์์ฒญ์ ์ ์ํ๊ณ , ์ค์ ์์
์ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๋๊น๋๋ค.
+ */
+ public void refreshAssetSync(Long memberId) {
+ Member member = memberRepository.findById(memberId)
+ .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
+
+ validateSyncCoolDown(member);
+
+ // ์ค์ ๋๊ธฐํ ํ๋ก์ธ์ค๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ํธ์ถ
+ assetSyncProcessor.runSyncProcess(member);
+ }
+
+ private void validateSyncCoolDown(Member member) {
+ if (member.getLastSyncedAt() == null) {
+ return;
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ Duration duration = Duration.between(member.getLastSyncedAt(), now);
+
+ if (duration.toMinutes() < SYNC_COOL_DOWN_MINUTES) {
+ throw new AssetException(AssetErrorCode.SYNC_COOL_DOWN);
+ }
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java
new file mode 100644
index 00000000..cc50e8bc
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java
@@ -0,0 +1,50 @@
+package org.umc.valuedi.domain.asset.service.command;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
+import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService;
+import org.umc.valuedi.domain.member.entity.Member;
+
+import java.time.LocalDate;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AssetSyncProcessor {
+
+ private final AssetFetchService assetFetchService;
+ private final LedgerSyncService ledgerSyncService;
+
+ /**
+ * ์ค์ ๋๊ธฐํ ๋ก์ง์ ์ํํ๋ ๋น๋๊ธฐ ๋ฉ์๋
+ */
+ @Async("assetFetchExecutor")
+ public void runSyncProcess(Member member) {
+ log.info("์์ฐ ๋๊ธฐํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ ์์ํฉ๋๋ค. ํ์ ID: {}", member.getId());
+ try {
+ // ํธ๋์ญ์
1: ๋ฐ์ดํฐ ์์ง ๋ฐ ์ ์ฅ
+ AssetResDTO.AssetSyncResult syncResult = assetFetchService.fetchAndSaveLatestData(member);
+
+ // ์๋ก ์์ง๋ ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ์๋ง ๊ฐ๊ณ๋ถ ๋๊ธฐํ ๋ก์ง ์ํ
+ boolean hasNewData = syncResult.getNewBankTransactionCount() > 0 || syncResult.getNewCardApprovalCount() > 0;
+ if (hasNewData) {
+ LocalDate fromDate = syncResult.getFromDate();
+ LocalDate toDate = syncResult.getToDate();
+
+ // ํธ๋์ญ์
2: ๊ฐ๊ณ๋ถ ์ฐ๋ ๋ฐ ์ต์ข
์
๋ฐ์ดํธ
+ // Member ๊ฐ์ฒด ๋์ ID๋ฅผ ์ ๋ฌํ์ฌ ์์์ฑ ์ปจํ
์คํธ ๋ด์์ ์กฐํ ๋ฐ ์
๋ฐ์ดํธํ๋๋ก ๋ณ๊ฒฝ
+ ledgerSyncService.syncTransactionsAndUpdateMember(member.getId(), fromDate, toDate);
+ } else {
+ // ์๋ก ์์ง๋ ๋ฐ์ดํฐ๊ฐ ์์ด๋ ๋๊ธฐํ ์๊ฐ์ ๊ฐฑ์
+ ledgerSyncService.updateMemberLastSyncedAt(member.getId());
+ }
+ log.info("์์ฐ ๋๊ธฐํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃํ์ต๋๋ค. ํ์ ID: {}", member.getId());
+ } catch (Exception e) {
+ // ๋น๋๊ธฐ ์์
๋ด์์ ๋ฐ์ํ๋ ๋ชจ๋ ์์ธ๋ฅผ ๋ก๊น
+ log.error("์์ฐ ๋๊ธฐํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ค ์ค๋ฅ ๋ฐ์. ํ์ ID: {}", member.getId(), e);
+ }
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
index bb847003..0d58ea3b 100644
--- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java
@@ -1,7 +1,5 @@
package org.umc.valuedi.domain.asset.service.command;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +18,6 @@
import org.umc.valuedi.domain.connection.enums.BusinessType;
import org.umc.valuedi.global.external.codef.service.CodefAssetService;
-import java.util.ArrayList;
import java.util.List;
@Slf4j
@@ -134,71 +131,24 @@ private void syncCardApprovals(CodefConnection connection) {
log.warn("์ฐ๋๋ ์นด๋๊ฐ ์์ด ์น์ธ๋ด์ญ ๋๊ธฐํ๋ฅผ ๊ฑด๋๋๋๋ค.");
return;
}
+ try {
+
+ connection.getCardList().clear();
+ connection.getCardList().addAll(cards);
+ } catch (Exception e) {
+ log.warn("Connection ๊ฐ์ฒด์ ์นด๋ ๋ฆฌ์คํธ ๊ฐฑ์ ์ค ์ค๋ฅ (๋ฌด์ํ๊ณ ์งํ): {}", e.getMessage());
+ }
// ์ ์ฒด ์น์ธ ๋ด์ญ ์กฐํ (API)
+ // CodefAssetService ๋ด๋ถ์์ CodefAssetConverter๋ฅผ ํตํด ๋งค์นญ๊น์ง ์๋ฃ๋ ๋ฆฌ์คํธ ๋ฐํ
List approvals = codefAssetService.getCardApprovals(connection);
if (approvals.isEmpty()) {
log.info("์กฐํ๋ ์น์ธ๋ด์ญ์ด ์์ต๋๋ค.");
return;
}
- List matchedApprovals = new ArrayList<>();
-
- // ๋งค์นญ ๋ก์ง
- for (CardApproval approval : approvals) {
- String resCardNo = extractResCardNo(approval);
- if (resCardNo == null) {
- log.warn("์น์ธ๋ด์ญ์์ ์นด๋๋ฒํธ๋ฅผ ์ฐพ์ ์ ์์ด ์คํตํฉ๋๋ค. ApprovalNo: {}", approval.getApprovalNo());
- continue;
- }
-
- Card matchedCard = findMatchingCard(cards, resCardNo);
- if (matchedCard != null) {
- approval.assignCard(matchedCard);
- matchedApprovals.add(approval);
- } else {
- log.warn("์น์ธ๋ด์ญ์ ์นด๋๋ฒํธ({})์ ์ผ์นํ๋ ์นด๋๋ฅผ ์ฐพ์ ์ ์์ด ์คํตํฉ๋๋ค.", resCardNo);
- }
- }
-
// ์ ์ฅ
- if (!matchedApprovals.isEmpty()) {
- cardApprovalRepository.bulkInsert(matchedApprovals);
- log.info("์นด๋ ์น์ธ๋ด์ญ Bulk Insert ์๋ฃ - {}๊ฑด ์ ์ฅ (์ด ์กฐํ: {}๊ฑด)", matchedApprovals.size(), approvals.size());
- }
- }
-
- private String extractResCardNo(CardApproval approval) {
- try {
- JsonNode root = objectMapper.readTree(approval.getRawJson());
- if (root.has("resCardNo")) {
- return root.get("resCardNo").asText();
- }
- } catch (JsonProcessingException e) {
- log.error("JSON ํ์ฑ ์คํจ: {}", e.getMessage());
- }
- return null;
- }
-
- private Card findMatchingCard(List cards, String resCardNo) {
- // 1์์: ์ ํํ ์ผ์น
- for (Card card : cards) {
- if (resCardNo.equals(card.getCardNoMasked())) {
- return card;
- }
- }
-
- // 2์์: ๋ค 4์๋ฆฌ ์ผ์น
- if (resCardNo.length() >= 4) {
- String last4 = resCardNo.substring(resCardNo.length() - 4);
- for (Card card : cards) {
- String cardNo = card.getCardNoMasked();
- if (cardNo != null && cardNo.length() >= 4 && cardNo.endsWith(last4)) {
- return card;
- }
- }
- }
-
- return null;
+ cardApprovalRepository.bulkInsert(approvals);
+ log.info("์นด๋ ์น์ธ๋ด์ญ Bulk Insert ์๋ฃ - {}๊ฑด", approvals.size());
}
}
diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java
new file mode 100644
index 00000000..be48490e
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java
@@ -0,0 +1,108 @@
+package org.umc.valuedi.domain.asset.service.command.worker;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.umc.valuedi.domain.asset.entity.BankAccount;
+import org.umc.valuedi.domain.asset.entity.BankTransaction;
+import org.umc.valuedi.domain.asset.entity.Card;
+import org.umc.valuedi.domain.asset.entity.CardApproval;
+import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
+import org.umc.valuedi.domain.asset.repository.card.card.CardRepository;
+import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
+import org.umc.valuedi.domain.connection.entity.CodefConnection;
+import org.umc.valuedi.domain.connection.enums.BusinessType;
+import org.umc.valuedi.domain.member.entity.Member;
+import org.umc.valuedi.global.external.codef.service.CodefAssetService;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AssetFetchWorker {
+
+ private final CodefAssetService codefAssetService;
+ private final BankTransactionRepository bankTransactionRepository;
+ private final CardApprovalRepository cardApprovalRepository;
+ private final CardRepository cardRepository;
+
+ public record FetchResult(
+ CodefConnection connection,
+ LocalDate startDate,
+ List bankTransactions,
+ List cardApprovals,
+ boolean isSuccess
+ ) {}
+
+ @Async("assetFetchExecutor")
+ public CompletableFuture fetchAndConvertData(CodefConnection connection, Member member) {
+ LocalDate today = LocalDate.now();
+ LocalDate defaultStartDate = today.minusMonths(3); // ๊ธฐ๋ณธ ์์์ผ์ 3๊ฐ์ ์ ์ผ๋ก ์ค์
+ LocalDate overallStartDate = today; // ์ ์ฒด ๊ธฐ๊ด์ ์์์ผ ๊ธฐ๋ก์ฉ
+
+ try {
+ if (connection.getBusinessType() == BusinessType.BK) {
+ List accounts = connection.getBankAccountList();
+ List allTransactions = new ArrayList<>();
+
+ for (BankAccount account : accounts) {
+ try {
+ // ๊ณ์ข๋ณ๋ก ์ต์ ์ startDate ๊ณ์ฐ (์์ผ๋ฉด 3๊ฐ์ ์ )
+ LocalDate accountStartDate = bankTransactionRepository.findLatestTransactionDateByAccount(account)
+ .orElse(defaultStartDate);
+
+ // ๊ณ์ข๋ณ๋ก API ํธ์ถ
+ List fetched = codefAssetService.getBankTransactions(connection, account, accountStartDate, today);
+ allTransactions.addAll(fetched);
+
+ // ๊ฐ์ฅ ์ด๋ฅธ ์์์ผ์ ๊ธฐ๋ก
+ if (accountStartDate.isBefore(overallStartDate)) {
+ overallStartDate = accountStartDate;
+ }
+ } catch (Exception e) {
+ log.warn("[ASSET-FETCH-WORKER] ์ํ ๊ณ์ข ๊ฑฐ๋๋ด์ญ ์์ง ์ค ์ค๋ฅ ๋ฐ์. ๊ณ์ข: {}, ๊ธฐ๊ด: {}, ํ์ ID: {}",
+ account.getAccountDisplay(), connection.getOrganization(), member.getId(), e);
+ }
+ }
+ return CompletableFuture.completedFuture(new FetchResult(connection, overallStartDate, allTransactions, Collections.emptyList(), true));
+
+ } else if (connection.getBusinessType() == BusinessType.CD) {
+ // 1. ์นด๋ ๋ชฉ๋ก ๋จผ์ ๋๊ธฐํ (ํ์)
+ List fetchedCards = codefAssetService.getCards(connection);
+ List savedCards = Collections.emptyList();
+ if (!fetchedCards.isEmpty()) {
+ List existingCards = cardRepository.findAllByCodefConnection(connection);
+
+ List cardsToSave = fetchedCards.stream().map(newCard -> {
+ return existingCards.stream()
+ .filter(oldCard -> oldCard.getCardNoMasked().equals(newCard.getCardNoMasked()))
+ .findFirst()
+ .map(oldCard -> oldCard) // ์ ๋ณด ๊ฐฑ์ ํ์ ์ ์ฌ๊ธฐ์ ์ํ
+ .orElse(newCard);
+ }).toList();
+
+ // saveAll์ ๊ฒฐ๊ณผ(์์ํ๋ ๊ฐ์ฒด๋ค) ์ ์ฅ
+ savedCards = cardRepository.saveAll(cardsToSave);
+ }
+
+ LocalDate cardStartDate = cardApprovalRepository.findLatestApprovalDateByMember(member)
+ .orElse(defaultStartDate);
+
+ List fetched = codefAssetService.getCardApprovals(connection, savedCards, cardStartDate, today);
+
+ return CompletableFuture.completedFuture(new FetchResult(connection, cardStartDate, Collections.emptyList(), fetched, true));
+ }
+ } catch (Exception e) {
+ log.error("[ASSET-FETCH-WORKER] ์์ฐ ๋ฐ์ดํฐ ์์ง ๋น๋๊ธฐ ์์
์ค ์์ธกํ์ง ๋ชปํ ์ค๋ฅ ๋ฐ์. ๊ธฐ๊ด: {}, ํ์ ID: {}",
+ connection.getOrganization(), member.getId(), e);
+ return CompletableFuture.completedFuture(new FetchResult(connection, defaultStartDate, Collections.emptyList(), Collections.emptyList(), false));
+ }
+ return CompletableFuture.completedFuture(new FetchResult(connection, defaultStartDate, Collections.emptyList(), Collections.emptyList(), true));
+ }
+}
diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
index e21d2f08..ff718ddf 100644
--- a/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
+++ b/src/main/java/org/umc/valuedi/domain/connection/service/ConnectionEventListener.java
@@ -16,7 +16,7 @@ public class ConnectionEventListener {
private final AssetSyncService assetSyncService;
- @Async("assetSyncExecutor")
+ @Async("assetFetchExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleConnectionSuccess(ConnectionSuccessEvent event) {
log.info("๊ธ์ต์ฌ ์ฐ๋ ์ฑ๊ณต ์ด๋ฒคํธ ์์ - Connection ID: {}, Organization: {}",
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/entity/LedgerEntry.java b/src/main/java/org/umc/valuedi/domain/ledger/entity/LedgerEntry.java
index 3c8592f0..9724958e 100644
--- a/src/main/java/org/umc/valuedi/domain/ledger/entity/LedgerEntry.java
+++ b/src/main/java/org/umc/valuedi/domain/ledger/entity/LedgerEntry.java
@@ -6,6 +6,7 @@
import org.umc.valuedi.domain.asset.entity.CardApproval;
import org.umc.valuedi.domain.ledger.enums.TransactionType;
import org.umc.valuedi.domain.member.entity.Member;
+import org.umc.valuedi.global.entity.BaseEntity;
import java.time.LocalDateTime;
@@ -20,7 +21,7 @@
@Index(name = "uq_ledger_entry_bank_transaction_id", columnList = "bank_transaction_id", unique = true),
@Index(name = "uq_ledger_entry_card_approval_id", columnList = "card_approval_id", unique = true)
})
-public class LedgerEntry {
+public class LedgerEntry extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepository.java b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepository.java
index 76d63928..50d637f8 100644
--- a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepository.java
+++ b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepository.java
@@ -1,9 +1,20 @@
package org.umc.valuedi.domain.ledger.repository;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.umc.valuedi.domain.ledger.entity.LedgerEntry;
-public interface LedgerEntryRepository extends JpaRepository {
+import java.util.List;
+import java.util.Set;
+
+public interface LedgerEntryRepository extends JpaRepository, LedgerEntryRepositoryCustom {
boolean existsByBankTransactionId(Long bankTransactionId);
boolean existsByCardApprovalId(Long cardApprovalId);
+
+ @Query("SELECT le.cardApproval.id FROM LedgerEntry le WHERE le.cardApproval.id IN :ids")
+ Set findExistingCardApprovalIds(@Param("ids") List ids);
+
+ @Query("SELECT le.bankTransaction.id FROM LedgerEntry le WHERE le.bankTransaction.id IN :ids")
+ Set findExistingBankTransactionIds(@Param("ids") List ids);
}
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryCustom.java b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryCustom.java
new file mode 100644
index 00000000..3a4e5ea5
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryCustom.java
@@ -0,0 +1,8 @@
+package org.umc.valuedi.domain.ledger.repository;
+
+import org.umc.valuedi.domain.ledger.entity.LedgerEntry;
+import java.util.List;
+
+public interface LedgerEntryRepositoryCustom {
+ void bulkInsert(List entries);
+}
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryImpl.java b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryImpl.java
new file mode 100644
index 00000000..1b07842f
--- /dev/null
+++ b/src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepositoryImpl.java
@@ -0,0 +1,42 @@
+package org.umc.valuedi.domain.ledger.repository;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.umc.valuedi.domain.ledger.entity.LedgerEntry;
+
+import java.sql.PreparedStatement;
+import java.sql.Timestamp;
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class LedgerEntryRepositoryImpl implements LedgerEntryRepositoryCustom {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ @Override
+ public void bulkInsert(List entries) {
+ if (entries == null || entries.isEmpty()) {
+ return;
+ }
+
+ String sql = "INSERT INTO ledger_entry (member_id, category_id, bank_transaction_id, card_approval_id, transaction_at, transaction_type, title, memo, is_user_modified, created_at, updated_at) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";
+
+ jdbcTemplate.batchUpdate(sql,
+ entries,
+ 100,
+ (PreparedStatement ps, LedgerEntry le) -> {
+ ps.setLong(1, le.getMember().getId());
+ ps.setLong(2, le.getCategory().getId());
+ ps.setObject(3, le.getBankTransaction() != null ? le.getBankTransaction().getId() : null);
+ ps.setObject(4, le.getCardApproval() != null ? le.getCardApproval().getId() : null);
+ ps.setTimestamp(5, Timestamp.valueOf(le.getTransactionAt()));
+ ps.setString(6, le.getTransactionType().toString());
+ ps.setString(7, le.getTitle());
+ ps.setString(8, le.getMemo());
+ ps.setBoolean(9, le.getIsUserModified());
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
index de790c2a..311834ef 100644
--- a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
+++ b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java
@@ -2,7 +2,6 @@
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
-import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
@@ -27,15 +26,16 @@
import java.time.Duration;
import java.time.LocalDate;
-import java.util.Collections;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@RequiredArgsConstructor
-@Transactional
public class LedgerSyncService {
private final LedgerEntryRepository ledgerEntryRepository;
@@ -65,247 +65,161 @@ public void init() {
refreshKeywordCache();
}
- // ์ฃผ๊ธฐ์ ์ผ๋ก ์บ์ ๊ฐฑ์ ์ด ํ์ํ๋ค๋ฉด ๋ณ๋ ์ค์ผ์ค๋ฌ ์ฌ์ฉ ๊ฐ๋ฅ
public void refreshKeywordCache() {
List keywords = categoryKeywordRepository.findAllWithCategory();
- Map newCache = keywords.stream()
- .collect(Collectors.toMap(CategoryKeyword::getKeyword, CategoryKeyword::getCategory, (existing, replacement) -> existing));
- this.keywordCache = Collections.unmodifiableMap(newCache);
+ this.keywordCache = keywords.stream().collect(Collectors.toMap(CategoryKeyword::getKeyword, CategoryKeyword::getCategory, (e, r) -> e));
}
- public void syncTransactions(Long memberId, LedgerSyncRequest request) {
- // ์์ฒญ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ
- if (ObjectUtils.isEmpty(request.getYearMonth()) && ObjectUtils.isEmpty(request.getFromDate()) || ObjectUtils.isEmpty(request.getToDate())) {
- throw new LedgerException(LedgerErrorCode.INVALID_SYNC_REQUEST);
- }
+ @Transactional
+ public void syncTransactionsAndUpdateMember(Long memberId, LocalDate from, LocalDate to) {
+ Member member = memberRepository.findById(memberId)
+ .orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
+ syncTransactions(member, from, to);
+ member.updateLastSyncedAt();
+ }
+ @Transactional
+ public void updateMemberLastSyncedAt(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
+ member.updateLastSyncedAt();
+ }
+
+ @Transactional
+ public void syncTransactions(Long memberId, LedgerSyncRequest request) {
+ Member member = memberRepository.findById(memberId).orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
+ LocalDate from = request.getFromDate();
+ LocalDate to = request.getToDate();
+ syncTransactions(member, from, to);
+ }
+
+ // ํธ๋์ญ์
์ด๋
ธํ
์ด์
์ ๊ฑฐ (์์ ๋ฉ์๋์์ ๊ด๋ฆฌ)
+ public void syncTransactions(Member member, LocalDate from, LocalDate to) {
+ if (to.isBefore(from)) {
+ throw new LedgerException(LedgerErrorCode.INVALID_DATE_RANGE);
+ }
Category defaultCategory = categoryRepository.findByCode("ETC")
.orElseThrow(() -> new LedgerException(LedgerErrorCode.CATEGORY_NOT_FOUND));
Category transferCategory = categoryRepository.findByCode("TRANSFER")
.orElseThrow(() -> new LedgerException(LedgerErrorCode.CATEGORY_NOT_FOUND));
- // 2. ๋ ์ง ๋ฒ์ ์ค์ ๋ก์ง ๊ฐ์
- LocalDate from;
- LocalDate to;
-
- if (ObjectUtils.isNotEmpty(request.getYearMonth())) {
- // Case 1: yearMonth๊ฐ ์๋ ๊ฒฝ์ฐ -> ํด๋น ์ ์ ์ฒด (1์ผ ~ ๋ง์ผ)
- from = request.getYearMonth().atDay(1);
- to = request.getYearMonth().atEndOfMonth();
- } else {
- // Case 2: fromDate๊ฐ ์๋ ๊ฒฝ์ฐ
- from = request.getFromDate();
- if (ObjectUtils.isNotEmpty(request.getToDate())) {
- // Case 2-1: toDate๋ ์๋ ๊ฒฝ์ฐ -> fromDate ~ toDate
- to = request.getToDate();
- } else {
- // Case 2-2: toDate๊ฐ ์๋ ๊ฒฝ์ฐ -> fromDate ~ ์ค๋(ํ์ฌ)
- to = LocalDate.now();
- }
- }
-
- // ์ ํจ์ฑ ๊ฒ์ฌ: ์ข
๋ฃ์ผ์ด ์์์ผ๋ณด๋ค ์์๋ฉด ์๋จ
- if (to.isBefore(from)) {
- throw new LedgerException(LedgerErrorCode.INVALID_DATE_RANGE); // ์๋ฌ ์ฝ๋ ์ถ๊ฐ ํ์
- }
-
- // ์นด๋ ์น์ธ ๋ด์ญ ๋ฏธ๋ฆฌ ์กฐํ (๋งค์นญ ์ค์ฐจ ๊ณ ๋ ค +-1์ผ ๋ฒํผ)
List cards = cardApprovalRepository.findByUsedDateBetween(from.minusDays(1), to.plusDays(1));
- // ์นด๋ ์น์ธ ๋ด์ญ ๋๊ธฐํ (์กฐํ๋ ๋ฆฌ์คํธ ์ ๋ฌ)
- syncCardApprovals(member, from, to, defaultCategory);
+ List allNewEntries = new ArrayList<>();
+ syncCardApprovals(member, from, to, defaultCategory, allNewEntries);
+ syncBankTransactions(member, from, to, cards, defaultCategory, transferCategory, allNewEntries);
- // ์ํ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ (์กฐํ๋ ์นด๋ ๋ฆฌ์คํธ ์ ๋ฌํ์ฌ ์ค๋ณต ๋งค์นญ)
- syncBankTransactions(member, from, to, cards, defaultCategory, transferCategory);
+ if (!allNewEntries.isEmpty()) {
+ ledgerEntryRepository.bulkInsert(allNewEntries);
+ }
}
- private void syncCardApprovals(Member member, LocalDate from, LocalDate to, Category defaultCategory) {
+ private void syncCardApprovals(Member member, LocalDate from, LocalDate to, Category defaultCategory, List allNewEntries) {
List cards = cardApprovalRepository.findByUsedDateBetween(from, to);
+ if (cards.isEmpty()) return;
- for (CardApproval ca : cards) {
- if (ledgerEntryRepository.existsByCardApprovalId(ca.getId())) continue;
- if (ObjectUtils.isEmpty(ca.getUsedDatetime())) continue;
-
- String merchantName = ca.getMerchantName();
- String merchantType = ca.getMerchantType();
-
- Category category = null;
- TransactionType transactionType;
+ // ID ๋ชฉ๋ก์ ์ถ์ถ
+ List cardApprovalIds = cards.stream().map(CardApproval::getId).collect(Collectors.toList());
- // ์
์ข
(merchantType)์ผ๋ก ๋จผ์ ๋งคํ ์๋
- if (!ObjectUtils.isEmpty(merchantType)) {
- category = mapCategoryByKeyword(merchantType, null);
- }
+ // ์ด๋ฏธ ์กด์ฌํ๋ LedgerEntry์ CardApproval ID๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์กฐํ
+ Set existingIds = ledgerEntryRepository.findExistingCardApprovalIds(cardApprovalIds);
- // ์
์ข
๋งคํ ์คํจ ์ ๊ฐ๋งน์ ๋ช
(merchantName)์ผ๋ก ๋งคํ ์๋
- if (ObjectUtils.isEmpty(category) && !ObjectUtils.isEmpty(merchantName)) {
- category = mapCategoryByKeyword(merchantName, defaultCategory);
- }
+ for (CardApproval ca : cards) {
+ // DB ์ฟผ๋ฆฌ ๋์ ๋ฉ๋ชจ๋ฆฌ์ Set์์ ํ์ธ
+ if (existingIds.contains(ca.getId())) continue;
+ if (ca.getUsedDatetime() == null) continue;
- // ๋ชจ๋ ๋งคํ ์คํจ ์ ๊ธฐ๋ณธ ์นดํ
๊ณ ๋ฆฌ ์ฌ์ฉ
- if (ObjectUtils.isEmpty(category)) {
- category = defaultCategory;
- }
+ String merchantName = ca.getMerchantName();
+ Category category = mapCategoryByKeyword(ca.getMerchantType(), null);
+ if (category == null) category = mapCategoryByKeyword(merchantName, defaultCategory);
+ if (category == null) category = defaultCategory;
- if (ca.getCancelYn().equals(CancelStatus.NORMAL)) {
- transactionType = TransactionType.EXPENSE;
- } else {
- transactionType = TransactionType.INCOME;
- }
+ TransactionType transactionType = ca.getCancelYn() == CancelStatus.NORMAL ? TransactionType.EXPENSE : TransactionType.INCOME;
- LedgerEntry entry = LedgerEntry.builder()
- .member(member)
- .cardApproval(ca)
- .category(category)
- .title(ObjectUtils.isEmpty(merchantName) ? "์นด๋ ์น์ธ" : merchantName)
- .transactionAt(ca.getUsedDatetime())
- .transactionType(transactionType) // transactionType ํ๋ ์ค์
- .build();
- ledgerEntryRepository.save(entry);
+ allNewEntries.add(LedgerEntry.builder()
+ .member(member).cardApproval(ca).category(category)
+ .title(merchantName == null || merchantName.isBlank() ? "์นด๋ ์น์ธ" : merchantName)
+ .transactionAt(ca.getUsedDatetime()).transactionType(transactionType).build());
}
}
- public void syncBankTransactions(Member member, LocalDate from, LocalDate to, List cards, Category defaultCategory, Category transferCategory) {
+ private void syncBankTransactions(Member member, LocalDate from, LocalDate to, List cards, Category defaultCategory, Category transferCategory, List allNewEntries) {
List banks = bankTransactionRepository.findByTrDateBetween(from, to);
+ if (banks.isEmpty()) return;
- for (BankTransaction bt : banks) {
- // ์ด๋ฏธ ๋๊ธฐํ๋ ๋ด์ญ์ ์คํต (CardApproval์์ ์ด๋ฏธ ์ฒ๋ฆฌํ์ ๊ฐ๋ฅ์ฑ ํฌํจ)
- if (ledgerEntryRepository.existsByBankTransactionId(bt.getId())) continue;
- // ํ์ ๊ฐ ์ฒดํฌ
- if (ObjectUtils.isEmpty(bt.getTrDatetime())) continue;
+ // ID ๋ชฉ๋ก ์ถ์ถ
+ List bankTransactionIds = banks.stream().map(BankTransaction::getId).collect(Collectors.toList());
- // desc ํ๋ ๊ฒฐํฉ
- String combinedDesc = Stream.of(bt.getDesc2(), bt.getDesc3(), bt.getDesc4())
- .filter(s -> !ObjectUtils.isEmpty(s))
- .collect(Collectors.joining(" "));
+ // ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์กฐํ
+ Set existingIds = ledgerEntryRepository.findExistingBankTransactionIds(bankTransactionIds);
- // ์ค๋ณต ์ ๊ฑฐ ๋ก์ง ๋ณ๊ฒฝ : ํค์๋ ํฌํจ ์ + ์ค์ ๋งค์นญ ์ฑ๊ณต ์์๋ง ์คํต
- if (isDuplicateOfCardApproval(bt, combinedDesc, cards)) continue;
+ for (BankTransaction bt : banks) {
+ // ๋ฉ๋ชจ๋ฆฌ์์ ํ์ธ
+ if (existingIds.contains(bt.getId())) continue;
+ if (bt.getTrDatetime() == null) continue;
+ String combinedDesc = Stream.of(bt.getDesc2(), bt.getDesc3(), bt.getDesc4()).filter(Objects::nonNull).collect(Collectors.joining(" "));
+ if (isDuplicateOfCardApproval(bt, combinedDesc, cards)) continue;
Category category;
TransactionType transactionType;
- String title = ObjectUtils.isEmpty(combinedDesc) ? "์ํ ๊ฑฐ๋" : combinedDesc;
- if (title.length() > 50) {
- title = title.substring(0, 50);
- }
+ String title = combinedDesc.isEmpty() ? "์ํ ๊ฑฐ๋" : combinedDesc;
+ if (title.length() > 50) title = title.substring(0, 50);
if (isCardSettlement(combinedDesc)) {
category = transferCategory;
- transactionType = TransactionType.EXPENSE; // ์นด๋๋๊ธ ์ ์ฐ์ ์ถ๊ธ
+ transactionType = TransactionType.EXPENSE;
} else {
category = mapCategoryByKeyword(combinedDesc, defaultCategory);
- // BankTransaction์ direction์ ๋ฐ๋ฆ
- if (bt.getDirection() == TransactionDirection.IN) {
- transactionType = TransactionType.INCOME;
- } else if (bt.getDirection() == TransactionDirection.OUT) {
- transactionType = TransactionType.EXPENSE;
- } else {
- transactionType = TransactionType.EXPENSE; // ๊ธฐ๋ณธ๊ฐ
- }
+ transactionType = bt.getDirection() == TransactionDirection.IN ? TransactionType.INCOME : TransactionType.EXPENSE;
}
- LedgerEntry entry = LedgerEntry.builder()
- .member(member)
- .bankTransaction(bt)
- .category(category)
- .title(title)
- .transactionAt(bt.getTrDatetime())
- .transactionType(transactionType) // transactionType ํ๋ ์ค์
- .build();
- ledgerEntryRepository.save(entry);
+ allNewEntries.add(LedgerEntry.builder()
+ .member(member).bankTransaction(bt).category(category)
+ .title(title).transactionAt(bt.getTrDatetime()).transactionType(transactionType).build());
}
}
- // --- ํ๋จ ํจ์ ---
private boolean isCardSettlement(String text) {
- if (ObjectUtils.isEmpty(text)) return false;
- return CARD_SETTLEMENT_KEYWORDS.stream().anyMatch(text::contains);
- }
-
- private boolean isDebitCardDuplicate(String text) {
- if (ObjectUtils.isEmpty(text)) return false;
-
- // ์ ์ฐ/์ฒญ๊ตฌ๋ ์ค๋ณต์ด ์๋๋ฏ๋ก, ๋จผ์ ํ์ธํ์ฌ ์ ์ธ
- if (isCardSettlement(text)) {
- return false;
- }
-
- return CARD_PAYMENT_KEYWORDS.stream().anyMatch(text::contains);
+ return text != null && CARD_SETTLEMENT_KEYWORDS.stream().anyMatch(text::contains);
}
private Category mapCategoryByKeyword(String text, Category defaultCategory) {
- if (ObjectUtils.isEmpty(text) || text.isEmpty()) return defaultCategory;
-
- // ์บ์๋ ํค์๋ ๋งต์ ์ํํ๋ฉฐ ํฌํจ ์ฌ๋ถ ํ์ธ
- // (ํค์๋๊ฐ ๋ง์์ง๋ฉด Aho-Corasick ๋ฑ ์๊ณ ๋ฆฌ์ฆ ์ต์ ํ ํ์, ํ์ฌ๋ ๋จ์ ๋ฃจํ)
+ if (text == null || text.isEmpty()) return defaultCategory;
for (Map.Entry entry : keywordCache.entrySet()) {
- if (text.contains(entry.getKey())) {
- return entry.getValue();
- }
+ if (text.contains(entry.getKey())) return entry.getValue();
}
return defaultCategory;
}
- // ์ํ ๊ฑฐ๋๊ฐ ์นด๋ ์น์ธ ๋ด์ญ๊ณผ ์ค๋ณต๋๋์ง ์ ๋ฐํ๊ฒ ํ๋จ
private boolean isDuplicateOfCardApproval(BankTransaction bt, String combinedDesc, List cards) {
- // 1. ์ถ๊ธ(OUT)์ธ ๊ฒฝ์ฐ์๋ง ์ฒดํฌ
- if (bt.getDirection() != TransactionDirection.OUT) return false;
-
- // 2. ์ ์ฐ/์ฒญ๊ตฌ ํค์๋๊ฐ ์์ผ๋ฉด ์ค๋ณต ์๋ (TRANSFER๋ก ์ฒ๋ฆฌ๋จ)
- if (isCardSettlement(combinedDesc)) return false;
-
- // 3. ์นด๋ ๊ฒฐ์ ํ๋ณด ํค์๋๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง ๋งค์นญ ์๋ (์ ํ ์ฌํญ์ด์ง๋ง ์ฑ๋ฅ ์ต์ ํ ์ํด ์ ์ฉ)
- if (!hasCardPaymentKeyword(combinedDesc)) return false;
-
- // 4. ์ค์ ๋งค์นญ ๋ก์ง (๊ธ์ก, ์๊ฐ, ์ํธ๋ช
์ ์ฌ๋)
+ if (bt.getDirection() != TransactionDirection.OUT || isCardSettlement(combinedDesc) || !hasCardPaymentKeyword(combinedDesc)) return false;
return cards.stream().anyMatch(ca -> isMatch(bt, combinedDesc, ca));
}
private boolean isMatch(BankTransaction bt, String bankDesc, CardApproval ca) {
- // 1. ๊ธ์ก ์ผ์น ์ฌ๋ถ (์ ํํ ์ผ์น)
if (bt.getOutAmount().compareTo(ca.getUsedAmount()) != 0) return false;
+ if (Duration.between(bt.getTrDatetime(), ca.getUsedDatetime()).abs().toHours() > 6) return false;
- // 2. ์๊ฐ ๊ทผ์ ์ฌ๋ถ (ยฑ6์๊ฐ ์ด๋ด)
- long hoursDiff = Duration.between(bt.getTrDatetime(), ca.getUsedDatetime()).abs().toHours();
- if (hoursDiff > 6) return false;
-
- // 3. ์ํธ/์ ์ ์ ์ฌ๋ (์ ๊ทํ ํ ํฌํจ ์ฌ๋ถ ํ์ธ)
String normBankDesc = normalizeText(bankDesc);
String normMerchant = normalizeText(ca.getMerchantName());
- // ํ์ชฝ์ด ๋๋ฌด ์งง์ผ๋ฉด ๋งค์นญ ์ํํ๋ฏ๋ก ์คํต (์: 2์ ๋ฏธ๋ง)
if (normBankDesc.length() < 2 || normMerchant.length() < 2) return false;
- // LCS ๊ธธ์ด ๊ณ์ฐ
int lcsLength = getLongestCommonSubstringLength(normBankDesc, normMerchant);
-
-
- // ๊ธฐ์ค: ์งง์ ๋ฌธ์์ด ๊ธธ์ด์ 60% ์ด์ ๊ฒน์น๊ฑฐ๋, 4๊ธ์ ์ด์ ๊ฒน์น๋ฉด ์ผ์น๋ก ํ๋จ
- int minLength = Math.min(normBankDesc.length(), normMerchant.length());
- return lcsLength >= 4 || (double) lcsLength / minLength >= 0.6;
+ return lcsLength >= 4 || (double) lcsLength / Math.min(normBankDesc.length(), normMerchant.length()) >= 0.6;
}
private String normalizeText(String text) {
- if (text == null) return "";
- return text.replaceAll("[^a-zA-Z0-9๊ฐ-ํฃ]", "") // ํน์๋ฌธ์, ๊ณต๋ฐฑ ์ ๊ฑฐ
- .replace("์ฃผ์ํ์ฌ", "")
- .replace("์ ํํ์ฌ", "")
- .replace("์ฒดํฌ", "") // "์ฒดํฌ์ฐ๋ฆฌ" ๋ฑ์์ ์ ๊ฑฐ
- .replace("์นด๋", "") // "์นด๋" ์ ๊ฑฐ
- .toUpperCase();
+ return text == null ? "" : text.replaceAll("[^a-zA-Z0-9๊ฐ-ํฃ]", "").replace("์ฃผ์ํ์ฌ", "").replace("์ ํํ์ฌ", "").replace("์ฒดํฌ", "").replace("์นด๋", "").toUpperCase();
}
- // LCS (Longest Common Substring) ๊ธธ์ด ๊ณ์ฐ
private int getLongestCommonSubstringLength(String s1, String s2) {
int m = s1.length();
int n = s2.length();
int[][] dp = new int[m + 1][n + 1];
int maxLength = 0;
-
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
@@ -319,11 +233,7 @@ private int getLongestCommonSubstringLength(String s1, String s2) {
return maxLength;
}
-
private boolean hasCardPaymentKeyword(String text) {
- if (ObjectUtils.isEmpty(text)) return false;
- return CARD_PAYMENT_KEYWORDS.stream().anyMatch(text::contains);
+ return text != null && CARD_PAYMENT_KEYWORDS.stream().anyMatch(text::contains);
}
-
-
}
diff --git a/src/main/java/org/umc/valuedi/domain/mbti/entity/MbtiTypeInfo.java b/src/main/java/org/umc/valuedi/domain/mbti/entity/MbtiTypeInfo.java
index 27ea7ad4..bd68bf93 100644
--- a/src/main/java/org/umc/valuedi/domain/mbti/entity/MbtiTypeInfo.java
+++ b/src/main/java/org/umc/valuedi/domain/mbti/entity/MbtiTypeInfo.java
@@ -30,7 +30,7 @@ public class MbtiTypeInfo {
private String tagline;
@Lob
- @Column(name = "detail", nullable = false)
+ @Column(name = "detail", nullable = false, columnDefinition = "TEXT")
private String detail;
@Column(name = "warning", nullable = false, length = 500)
diff --git a/src/main/java/org/umc/valuedi/domain/member/entity/Member.java b/src/main/java/org/umc/valuedi/domain/member/entity/Member.java
index 1eac49be..96043fbd 100644
--- a/src/main/java/org/umc/valuedi/domain/member/entity/Member.java
+++ b/src/main/java/org/umc/valuedi/domain/member/entity/Member.java
@@ -76,6 +76,9 @@ public class Member extends BaseEntity {
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
+ @Column(name = "last_synced_at")
+ private LocalDateTime lastSyncedAt;
+
@Builder.Default
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List memberTermsList = new ArrayList<>();
@@ -102,4 +105,8 @@ public void withdraw(WithdrawalReason reason) {
this.passwordHash = null;
this.withdrawalReason = reason;
}
+
+ public void updateLastSyncedAt() {
+ this.lastSyncedAt = LocalDateTime.now();
+ }
}
diff --git a/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java b/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java
index 15c4f9c4..0fb4480d 100644
--- a/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java
+++ b/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java
@@ -23,13 +23,13 @@ public Executor mailExecutor() {
return executor;
}
- @Bean(name = "assetSyncExecutor")
- public Executor assetSyncExecutor() {
+ @Bean(name = "assetFetchExecutor")
+ public Executor assetFetchExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
- executor.setCorePoolSize(5); // ๋์์ ์ฒ๋ฆฌํ ์ฐ๋ ์์
์
- executor.setMaxPoolSize(10);
- executor.setQueueCapacity(20);
- executor.setThreadNamePrefix("AssetSync-");
+ executor.setCorePoolSize(5); // ๊ธฐ๋ณธ ์ค๋ ๋ ์ - ๋์์ ์ฒ๋ฆฌํ ์์
์ (CPU ์ฝ์ด ์์ ๋ง๊ฒ ์กฐ์ )
+ executor.setMaxPoolSize(10); // ์ต๋ ์ค๋ ๋ ์
+ executor.setQueueCapacity(100); // ํ ์ฉ๋
+ executor.setThreadNamePrefix("AssetFetch-");
executor.initialize();
return executor;
}
diff --git a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java
index d765f19b..ac89e614 100644
--- a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java
+++ b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java
@@ -4,8 +4,10 @@
import feign.RequestInterceptor;
import feign.codec.Decoder;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.umc.valuedi.global.external.codef.service.CodefTokenService;
+@Configuration
public class CodefFeignConfig {
@Bean
diff --git a/src/main/java/org/umc/valuedi/global/external/codef/converter/CodefAssetConverter.java b/src/main/java/org/umc/valuedi/global/external/codef/converter/CodefAssetConverter.java
index 245e9fbe..11f2714c 100644
--- a/src/main/java/org/umc/valuedi/global/external/codef/converter/CodefAssetConverter.java
+++ b/src/main/java/org/umc/valuedi/global/external/codef/converter/CodefAssetConverter.java
@@ -21,6 +21,7 @@
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -125,19 +126,31 @@ private Card toCard(CodefAssetResDTO.Card item, CodefConnection connection) {
}
}
- public List toCardApprovalList(List data) {
+ public List toCardApprovalList(List data, List cardList) {
return data.stream()
- .map(this::toCardApproval)
+ .map(item -> toCardApproval(item, cardList))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
- private CardApproval toCardApproval(CodefAssetResDTO.CardApproval item) {
+ private CardApproval toCardApproval(CodefAssetResDTO.CardApproval item, List cardList) {
try {
LocalDate usedDate = parseDate(item.getResUsedDate());
LocalTime usedTime = parseTime(item.getResUsedTime());
+ Card card = findMatchingCard(item.getResCardNo(), cardList);
+ if (card == null) {
+ log.warn("์น์ธ ๋ด์ญ์ ํด๋นํ๋ ์นด๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ์นด๋๋ฒํธ: {}", item.getResCardNo());
+ return null;
+ }
+
+ String merchantType = item.getResMemberStoreType();
+ if (merchantType == null || merchantType.isBlank()) {
+ merchantType = "๊ธฐํ"; // ๊ธฐ๋ณธ๊ฐ ์ค์
+ }
+
return CardApproval.builder()
+ .card(card)
.usedDate(usedDate)
.usedTime(usedTime)
.usedDatetime(LocalDateTime.of(usedDate, usedTime))
@@ -151,7 +164,7 @@ private CardApproval toCardApproval(CodefAssetResDTO.CardApproval item) {
.cancelAmount(parseAmount(item.getResCancelAmount()))
.merchantCorpNo(item.getResMemberStoreCorpNo())
.merchantName(item.getResMemberStoreName())
- .merchantType(item.getResMemberStoreType())
+ .merchantType(merchantType) // ๊ธฐ๋ณธ๊ฐ์ด ์ค์ ๋ ๋ณ์ ์ฌ์ฉ
.merchantNo(item.getResMemberStoreNo())
.commStartDate(parseDate(item.getCommStartDate()).atStartOfDay())
.commEndDate(parseDate(item.getCommEndDate()).atStartOfDay())
@@ -164,6 +177,38 @@ private CardApproval toCardApproval(CodefAssetResDTO.CardApproval item) {
}
}
+ private Card findMatchingCard(String approvalCardNo, List cardList) {
+ if (approvalCardNo == null) {
+ return null;
+ }
+
+ // ์ซ์๋ง ์ถ์ถ
+ String cleanApprovalNo = approvalCardNo.replaceAll("[^0-9]", "");
+ if (cleanApprovalNo.length() < 8) {
+ // ์ซ์๊ฐ ๋๋ฌด ์ ์ผ๋ฉด ๋งค์นญ ๋ถ๊ฐ (์ต์ ์4, ๋ค4 ํ์)
+ return null;
+ }
+
+ String approvalPrefix = cleanApprovalNo.substring(0, 4);
+ String approvalSuffix = cleanApprovalNo.substring(cleanApprovalNo.length() - 4);
+
+ return cardList.stream()
+ .filter(c -> {
+ String cardNo = c.getCardNoMasked();
+ if (cardNo == null) return false;
+
+ String cleanCardNo = cardNo.replaceAll("[^0-9]", "");
+ if (cleanCardNo.length() < 8) return false;
+
+ String cardPrefix = cleanCardNo.substring(0, 4);
+ String cardSuffix = cleanCardNo.substring(cleanCardNo.length() - 4);
+
+ return approvalPrefix.equals(cardPrefix) && approvalSuffix.equals(cardSuffix);
+ })
+ .findFirst()
+ .orElse(null);
+ }
+
private Long parseAmount(String amount) {
if (amount == null || amount.isEmpty()) return 0L;
try {
diff --git a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAssetService.java b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAssetService.java
index b5b5f61c..bf750544 100644
--- a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAssetService.java
+++ b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAssetService.java
@@ -24,6 +24,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
@Slf4j
@Service
@@ -57,15 +59,21 @@ public List getBankAccounts(CodefConnection connection) {
return codefAssetConverter.toBankAccountList(allAccounts, connection);
}
+ // ๊ธฐ์กด ๋ฉ์๋ (3๊ฐ์ ์ ๋ถํฐ ํ์ฌ๊น์ง)
public List getBankTransactions(CodefConnection connection, BankAccount account) {
+ LocalDate now = LocalDate.now();
+ return getBankTransactions(connection, account, now.minusMonths(3), now);
+ }
+
+ // ์ค๋ฒ๋ก๋ฉ๋ ๋ฉ์๋ (์์์ผ, ์ข
๋ฃ์ผ ์ง์ )
+ public List getBankTransactions(CodefConnection connection, BankAccount account, LocalDate startDate, LocalDate endDate) {
Map requestBody = createAssetRequestBody(connection);
String originalAccountNo = encryptUtil.decryptAES(account.getAccountNoEnc());
requestBody.put("account", originalAccountNo);
- LocalDate now = LocalDate.now();
- requestBody.put("startDate", now.minusMonths(3).format(DateTimeFormatter.BASIC_ISO_DATE));
- requestBody.put("endDate", now.format(DateTimeFormatter.BASIC_ISO_DATE));
+ requestBody.put("startDate", startDate.format(DateTimeFormatter.BASIC_ISO_DATE));
+ requestBody.put("endDate", endDate.format(DateTimeFormatter.BASIC_ISO_DATE));
requestBody.put("orderBy", "0");
requestBody.put("inquiryType", "1");
@@ -119,14 +127,26 @@ public List getCards(CodefConnection connection) {
return codefAssetConverter.toCardList(cardList, connection);
}
+ // ๊ธฐ์กด ๋ฉ์๋ (3๊ฐ์ ์ ๋ถํฐ ํ์ฌ๊น์ง)
public List getCardApprovals(CodefConnection connection) {
+ LocalDate now = LocalDate.now();
+ return getCardApprovals(connection, now.minusMonths(3), now);
+ }
+
+ // ์ค๋ฒ๋ก๋ฉ๋ ๋ฉ์๋ (์์์ผ, ์ข
๋ฃ์ผ ์ง์ )
+ public List getCardApprovals(CodefConnection connection, LocalDate startDate, LocalDate endDate) {
+ // ์ด ๋ฉ์๋๋ ์ด์ connection ๋ด๋ถ์ cardList๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, ์ธ๋ถ์์ cardList๊ฐ ์ฑ์์ ธ ์์ด์ผ ํจ.
+ return getCardApprovals(connection, connection.getCardList(), startDate, endDate);
+ }
+
+ // AssetFetchWorker๊ฐ ์ฌ์ฉํ ์๋ก์ด ์ค๋ฒ๋ก๋ฉ ๋ฉ์๋
+ public List getCardApprovals(CodefConnection connection, List cards, LocalDate startDate, LocalDate endDate) {
Map requestBody = new HashMap<>();
requestBody.put("connectedId", connection.getConnectedId());
requestBody.put("organization", connection.getOrganization());
- LocalDate now = LocalDate.now();
- requestBody.put("startDate", now.minusMonths(3).format(DateTimeFormatter.BASIC_ISO_DATE));
- requestBody.put("endDate", now.format(DateTimeFormatter.BASIC_ISO_DATE));
+ requestBody.put("startDate", startDate.format(DateTimeFormatter.BASIC_ISO_DATE));
+ requestBody.put("endDate", endDate.format(DateTimeFormatter.BASIC_ISO_DATE));
requestBody.put("orderBy", "0");
requestBody.put("inquiryType", "1");
requestBody.put("memberStoreInfoType", "1"); // ๊ฐ๋งน์ ์์ธ ์ ๋ณด ์กฐํ ์ต์
@@ -145,7 +165,8 @@ public List getCardApprovals(CodefConnection connection) {
return List.of();
}
- return codefAssetConverter.toCardApprovalList(approvalList);
+ // ์น์ธ ๋ด์ญ์ ์นด๋์ ๋งคํํ๊ธฐ ์ํด ๋ช
์์ ์ผ๋ก ์ ๋ฌ๋ฐ์ ์นด๋ ๋ชฉ๋ก์ ์ฌ์ฉ
+ return codefAssetConverter.toCardApprovalList(approvalList, cards);
}
private Map createAssetRequestBody(CodefConnection connection) {