Skip to content

Commit 293db2b

Browse files
authored
[FEATURE/#54] 전체 자산 동기화 API 구현 (#70)
## 🔗 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) <!-- API 테스트 스크린샷 첨부 --> <img width="1949" height="1216" alt="image" src="https://github.com/user-attachments/assets/890e548e-8e34-429c-9ce6-0c0c970b14d0" /> ## ✅ Checklist - [X] API 테스트 완료 - [X] 테스트 결과 사진 첨부 - [X] 빌드 성공 확인 (./gradlew build)
1 parent fc7ee86 commit 293db2b

29 files changed

Lines changed: 776 additions & 256 deletions

src/main/java/org/umc/valuedi/domain/asset/controller/AssetController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import lombok.RequiredArgsConstructor;
55
import org.springframework.web.bind.annotation.GetMapping;
66
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.PostMapping;
78
import org.springframework.web.bind.annotation.RequestMapping;
89
import org.springframework.web.bind.annotation.RestController;
10+
import org.umc.valuedi.domain.asset.exception.code.AssetSuccessCode;
911
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
1012
import org.umc.valuedi.domain.asset.dto.res.BankResDTO;
1113
import org.umc.valuedi.domain.asset.dto.res.CardResDTO;
1214
import org.umc.valuedi.domain.asset.service.query.AssetQueryService;
15+
import org.umc.valuedi.domain.asset.service.command.AssetSyncFacadeService;
1316
import org.umc.valuedi.domain.connection.service.ConnectionQueryService;
1417
import org.umc.valuedi.global.apiPayload.ApiResponse;
1518
import org.umc.valuedi.global.apiPayload.code.GeneralSuccessCode;
@@ -25,6 +28,7 @@ public class AssetController implements AssetControllerDocs {
2528

2629
private final ConnectionQueryService connectionQueryService;
2730
private final AssetQueryService assetQueryService;
31+
private final AssetSyncFacadeService assetSyncFacadeService;
2832

2933

3034
@GetMapping("/cards")
@@ -79,4 +83,12 @@ public ApiResponse<AssetResDTO.AssetSummaryCountDTO> getAssetCount(
7983
) {
8084
return ApiResponse.onSuccess(GeneralSuccessCode.OK, assetQueryService.getAssetSummaryCount(memberId));
8185
}
86+
87+
@PostMapping("/sync/refresh")
88+
public ApiResponse<Void> refreshAssetSync(
89+
@CurrentMember Long memberId
90+
) {
91+
assetSyncFacadeService.refreshAssetSync(memberId);
92+
return ApiResponse.onSuccess(AssetSuccessCode.SYNC_REQUEST_SUCCESS, null);
93+
}
8294
}

src/main/java/org/umc/valuedi/domain/asset/controller/AssetControllerDocs.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,52 @@ ApiResponse<BankResDTO.BankAssetResponse> getAccountsByBank(
319319
)
320320
})
321321
ApiResponse<AssetResDTO.AssetSummaryCountDTO> getAssetCount(@CurrentMember Long memberId);
322+
323+
@Operation(
324+
summary = "전체 자산 새로고침(동기화) 요청 API",
325+
description = """
326+
사용자가 연동한 모든 금융사의 최신 거래내역 수집을 **백그라운드에서 시작**하도록 요청합니다.
327+
- **즉시 응답**: API는 동기화 작업을 백그라운드로 넘기고 즉시 '요청 성공' 응답을 반환합니다.
328+
- **결과 확인**: 실제 동기화 결과는 잠시 후 자산 관련 다른 API를 통해 확인해야 합니다.
329+
- **10분 쿨타임**: 마지막 동기화 시간으로부터 10분 이내에는 재호출할 수 없습니다.
330+
"""
331+
)
332+
@ApiResponses({
333+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
334+
responseCode = "200",
335+
description = "성공 - 동기화 작업이 성공적으로 시작됨",
336+
content = @Content(
337+
schema = @Schema(implementation = ApiResponse.class),
338+
examples = @ExampleObject(
339+
name = "성공 예시",
340+
value = """
341+
{
342+
"isSuccess": true,
343+
"code": "ASSET200_1",
344+
"message": "자산 동기화 요청이 성공적으로 접수되었습니다. 잠시 후 데이터를 확인해주세요.",
345+
"result": null
346+
}
347+
"""
348+
)
349+
)
350+
),
351+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
352+
responseCode = "429",
353+
description = "실패 - 10분 쿨타임 제한",
354+
content = @Content(
355+
schema = @Schema(implementation = ApiResponse.class),
356+
examples = @ExampleObject(
357+
name = "쿨타임 실패 예시",
358+
value = """
359+
{
360+
"isSuccess": false,
361+
"code": "ASSET429_1",
362+
"message": "자산 동기화는 10분에 한 번만 요청할 수 있습니다."
363+
}
364+
"""
365+
)
366+
)
367+
)
368+
})
369+
ApiResponse<Void> refreshAssetSync(@Parameter(hidden = true) @CurrentMember Long memberId);
322370
}

src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import lombok.Getter;
77
import lombok.NoArgsConstructor;
88

9+
import java.time.LocalDate;
10+
import java.util.List;
11+
912
@Schema(description = "자산 통합 응답 DTO")
1013
public class AssetResDTO {
1114

@@ -24,4 +27,28 @@ public static class AssetSummaryCountDTO {
2427
@Schema(description = "총 연동 자산 수 (계좌 + 카드)", example = "8")
2528
private Long totalAssetCount;
2629
}
27-
}
30+
31+
@Getter
32+
@AllArgsConstructor
33+
@Builder
34+
@Schema(description = "AssetFetchService의 내부 처리 결과 DTO (API 응답으로 직접 사용되지 않음)")
35+
public static class AssetSyncResult {
36+
@Schema(description = "새로 수집된 은행 거래내역 수")
37+
private int newBankTransactionCount;
38+
39+
@Schema(description = "새로 수집된 카드 승인내역 수")
40+
private int newCardApprovalCount;
41+
42+
@Schema(description = "데이터 수집에 성공한 기관 목록")
43+
private List<String> successOrganizations;
44+
45+
@Schema(description = "데이터 수집에 실패한 기관 목록")
46+
private List<String> failureOrganizations;
47+
48+
@Schema(description = "가계부 동기화에 사용될 조회 시작일")
49+
private LocalDate fromDate;
50+
51+
@Schema(description = "가계부 동기화에 사용될 조회 종료일")
52+
private LocalDate toDate;
53+
}
54+
}

src/main/java/org/umc/valuedi/domain/asset/entity/BankTransaction.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@
1717
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1818
@AllArgsConstructor(access = AccessLevel.PRIVATE)
1919
@Getter
20-
@Table(name = "bank_transaction")
20+
@Table(
21+
name = "bank_transaction",
22+
uniqueConstraints = {
23+
@UniqueConstraint(
24+
name = "uk_bank_transaction_identity",
25+
columnNames = {"bank_account_id", "tr_datetime", "in_amount", "out_amount", "desc3"}
26+
)
27+
}
28+
)
2129
public class BankTransaction extends BaseEntity {
2230
@Id
2331
@GeneratedValue(strategy = GenerationType.IDENTITY)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.umc.valuedi.domain.asset.exception;
2+
3+
import org.umc.valuedi.global.apiPayload.code.BaseErrorCode;
4+
import org.umc.valuedi.global.apiPayload.exception.GeneralException;
5+
6+
public class AssetException extends GeneralException {
7+
public AssetException(BaseErrorCode code) {
8+
super(code);
9+
}
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.umc.valuedi.domain.asset.exception.code;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
import org.umc.valuedi.global.apiPayload.code.BaseErrorCode;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public enum AssetErrorCode implements BaseErrorCode {
11+
12+
SYNC_COOL_DOWN(HttpStatus.TOO_MANY_REQUESTS, "ASSET429_1", "전체 동기화는 10분에 한 번만 요청할 수 있습니다."),
13+
;
14+
15+
private final HttpStatus status;
16+
private final String code;
17+
private final String message;
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.umc.valuedi.domain.asset.exception.code;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
import org.umc.valuedi.global.apiPayload.code.BaseSuccessCode;
7+
8+
9+
@Getter
10+
@AllArgsConstructor
11+
public enum AssetSuccessCode implements BaseSuccessCode {
12+
13+
SYNC_REQUEST_SUCCESS(HttpStatus.OK, "ASSET200_1", "자산 동기화 요청이 성공적으로 접수되었습니다. 잠시 후 데이터를 확인해주세요."),
14+
;
15+
16+
private final HttpStatus status;
17+
private final String code;
18+
private final String message;
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package org.umc.valuedi.domain.asset.repository.bank.bankTransaction;
22

33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Query;
5+
import org.springframework.data.repository.query.Param;
6+
import org.umc.valuedi.domain.asset.entity.BankAccount;
47
import org.umc.valuedi.domain.asset.entity.BankTransaction;
58

69
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
711
import java.util.List;
12+
import java.util.Optional;
813

914
public interface BankTransactionRepository extends JpaRepository<BankTransaction, Long>, BankTransactionRepositoryCustom {
1015
List<BankTransaction> findByTrDateBetween(LocalDate fromDate, LocalDate toDate);
16+
17+
@Query("SELECT MAX(bt.trDate) FROM BankTransaction bt WHERE bt.bankAccount = :account")
18+
Optional<LocalDate> findLatestTransactionDateByAccount(@Param("account") BankAccount account);
19+
20+
List<BankTransaction> findByBankAccountInAndTrDatetimeAfter(List<BankAccount> accounts, LocalDateTime startTime);
1121
}

src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepositoryImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33
import lombok.RequiredArgsConstructor;
44
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
55
import org.springframework.jdbc.core.JdbcTemplate;
6+
import org.springframework.stereotype.Repository;
67
import org.umc.valuedi.domain.asset.entity.BankTransaction;
78

89
import java.sql.PreparedStatement;
910
import java.sql.SQLException;
1011
import java.sql.Timestamp;
1112
import java.util.List;
1213

14+
@Repository
1315
@RequiredArgsConstructor
1416
public class BankTransactionRepositoryImpl implements BankTransactionRepositoryCustom {
1517

1618
private final JdbcTemplate jdbcTemplate;
1719

1820
@Override
1921
public void bulkInsert(List<BankTransaction> transactions) {
20-
if (transactions.isEmpty()) {
22+
if (transactions == null || transactions.isEmpty()) {
2123
return;
2224
}
2325

src/main/java/org/umc/valuedi/domain/asset/repository/card/card/CardRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ public interface CardRepository extends JpaRepository<Card, Long> {
3232
"AND c.isActive = true " +
3333
"ORDER BY c.createdAt DESC")
3434
List<Card> findAllByMemberId(@Param("memberId") Long memberId);
35+
36+
List<Card> findAllByCodefConnection(CodefConnection connection);
3537
}

0 commit comments

Comments
 (0)