Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
19ed21c
0001
earlydreamer Dec 23, 2025
6426346
0002
earlydreamer Dec 23, 2025
d2d4db3
0003
earlydreamer Dec 23, 2025
1abcb53
private 변수로 수정
earlydreamer Dec 23, 2025
1dce73a
0004
earlydreamer Dec 23, 2025
6107f65
0005
earlydreamer Dec 23, 2025
5bc68bd
0006
earlydreamer Dec 23, 2025
a1a8a5d
0007
earlydreamer Dec 23, 2025
25df26c
0008
earlydreamer Dec 24, 2025
e188bf2
0009
earlydreamer Dec 24, 2025
1e5bebe
0010
earlydreamer Dec 24, 2025
363fc26
0011
earlydreamer Dec 24, 2025
378c6f8
0012
earlydreamer Dec 24, 2025
0e0cb55
0013
earlydreamer Dec 24, 2025
319b9e2
0014
earlydreamer Dec 24, 2025
3b7ad4c
0015
earlydreamer Dec 24, 2025
2f0586d
0016
earlydreamer Dec 24, 2025
7c209ac
0017
earlydreamer Dec 24, 2025
951614f
0018
earlydreamer Dec 26, 2025
bf3f67c
0019
earlydreamer Dec 26, 2025
6289fa2
0020
earlydreamer Dec 26, 2025
730a7b0
0020 : 이벤트 발행 방식 리팩토링
earlydreamer Dec 26, 2025
173e552
0021
earlydreamer Dec 26, 2025
79de1d8
0021 : 생성자 파라미터 순서 버그 수정
earlydreamer Dec 26, 2025
3dec5ef
0022
earlydreamer Dec 26, 2025
9854ecc
0023
earlydreamer Dec 27, 2025
e2840cd
0024
earlydreamer Dec 28, 2025
f4fa701
0025
earlydreamer Dec 28, 2025
a550f08
0026
earlydreamer Dec 28, 2025
8ad8ab5
0027
earlydreamer Dec 28, 2025
15f7e24
0028
earlydreamer Dec 28, 2025
c195967
0029
earlydreamer Dec 28, 2025
3955a33
0030
earlydreamer Dec 29, 2025
d653fe1
0031
earlydreamer Dec 29, 2025
c410c45
0032
earlydreamer Dec 29, 2025
14bb07b
0033
earlydreamer Dec 29, 2025
108a523
0034
earlydreamer Dec 29, 2025
69e62ee
금액 타입 리팩토링
earlydreamer Dec 29, 2025
55fefa8
0035
earlydreamer Dec 30, 2025
5d6c8d8
0036
earlydreamer Dec 30, 2025
24bfbfa
0037
earlydreamer Dec 30, 2025
b2af7ce
0038
earlydreamer Dec 30, 2025
9e9107d
0039
earlydreamer Dec 30, 2025
3db7d7b
0040
earlydreamer Dec 30, 2025
2578ed4
0041
earlydreamer Dec 30, 2025
3c5bc59
0042
earlydreamer Dec 30, 2025
6ca42ad
0043
earlydreamer Dec 30, 2025
132c964
0044
earlydreamer Dec 30, 2025
8db3cc7
0045
earlydreamer Dec 30, 2025
4ddef41
0046
earlydreamer Dec 31, 2025
8f4b78c
0047
earlydreamer Dec 31, 2025
b4fc991
0048
earlydreamer Dec 31, 2025
da86296
0049
earlydreamer Dec 31, 2025
9871238
0050
earlydreamer Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.default
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TOSS_PAYMENTS_SECRET_KEY=NEED_TO_INPUT
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ out/

### VS Code ###
.vscode/
application.yml
application-dev.yml
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-h2console'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation 'org.springframework.boot:spring-boot-starter-batch'
testImplementation 'org.springframework.batch:spring-batch-test'

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
Expand Down
18 changes: 18 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- # 토스 페이먼츠
- [토스 페이먼츠, 내 개발정보](https://developers.tosspayments.com/my/api-logs)
- API 개별 연동 키
- 테스트 클라이언트 키
- 시크릿 키
- API 로그
- 테스트 결제내역

# 토스 페이먼츠 테스트

- [결제시도](https://codepen.io/jangka44/debug/yyJBXaM)
- [소스코드](https://codepen.io/jangka44/pen/yyJBXaM?editors=1000)
- [최종승인](https://codepen.io/jangka44/debug/GgqKEWV)
- 직접 접근 금지
- [소스코드](https://codepen.io/jangka44/pen/GgqKEWV?editors=1000)
- [결제실패](https://codepen.io/jangka44/debug/xbOKrdJ)
- 직접 접근 금지
- [소스코드](https://codepen.io/jangka44/pen/xbOKrdJ?editors=1000)
3 changes: 3 additions & 0 deletions src/main/java/com/back/BackApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class BackApplication {

public static void main(String[] args) {
SpringApplication.run(BackApplication.class, args);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.back.boundedContext.cash.app.facade;

import com.back.boundedContext.cash.app.query.CashMemberQuery;
import com.back.boundedContext.cash.app.query.WalletQuery;
import com.back.boundedContext.cash.app.usecase.CashCompleteOrderPaymentUseCase;
import com.back.boundedContext.cash.app.usecase.CashCompletePayoutUseCase;
import com.back.boundedContext.cash.app.usecase.CashCreateWalletUseCase;
import com.back.boundedContext.cash.app.usecase.CashSyncMemberUseCase;
import com.back.boundedContext.cash.domain.CashMember;
import com.back.boundedContext.cash.domain.Wallet;
import com.back.shared.market.dto.OrderDto;
import com.back.shared.member.dto.MemberDto;
import com.back.shared.payout.dto.PayoutDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class CashFacade {
private final CashMemberQuery cashMemberQuery;
private final WalletQuery walletQuery;

private final CashCreateWalletUseCase cashCreateWalletUseCase;
private final CashSyncMemberUseCase cashSyncMemberUseCase;
private final CashCompleteOrderPaymentUseCase cashCompleteOrderPaymentUseCase;
private final CashCompletePayoutUseCase cashCompletePayoutUseCase;

@Transactional
public Wallet createWallet(CashMember holder) {
return cashCreateWalletUseCase.createWallet(holder);
}

@Transactional
public long count() {return cashMemberQuery.count();}

@Transactional
public Optional<CashMember> findCashMemberById(Long id) {
return cashMemberQuery.findCashMemberById(id);
}

@Transactional(readOnly = true)
public Optional<CashMember> findCashMemberByUsername(String username) {
return cashMemberQuery.findCashMemberByUsername(username);
}

@Transactional(readOnly = true)
public Optional<Wallet> findWalletByHolder(CashMember holder) {
return walletQuery.findWalletByHolder(holder);
}

@Transactional
public CashMember syncMember(MemberDto member) {
return cashSyncMemberUseCase.syncMember(member);
}

@Transactional
public void completeOrderPayment(OrderDto order, long pgPaymentAmount) {
cashCompleteOrderPaymentUseCase.completeOrderPayment(order, pgPaymentAmount);
} @Transactional(readOnly = true)
public Optional<Wallet> findWalletByHolderId(Long holderId) {
return walletQuery.findWalletByHolderId(holderId);
}

@Transactional
public void completePayout(PayoutDto payout) {
cashCompletePayoutUseCase.completePayout(payout);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.back.boundedContext.cash.app.query;

import com.back.boundedContext.cash.domain.CashMember;
import com.back.boundedContext.cash.out.repository.CashMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;



@Component
@RequiredArgsConstructor
public class CashMemberQuery {

private final CashMemberRepository cashMemberRepository;

@Transactional
public long count() {
return cashMemberRepository.count();
}


public Optional<CashMember> findCashMemberById(Long id) {
return cashMemberRepository.findById(id);
}


@Transactional(readOnly = true)
public Optional<CashMember> findCashMemberByUsername(String username) {
return cashMemberRepository.findByUsername(username);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.back.boundedContext.cash.app.query;

import com.back.boundedContext.cash.domain.CashMember;
import com.back.boundedContext.cash.domain.CashPolicy;
import com.back.boundedContext.cash.domain.Wallet;
import com.back.boundedContext.cash.out.repository.WalletRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;


@Component
@RequiredArgsConstructor
public class WalletQuery {

private final WalletRepository walletRepository;

@Transactional(readOnly = true)
public Optional<Wallet> findWalletByHolder(CashMember holder) {
return walletRepository.findByHolder(holder);
}

public Optional<Wallet> findWalletByHolderId(Long holderId) {
return walletRepository.findByHolderId(holderId);
}

public Optional<Wallet> findHoldingWallet() {
return walletRepository.findByHolderId(CashPolicy.HOLDING_MEMBER_ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.back.boundedContext.cash.app.usecase;

import com.back.boundedContext.cash.app.query.CashMemberQuery;
import com.back.boundedContext.cash.app.query.WalletQuery;
import com.back.boundedContext.cash.domain.CashEventType;
import com.back.boundedContext.cash.domain.Wallet;
import com.back.global.eventPublisher.EventPublisher;
import com.back.shared.cash.event.CashOrderPaymentFailedEvent;
import com.back.shared.cash.event.CashOrderPaymentSucceededEvent;
import com.back.shared.market.dto.OrderDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CashCompleteOrderPaymentUseCase {
private final CashMemberQuery cashMemberQuery;
private final WalletQuery walletQuery;
private final EventPublisher eventPublisher;


public void completeOrderPayment(OrderDto order, long pgPaymentAmount) {
Wallet customerWallet = walletQuery.findWalletByHolderId(order.getCustomerId()).get();
Wallet holdingWallet = walletQuery.findHoldingWallet().get();

if (pgPaymentAmount > 0) {
customerWallet.credit(
pgPaymentAmount,
CashEventType.충전__PG결제_토스페이먼츠,
order.getModelTypeCode(),
order.getId() );
}

boolean canPay = customerWallet.getBalance() >= order.getSalePrice();

if (canPay) {
customerWallet.debit(
order.getSalePrice(),
CashEventType.사용__주문결제,
order.getModelTypeCode(),
order.getId()
);

holdingWallet.credit(
order.getSalePrice(),
CashEventType.임시보관__주문결제,
order.getModelTypeCode(),
order.getId()
);

eventPublisher.publish(
new CashOrderPaymentSucceededEvent(
order,
pgPaymentAmount
)
);
} else {
eventPublisher.publish(
new CashOrderPaymentFailedEvent(
"400-1",
"충전은 완료했지만 %번 주문을 결제완료처리를 하기에는 예치금이 부족합니다.".formatted(order.getId()),
order,
pgPaymentAmount,
pgPaymentAmount - customerWallet.getBalance()
)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.boundedContext.cash.app.usecase;

import com.back.boundedContext.cash.app.query.WalletQuery;
import com.back.boundedContext.cash.domain.CashEventType;
import com.back.boundedContext.cash.domain.Wallet;
import com.back.shared.payout.dto.PayoutDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CashCompletePayoutUseCase {
private final WalletQuery walletQuery;

public void completePayout(PayoutDto payout) {
Wallet holdingWallet = walletQuery.findHoldingWallet().get();
Wallet payeeWallet = walletQuery.findWalletByHolderId(payout.getPayeeId()).get();

holdingWallet.debit(
payout.getAmount(),
payout.isPayeeSystem() ? CashEventType.정산지급__상품판매_수수료 : CashEventType.정산지급__상품판매_대금,
payout.getModelTypeCode(),
payout.getId()
);

payeeWallet.credit(
payout.getAmount(),
payout.isPayeeSystem() ? CashEventType.정산수령__상품판매_수수료 : CashEventType.정산수령__상품판매_대금,
payout.getModelTypeCode(),
payout.getId()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.back.boundedContext.cash.app.usecase;

import com.back.boundedContext.cash.domain.CashMember;
import com.back.boundedContext.cash.domain.Wallet;
import com.back.boundedContext.cash.out.repository.WalletRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CashCreateWalletUseCase {

private final WalletRepository walletRepository;

@Transactional
public Wallet createWallet(CashMember holder) {
Wallet wallet = new Wallet(holder);

return walletRepository.save(wallet);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.back.boundedContext.cash.app.usecase;

import com.back.boundedContext.cash.domain.CashMember;
import com.back.boundedContext.cash.out.repository.CashMemberRepository;
import com.back.global.eventPublisher.EventPublisher;
import com.back.shared.cash.event.CashMemberCreatedEvent;
import com.back.shared.member.dto.MemberDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Member를 Cash 컨텍스트의 CashMember로 동기화하는 UseCase
*
* [설계 원칙]
* - MemberJoinedEvent, MemberUpdatedEvent 모두 동일한 MemberDto를 사용
* - 도메인 일관성 > 이벤트 책임분리: Member는 항상 동일한 구조(MemberDto)로 표현
* - 신규/갱신 판단은 수신 컨텍스트(Cash)의 책임
*/
@Service
@RequiredArgsConstructor
public class CashSyncMemberUseCase {

private final CashMemberRepository cashMemberRepository;
private final EventPublisher eventPublisher;

/**
* Member를 CashMember로 동기화한다.
*
* [동작 방식]
* - 신규 멤버: CashMember 생성 후 CashMemberCreatedEvent 발행 (Wallet 생성 트리거)
* - 기존 멤버: CashMember 정보 갱신 (이벤트 발행 없음)
*
* @param member MemberDto - 도메인 기반 공유 DTO
* @return 동기화된 CashMember
*/
@Transactional
public CashMember syncMember(MemberDto member) {
boolean isNew = !cashMemberRepository.existsById(member.getId());

// 민감정보인 Password는 미러링에 넘기지 않는다
CashMember cashMember = cashMemberRepository.save(
new CashMember(
member.getId(),
member.getCreatedAt(),
member.getUpdatedAt(),
member.getUsername(),
"",
member.getNickname(),
member.getActivityScore()
)
);

// 신규 생성시에만 CashMemberCreatedEvent 발행
// → CashEventListener가 수신하여 Wallet 생성
if (isNew) {
eventPublisher.publish(
new CashMemberCreatedEvent(
cashMember.toDto()
)
);
}


return cashMember;
}

}
Loading