포인트 시스템 (Point System) - fitpassTeam/fitpass GitHub Wiki

포인트 시스템 (Point System)

시스템 개요

FitPass의 포인트 시스템은 사용자의 모든 결제 및 환불을 포인트로 관리하는 핵심 시스템입니다. 토스페이먼츠와 연동하여 실시간 충전이 가능하며, PT 예약 결제 및 현금화 서비스를 제공합니다.

핵심 특징

  • 실시간 충전 : 토스페이먼츠 연동으로 즉시 포인트 충전
  • 안전한 결제 : 트랜잭션 보장으로 데이터 일관성 유지
  • 유연한 환불 : PT 취소 시 100% 즉시 환불
  • 현금화 서비스 : 90% 비율로 포인트를 현금으로 전환
  • 완전한 이력 관리 : 모든 포인트 거래 내역 추적 가능

포인트 타입 관리

타입 설명 사용 용도 잔액 변화
CHARGE 포인트 충전 토스페이먼츠 결제 완료 시 증가 (+)
USE 포인트 사용 PT 예약, 멤버십 구매 등 감소 (-)
REFUND 포인트 환불 PT 취소, 예약 거부 시 증가 (+)
CASH_OUT 포인트 현금화 사용자 현금 출금 요청 시 감소 (-)

포인트 상태 관리

상태 설명 처리 시점 되돌리기
PENDING 처리 대기 거래 시작 시 가능
COMPLETED 처리 완료 거래 성공 시 불가
CANCELED 거래 취소 거래 실패 시 불가

포인트 충전 프로세스

image

충전 프로세스 상세

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);
}

포인트 사용 프로세스

image

사용 로직

@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);
}

포인트 환불 시스템

100% 환불 (PT 취소 등)

사용 사례

  • 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);
}

현금화 시스템

90% 현금화 정책

정책 이유

  • 수수료 및 운영비 충당
  • 무분별한 현금화 방지
  • 플랫폼 지속가능성 확보

현금화 프로세스

@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());
}

API 명세

포인트 관리

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 적용
  • 포인트 차감과 서비스 제공의 원자성 보장
  • 롤백 시 포인트 복구 자동화

데이터 무결성

  • 잔액 음수 방지 검증
  • 포인트 이력과 사용자 잔액 일치성 검증
  • 정기적인 포인트 정합성 체크
⚠️ **GitHub.com Fallback** ⚠️