Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
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.exception.code.AssetSuccessCode;
import org.umc.valuedi.domain.asset.service.query.AssetQueryService;
import org.umc.valuedi.domain.connection.service.query.ConnectionQueryService;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.apiPayload.code.GeneralSuccessCode;
import org.umc.valuedi.global.security.annotation.CurrentMember;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;

@RestController
Expand Down Expand Up @@ -78,4 +78,36 @@ public ApiResponse<AssetResDTO.AssetSummaryCountDTO> getAssetCount(
) {
return ApiResponse.onSuccess(GeneralSuccessCode.OK, assetQueryService.getAssetSummaryCount(memberId));
}

@Override
@GetMapping("/accounts/{accountId}/transactions")
public ApiResponse<AssetResDTO.AssetTransactionResponse> getAccountTransactions(
@PathVariable Long accountId,
@CurrentMember Long memberId,
@RequestParam(required = false) YearMonth yearMonth,
@RequestParam(required = false) LocalDate date,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return ApiResponse.onSuccess(
AssetSuccessCode.ACCOUNT_TRANSACTIONS_FETCHED,
assetQueryService.getAccountTransactions(memberId, accountId, yearMonth, date, page, size)
);
}

@Override
@GetMapping("/cards/{cardId}/transactions")
public ApiResponse<AssetResDTO.AssetTransactionResponse> getCardTransactions(
@PathVariable Long cardId,
@CurrentMember Long memberId,
@RequestParam(required = false) YearMonth yearMonth,
@RequestParam(required = false) LocalDate date,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return ApiResponse.onSuccess(
AssetSuccessCode.CARD_TRANSACTIONS_FETCHED,
assetQueryService.getCardTransactions(memberId, cardId, yearMonth, date, page, size)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.security.annotation.CurrentMember;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;

@Tag(name = "Asset", description = "자산(계좌/카드) 관련 API")
Expand Down Expand Up @@ -319,4 +321,157 @@ ApiResponse<BankResDTO.BankAssetResponse> getAccountsByBank(
)
})
ApiResponse<AssetResDTO.AssetSummaryCountDTO> getAssetCount(@CurrentMember Long memberId);

@Operation(
summary = "특정 계좌 거래내역 조회",
description = """
계좌의 거래내역을 최신순으로 페이징 조회합니다.
- yearMonth, date 미입력 시 전체 내역을 조회합니다.
- transactionType: INCOME(입금), EXPENSE(출금)
- afterBalance: 해당 거래 직후의 잔액
- currentBalance: 계좌의 현재 잔액 (가장 최근 동기화 기준)
"""
)
@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": {
"totalElements": 25,
"page": 0,
"size": 20,
"totalPages": 2,
"organizationCode": "0020",
"assetName": "저축예금",
"assetNumber": "123-456-789012",
"currentBalance": 1250000,
"content": [
{
"transactionAt": "2026-02-13T14:30:00",
"title": "모바일 OOO 토스뱅크",
"amount": 50000,
"transactionType": "INCOME",
"categoryCode": "TRANSFER",
"categoryName": "이체",
"afterBalance": 1250000
}
]
}
}
"""
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "계좌를 찾을 수 없음",
content = @Content(
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "실패 예시",
value = """
{
"isSuccess": false,
"code": "ASSET404_1",
"message": "존재하지 않거나 접근 권한이 없는 계좌입니다.",
"result": null
}
"""
)
)
)
})
ApiResponse<AssetResDTO.AssetTransactionResponse> getAccountTransactions(
@Parameter(description = "계좌 ID", required = true, example = "1") Long accountId,
@CurrentMember Long memberId,
@Parameter(description = "조회 년월 (YYYY-MM)", example = "2026-02") YearMonth yearMonth,
@Parameter(description = "특정 일자 필터 (YYYY-MM-DD)", example = "2026-02-13") LocalDate date,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page,
@Parameter(description = "페이지 크기", example = "20") int size
);

@Operation(
summary = "특정 카드 승인내역 조회",
description = """
카드의 승인내역을 최신순으로 페이징 조회합니다.
- yearMonth, date 미입력 시 전체 내역을 조회합니다.
- transactionType: EXPENSE(일반 결제), INCOME(취소/환불)
- currentBalance, afterBalance는 항상 null입니다.
"""
)
@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_2",
"message": "카드 승인내역 조회에 성공했습니다.",
"result": {
"totalElements": 12,
"page": 0,
"size": 20,
"totalPages": 1,
"organizationCode": "0309",
"assetName": "신한카드 Deep Dream",
"assetNumber": "5336-****-****-5678",
"currentBalance": null,
"content": [
{
"transactionAt": "2026-02-13T18:20:00",
"title": "스시초밥",
"amount": 16500,
"transactionType": "EXPENSE",
"categoryCode": "FOOD",
"categoryName": "식비",
"afterBalance": null
}
]
}
}
"""
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "카드를 찾을 수 없음",
content = @Content(
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "실패 예시",
value = """
{
"isSuccess": false,
"code": "ASSET404_2",
"message": "존재하지 않거나 접근 권한이 없는 카드입니다.",
"result": null
}
"""
)
)
)
})
ApiResponse<AssetResDTO.AssetTransactionResponse> getCardTransactions(
@Parameter(description = "카드 ID", required = true, example = "1") Long cardId,
@CurrentMember Long memberId,
@Parameter(description = "조회 년월 (YYYY-MM)", example = "2026-02") YearMonth yearMonth,
@Parameter(description = "특정 일자 필터 (YYYY-MM-DD)", example = "2026-02-13") LocalDate date,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page,
@Parameter(description = "페이지 크기", example = "20") int size
);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package org.umc.valuedi.domain.asset.converter;

import org.springframework.data.domain.Page;
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.BankTransaction;
import org.umc.valuedi.domain.asset.entity.Card;
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.connection.enums.Organization;
import org.umc.valuedi.domain.goal.entity.Goal;
import org.umc.valuedi.domain.ledger.entity.Category;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class AssetConverter {

private static final int MAX_TITLE_LENGTH = 50;

// 개별 BankAccount 엔티티 -> BankAccountInfo 변환
public static BankResDTO.BankAccountInfo toBankAccountInfo(BankAccount account) {
BankResDTO.GoalInfo goalInfo = null;
Expand Down Expand Up @@ -51,6 +61,7 @@ public static BankResDTO.BankAccountListDTO toBankAccountListDTO(List<BankAccoun
// Card 엔티티 -> CardInfo 변환
public static CardResDTO.CardInfo toCardInfo(Card card) {
return CardResDTO.CardInfo.builder()
.cardId(card.getId())
.cardName(card.getCardName())
.cardNoMasked(card.getCardNoMasked())
.cardType(card.getCardType())
Expand Down Expand Up @@ -119,4 +130,73 @@ public static BankResDTO.BankAssetResponse toBankAssetResponse(String organizati
.goalList(goalList)
.build();
}

public static AssetResDTO.AssetTransactionDetail toBankTransactionDetail(BankTransaction bt, Category category) {
boolean isIncome = bt.getDirection() == TransactionDirection.IN;
long amount = isIncome ? bt.getInAmount() : bt.getOutAmount();

String title = Stream.of(bt.getDesc2(), bt.getDesc3(), bt.getDesc4())
.filter(Objects::nonNull)
.collect(Collectors.joining(" ")).strip();
if (title.isBlank()) title = "은행 거래";
if (title.length() > MAX_TITLE_LENGTH) title = title.substring(0, MAX_TITLE_LENGTH);

return AssetResDTO.AssetTransactionDetail.builder()
.transactionAt(bt.getTrDatetime())
.title(title)
.amount(amount)
.transactionType(isIncome ? "INCOME" : "EXPENSE")
.categoryCode(category != null ? category.getCode() : "ETC")
.categoryName(category != null ? category.getName() : "기타")
.afterBalance(bt.getAfterBalance())
.build();
}

public static AssetResDTO.AssetTransactionDetail toCardTransactionDetail(CardApproval ca, Category category) {
boolean isExpense = ca.getCancelYn() == CancelStatus.NORMAL;
String merchantName = ca.getMerchantName();
String title = (merchantName == null || merchantName.isBlank()) ? "카드 승인" : merchantName;

return AssetResDTO.AssetTransactionDetail.builder()
.transactionAt(ca.getUsedDatetime())
.title(title)
.amount(ca.getUsedAmount())
.transactionType(isExpense ? "EXPENSE" : "INCOME")
.categoryCode(category != null ? category.getCode() : "ETC")
.categoryName(category != null ? category.getName() : "기타")
.afterBalance(null)
.build();
}

public static AssetResDTO.AssetTransactionResponse toAccountTransactionResponse(
BankAccount account, Page<BankTransaction> page, List<AssetResDTO.AssetTransactionDetail> content) {
String orgCode = account.getCodefConnection().getOrganization();
return AssetResDTO.AssetTransactionResponse.builder()
.totalElements(page.getTotalElements())
.page(page.getNumber())
.size(page.getSize())
.totalPages(page.getTotalPages())
.organizationCode(orgCode)
.assetName(account.getAccountName())
.assetNumber(account.getAccountDisplay())
.currentBalance(account.getBalanceAmount())
.content(content)
.build();
}

public static AssetResDTO.AssetTransactionResponse toCardTransactionResponse(
Card cardEntity, Page<CardApproval> page, List<AssetResDTO.AssetTransactionDetail> content) {
String orgCode = cardEntity.getCodefConnection().getOrganization();
return AssetResDTO.AssetTransactionResponse.builder()
.totalElements(page.getTotalElements())
.page(page.getNumber())
.size(page.getSize())
.totalPages(page.getTotalPages())
.organizationCode(orgCode)
.assetName(cardEntity.getCardName())
.assetNumber(cardEntity.getCardNoMasked())
.currentBalance(null)
.content(content)
.build();
}
}
Loading