포인트 시스템 (Point System) - fitpassTeam/fitpass GitHub Wiki
FitPass의 포인트 시스템은 사용자의 모든 결제 및 환불을 포인트로 관리하는 핵심 시스템입니다. 토스페이먼츠와 연동하여 실시간 충전이 가능하며, PT 예약 결제 및 현금화 서비스를 제공합니다.
- 실시간 충전 : 토스페이먼츠 연동으로 즉시 포인트 충전
- 안전한 결제 : 트랜잭션 보장으로 데이터 일관성 유지
- 유연한 환불 : PT 취소 시 100% 즉시 환불
- 현금화 서비스 : 90% 비율로 포인트를 현금으로 전환
- 완전한 이력 관리 : 모든 포인트 거래 내역 추적 가능
타입 | 설명 | 사용 용도 | 잔액 변화 |
---|---|---|---|
CHARGE | 포인트 충전 | 토스페이먼츠 결제 완료 시 | 증가 (+) |
USE | 포인트 사용 | PT 예약, 멤버십 구매 등 | 감소 (-) |
REFUND | 포인트 환불 | PT 취소, 예약 거부 시 | 증가 (+) |
CASH_OUT | 포인트 현금화 | 사용자 현금 출금 요청 시 | 감소 (-) |
상태 | 설명 | 처리 시점 | 되돌리기 |
---|---|---|---|
PENDING | 처리 대기 | 거래 시작 시 | 가능 |
COMPLETED | 처리 완료 | 거래 성공 시 | 불가 |
CANCELED | 거래 취소 | 거래 실패 시 | 불가 |
1단계: 결제 준비
@Transactional
public PaymentUrlResponseDto preparePayment(Long userId, Integer amount, String orderName) {
User user = userRepository.findByIdOrElseThrow(userId);
// 고유한 주문 ID 생성
String orderId = generateOrderId();
// 결제 정보 저장 (PENDING 상태)
Payment payment = Payment.builder()
.orderId(orderId)
.orderName(orderName)
.amount(amount)
.status(PaymentStatus.PENDING)
.user(user)
.build();
return new PaymentUrlResponseDto(...);
}
2단계: 결제 승인 후 포인트 충전
@Transactional
public PointBalanceResponseDto chargePoint(Long userId, int amount, String description) {
User user = userRepository.findByIdOrElseThrow(userId);
// 현재 잔액에서 충전될 포인트 추가
int newBalance = user.getPointBalance() + amount;
// 포인트 이력 저장
Point point = new Point(user, amount, description, newBalance, PointType.CHARGE);
pointRepository.save(point);
// 사용자 잔액 업데이트
user.updatePointBalance(newBalance);
return new PointBalanceResponseDto(newBalance);
}
@Transactional
public PointBalanceResponseDto usePoint(Long userId, int amount, String description) {
User user = userRepository.findByIdOrElseThrow(userId);
// 잔액 부족 검증
if (user.getPointBalance() < amount) {
throw new BaseException(ExceptionCode.INSUFFICIENT_POINT_BALANCE);
}
// 현재 잔액에서 사용한 포인트 차감
int newBalance = user.getPointBalance() - amount;
// 포인트 이력 저장
Point point = new Point(user, amount, description, newBalance, PointType.USE);
pointRepository.save(point);
// 사용자 잔액 업데이트
user.updatePointBalance(newBalance);
return new PointBalanceResponseDto(newBalance);
}
사용 사례
- PT 예약 취소
- 헬스장 오너의 예약 거부
- 시스템 오류로 인한 환불
환불 프로세스
@Transactional
public PointBalanceResponseDto refundPoint(Long userId, int amount, String description) {
User user = userRepository.findByIdOrElseThrow(userId);
int newBalance = user.getPointBalance() + amount;
Point point = new Point(user, amount, description, newBalance, PointType.REFUND);
pointRepository.save(point);
user.updatePointBalance(newBalance);
return new PointBalanceResponseDto(newBalance);
}
정책 이유
- 수수료 및 운영비 충당
- 무분별한 현금화 방지
- 플랫폼 지속가능성 확보
현금화 프로세스
@Transactional
public PointCashOutResponseDto cashOutPoint(Long userId, int amount, String description) {
User user = userRepository.findByIdOrElseThrow(userId);
// 잔액 부족 검증
if (user.getPointBalance() < amount) {
throw new BaseException(ExceptionCode.INSUFFICIENT_POINT_BALANCE);
}
int requestedAmount = amount;
int cashAmount = (int) (requestedAmount * 0.9); // 90% 지급
int newBalance = user.getPointBalance() - requestedAmount;
Point point = new Point(user, requestedAmount, description, newBalance, PointType.CASH_OUT);
pointRepository.save(point);
user.updatePointBalance(newBalance);
return new PointCashOutResponseDto(requestedAmount, cashAmount, newBalance);
}
저장 정보
- 거래 금액 (amount)
- 거래 후 잔액 (balance)
- 거래 설명 (description)
- 거래 타입 (pointType)
- 거래 상태 (pointStatus)
- 거래 시간 (createdAt)
@Transactional(readOnly = true)
public List<PointResponseDto> getPointHistory(Long userId) {
List<Point> points = pointRepository.findByUserIdOrderByCreatedAtDesc(userId);
return points.stream()
.map(PointResponseDto::from)
.collect(Collectors.toList());
}
Method | Endpoint | 설명 | 권한 |
---|---|---|---|
POST | /users/points/charge/prepare | 포인트 충전 준비 | USER |
POST | /users/points/use | 포인트 사용 | USER |
POST | /users/points/refund | 포인트 환불 | USER |
POST | /users/points/cashout | 포인트 현금화 | USER |
Method | Endpoint | 설명 | 권한 |
---|---|---|---|
GET | /users/points | 포인트 잔액 조회 | USER |
GET | /users/points/history | 포인트 이력 조회 | USER |
Method | Endpoint | 설명 | 권한 |
---|---|---|---|
POST | /admin/points/charge/{userId} | 관리자 포인트 충전 | ADMIN |
POST | /admin/points/adjust/{userId} | 포인트 조정 | ADMIN |
{
"amount": 50000,
"orderName": "포인트 충전"
}
{
"statusCode": 200,
"message": "결제 준비가 완료되었습니다.",
"data": {
"orderId": "ORDER_20250702143022_a1b2c3d4",
"amount": 50000,
"orderName": "포인트 충전",
"customerEmail": "[email protected]",
"customerName": "김피트",
"successUrl": "https://fitpass.com/payment/success",
"failUrl": "https://fitpass.com/payment/fail"
}
}
{
"amount": 30000,
"description": "PT 예약 - 김트레이너"
}
{
"statusCode": 200,
"message": "포인트 사용이 완료되었습니다.",
"data": {
"balance": 20000
}
}
{
"amount": 10000,
"description": "포인트 현금화 신청"
}
{
"statusCode": 200,
"message": "포인트 현금화가 완료되었습니다.",
"data": {
"requestedAmount": 10000,
"cashAmount": 9000,
"newBalance": 10000
}
}
- 최소 충전 금액: 1,000원
- 최대 충전 금액: 1,000,000원
- 충전 단위: 1,000원 단위
- 결제 수단: 토스페이먼츠 지원 수단
- 최소 사용 금액: 100원
- 잔액 부족 시: 즉시 차단
- 동시 사용: 트랜잭션으로 보장
- 사용 취소: 불가 (환불로만 처리)
- 환불 비율: 100% 전액 환불
- 환불 처리: 즉시 처리
- 환불 제한: 없음
- 환불 방식: 포인트로만 환불
- 현금화 비율: 90%
- 최소 현금화: 10,000원
- 최대 현금화: 100,000원/일
- 처리 시간: 영업일 기준 1-3일
- 모든 포인트 거래는
@Transactional
적용 - 포인트 차감과 서비스 제공의 원자성 보장
- 롤백 시 포인트 복구 자동화
- 잔액 음수 방지 검증
- 포인트 이력과 사용자 잔액 일치성 검증
- 정기적인 포인트 정합성 체크