Files
sodalive-backend-spring-boot/docs/20261019_첫충전보너스신규지급중단/plan-task.md
T

20 KiB

첫 충전 보너스 신규 지급 중단 구현 계획

문서 항목 내용
상태 구현 완료, 검증·독립 리뷰 통과, 미배포
작성일 2026-10-19
요구사항 기준 prd.md
현재 Phase Phase 1, 신규 생성 분기 변경
현재 활성 Goal 없음
다음 Goal 없음, 구현 범위 완료
작업 위치 현재 test 브랜치, 별도 worktree·브랜치 생성 없음

목표

신규 충전에서 첫 충전 보너스 작업만 만들지 않고, 기존 활성 이벤트 지급과 이미 저장된 모든 작업 계약을 보존한다.

현재 상태

Phase 상태 완료 Task 활성/다음 Goal 차단 또는 남은 조건
1 완료 1/1 없음 운영 배포는 별도 범위

사용자의 구현 승인을 받아 P1-T1과 독립 리뷰를 포함한 P1-GATE를 완료했다. 아래 문서 작성 단계의 미실행 기록은 당시 이력으로 유지한다.

범위

포함

  • ChargeEventJobService.buildSnapshot(charge, member)에서 첫 충전 분기를 없애고 활성 이벤트 스냅샷 경로로 통일한다.
  • 삭제된 분기 때문에 사용되지 않는 생성자 의존성, 함수, import와 상수를 해당 파일에서만 정리한다.
  • 신규 생성 차단과 기존 활성 이벤트/저장 작업의 회귀 테스트를 추가 또는 보강한다.

제외

  • 구매·결제 완료 경로, 유료 캔, 회원 정보 응답의 chargeCount, isFirstCharged, API, DB schema, 이벤트 enum과 기존 지급 처리 변경.
  • PENDING/FAILED/PROCESSING/DONE 상태 정책, worker/admin 서비스, publishPaidNotification의 FIRST_CHARGE 분기 변경.
  • ChargeEventService.applyChargeEvent 및 그 공유 메서드의 정리. 현재 live caller가 없지만 별도 레거시 경계다.
  • migration, 과거 작업 수정/삭제/환수, feature flag, cutoff 값, dependency 추가, 별도 운영 실행.

구현 제약

  • 기술 스택은 Kotlin, Spring Boot, JUnit 5이며 기존 서비스 패턴을 따른다.
  • gateway 누락 예외 검증은 snapshot 선택보다 먼저 일어나도록 유지한다.
  • 인증 여부/charge count를 조회해 첫 충전 자격을 판정하지 않는다. 매 충전에서 기존 활성 이벤트와 회원별 횟수 한도를 평가한다.
  • processJob(jobId)는 저장 스냅샷으로 작업을 수행하므로 해당 경로와 retry/idempotency를 수정하지 않는다.
  • 기존 FIRST_CHARGE 데이터는 배포 시점과 관계없이 보존하고 처리한다.

Phase 1: 신규 생성 분기 변경

Phase 결과: 신규 FIRST_CHARGE 작업 생성은 중단되고 활성 이벤트 및 기존 저장 작업의 지급은 기존대로 동작한다.

선행조건: 별도의 구현 승인. 이 문서 작성은 구현 승인이 아니다.

Phase 완료 조건: P1-T1 완료, P1-GATE 통과 및 실제 결과 기록.

구현 항목

Task 1.1 신규 생성 차단과 회귀 보존

Goal 실행 P1-T1

Objective: 새 결제 완료 충전의 작업 스냅샷을 활성 이벤트로만 제한하면서 기존 이벤트·저장 작업의 계약을 테스트로 고정한다.

  • 시작 조건: PRD STOP-001~STOP-004 확정, 별도 구현 승인, 작업 트리에서 대상 파일 재확인.
  • 완료 증거: 모든 실행 체크박스 완료, focused test와 ktlint 결과, task Progress에 실제 명령/결과 기록.
  • 범위 밖: worker/admin 동작 수정, 결제 데이터 조작, 문서 밖 범위 확장.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobService.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobWorkerTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/event/charge/AdminChargeEventJobServiceTest.kt

Interfaces:

  • Consumes: ChargeEventJobService.createProcessingJob(sourceChargeId: Long, memberId: Long): ChargeEventJob?의 기존 gateway 검증, charge/member 조회, 멱등 키 조회 및 저장 계약.

  • Produces: 외부 시그니처 변경 없음. buildSnapshot(charge: Charge, member: Member): ChargeEventJob?는 활성 이벤트 스냅샷 결과 또는 null만 반환한다.

  • RED: ChargeEventJobServiceTest에 인증 회원 첫 충전에서 신규 FIRST_CHARGE가 생기지 않으며, 인증 이력 조회를 하지 않고 활성 이벤트 스냅샷만 생기는 테스트를 추가한다. 15% 계산 대신 활성 이벤트 설정의 addPercent/제목/ID/횟수 제한으로 저장 내용을 검증한다.

  • RED 확인: 아래 focused test 명령을 실행해 새 회귀 assertion이 FIRST_CHARGE 분기 때문에 실패하는지 확인한다. 테스트 설정 오류나 fixture 오류는 RED로 인정하지 않는다.

  • 기존 동작 고정: 활성 이벤트 테스트 픽스처로 PG, PAYVERSE, APPLE_IAP, GOOGLE_IAP 각각 gateway, container(pg/ios/aos), additionalCan, methodSnapshot, ACTIVE_CHARGE_EVENT와 event ID가 기존 규칙 그대로 저장되는지 확인한다. 프로세스 후 지급 알림의 제목/수신자/추가 캔 내용이 기존 규칙을 따르는지 검증한다. 기존 계약을 고정하는 테스트는 변경 전부터 통과해야 한다.

  • RED: 활성 이벤트 없음과 회원별 횟수 한도 소진에서 새 작업이 없고, 새로 첫 결제한 인증 회원도 일반 활성 이벤트를 적용받는 테스트를 추가한다.

  • 기존 동작 고정: 과거 FIRST_CHARGE snapshot fixture로 활성 이벤트가 없어도 저장된 PENDING/PROCESSING 작업을 processJob가 저장 금액·gateway·container·method대로 한 번 처리하고 인증/자격 조회나 이벤트 조회를 다시 하지 않는지 확인한다. 네 gateway의 보너스 잔액과 첫 충전 알림을 검증하며, 별도 상태 자동 복구 동작은 기대하지 않는다.

  • 기존 동작 고정: FAILED는 관리자 수동 retry가 PENDING으로 되돌린 뒤 worker에 처리되고, DONE은 재호출해도 재지급되지 않는 기존 동작을 검증한다.

  • RED 확인: ./gradlew test --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest'를 실행한다. 신규 중단 assertion은 미구현 동작으로 실패하고, 저장된 작업/일반 이벤트 보존 테스트는 통과하는지 구분한다.

  • GREEN: buildSnapshot에서 gateway 선검증을 유지하고 buildActiveEventSnapshot(charge, member, paymentGateway)을 반환하도록 최소 변경한다. 미사용 AuthRepository 생성자 의존성/import, buildFirstChargeSnapshot, ceil import 및 더 이상 참조되지 않는 FIRST_CHARGE_METHOD만 제거한다.

  • GREEN 확인: 동일 focused test 명령으로 신규 차단 및 활성 이벤트/기존 작업 회귀가 통과하는지 확인한다.

  • REFACTOR: 이번 변경이 만든 중복만 정리한다. 서비스·worker·admin 테스트와 ./gradlew ktlintCheck를 다시 실행하고 실제 결과를 Progress에 기록한다. 필요한 경우에만 테스트 fixture 중복을 정리한다.

Task 검증 기록 (2026-10-19): 원본 focused 11개 PASS 후 테스트를 추가했다. RED에서는 35개 중 신규 생성 assertion 9개만 실패하고 보존 테스트 26개는 통과했다. AuthRepository를 실제 주입한 fixture에 DI·충전 1건 stub을 설정해 기존 FIRST_CHARGE가 생성되는 실패를 확인했다. 최소 변경 후 서비스 25개·worker 6개·관리자 4개, 총 35개가 통과했다. 기존 테스트 삭제·완화는 없다. RED의 의존성 주입·stub은 production 의존성 제거에 맞춰 정리하되 결과 assertion은 유지했다.

필수 테스트와 판정

테스트 이름 핵심 assertion
shouldNotCreateFirstChargeJobForNewAuthenticatedCharge 첫 충전 대상 회원도 신규 FIRST_CHARGE를 만들지 않는다. 활성 이벤트가 없으면 저장 없이 null이다.
shouldApplyActiveEventToNewChargeAcrossGateways 네 gateway에서 일반 이벤트의 ID·제목·비율·container를 사용한다. 예: 100캔, 일반 이벤트 10%라면 15캔이 아닌 10캔이다.
shouldNotCreateJobWhenActiveEventLimitIsExhausted 일반 이벤트 한도가 소진되면 첫 충전 보너스로 우회하지 않고 저장 없이 null이다.
shouldPayExistingFirstChargeSnapshotWithoutActiveEvent 기존 스냅샷 금액이 15이면 현재 이벤트/인증과 무관하게 15를 유지하고, 네 gateway의 해당 보너스 잔액·기록·첫 충전 알림을 검증한다.
shouldProcessPendingFirstChargeJobAndSkipDoneJob worker의 기존 PENDING 처리와 DONE 재호출에서 지급 1회·잔액 불변을 검증한다.
shouldRetryFailedFirstChargeJobWithoutChangingSnapshot 관리자 재시도 후 PENDING, retryCount 0, 원본 스냅샷 보존과 후속 지급을 검증한다.

문서 작성의 TDD 예외 사유: 실행 코드 동작이 없는 문서 전용 작업이다. 대체 검증 방법: PRD와 계획의 요구사항·파일·불변조건 대조, Markdown 참조 확인 및 ./gradlew tasks --all 실행이다. 구현 Task P1-T1은 TDD 예외가 아니다.

Phase 1 Gate

Goal 실행 P1-GATE

Objective: 첫 충전 신규 생성 차단과 기존 활성 이벤트/저장 작업 계약 보존을 자동 테스트 및 배포 경계 체크리스트로 판정한다.

  • 시작 조건: P1-T1의 모든 체크박스 완료, 해당 Progress 기록 존재.
  • 완료 증거: focused test 및 ktlint 통과, 아래 불변조건 확인, 미실행 배포 체크를 운영 검증 완료로 오인하지 않도록 기록.
  • 범위 밖: 운영 DB/결제/SQL 조작, 배포 실행, 테스트 삭제·완화, 새 복구 정책.

Files:

  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobService.kt

  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJob.kt

  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobWorker.kt

  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/event/charge/AdminChargeEventJobService.kt

  • ./gradlew test --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest' 통과 확인.

  • ./gradlew ktlintCheck 통과 확인.

  • 코드 diff가 PRD 포함 파일과만 일치하며 enum, processJob, worker, admin service, 결제 API 변경이 없는지 확인.

  • 읽기 전용 배포 체크리스트에 cutoff 이후 신규 FIRST_CHARGE 부재 및 기존 snapshot 처리 지속을 남긴다. 이는 이 계획에서 실행하지 않는다.

  • 전체 회귀는 공통 경계/여러 domain 영향, targeted 결과로 영향 판단 불가, release/final 전체 증거 요구 또는 사용자 요청일 때 실행한다. 생략 시 근거와 focused 명령을 Progress에 기록한다.

실행 순서와 의존성

순서 Goal 선행조건 병행 가능 차단 시 다음 행동
1 P1-T1 별도 구현 승인 아니요 승인이 없으면 문서 상태 유지
2 P1-GATE P1-T1 완료 아니요 실패 원인 소유 테스트/변경 확인 후 별도 회귀 goal 결정
P1-T1 → P1-GATE

변경 금지 항목

  • ChargeEventJobType.FIRST_CHARGE와 기존 저장 snapshot 처리 코드를 삭제하지 않는다.
  • publishPaidNotification의 FIRST_CHARGE 처리, 지급 알림/현지화, retry, idempotency 또는 DONE 중복 방지를 바꾸지 않는다.
  • 원본 충전, 광고/클라이언트 추적, 공개 API, DB, 구매 완료 경로를 바꾸지 않는다.
  • AuthRepository/ChargeRepository 공유 메서드와 레거시 ChargeEventService를 삭제하지 않는다.
  • cutoff 설정, migration, 운영용 SQL, 새 dependency와 관계없는 정리를 추가하지 않는다.
  • RED 확인 전에 구현하지 않으며 테스트를 삭제/skip/완화하지 않는다.

Progress

문서 작성 및 검증, 2026-10-19

  • 상태: 문서 완료, 구현 미승인·미실행
  • 무엇을: 사용자 확정 정책과 기술 범위로 PRD 및 실행 계획을 작성했다.
  • 왜: 구현 전에 신규 생성 차단 범위와 기존 작업 보존 계약을 분명히 하기 위해서다.
  • 어떻게:
    • 가이드/샘플 및 소스·테스트 읽기 — 완료.
    • ./gradlew tasks --all — 문서 생성 후 실행, exit 0 / BUILD SUCCESSFUL. 계획에서 참조한 test, ktlintCheck task 등록 확인.
    • 현재 test 브랜치 확인 및 Git 변경 범위 확인 — 신규 문서 디렉터리만 추가됐으며 소스·테스트·설정 변경 없음.
    • 테스트/결제/운영 확인 — 이번 문서 작업에서는 미실행. 미래 구현 항목이며 코드 변경이나 지급 중단 효과가 발생하지 않았다.
  • 남은 항목: P1-T1 별도 승인, 이후 P1-GATE.
  • 다음 행동: 별도 승인 이후 P1-T1만 goal로 활성화한다.

문서 검증 기록

  • ./gradlew tasks --all 실행으로 명령 유효성과 task 등록을 확인했다.
  • Markdown LSP 진단은 .md 서버 미설정으로 불가했다. 문서 직접 읽기와 소스 대조로 보완하며, 서버를 설치하거나 설정을 바꾸지 않는다.
  • 문서 전용 변경이므로 focused/전체 테스트와 ktlint를 재실행하지 않았다. 대신 문서 검토와 Gradle task 등록을 확인했다. 위 구현 체크박스는 모두 미실행 상태다.
  • 앞선 동작 조사에서 관련 기존 테스트 11건이 통과했으나, 확인된 픽스처는 ACTIVE_CHARGE_EVENT다. 따라서 이는 신규 중단이나 FIRST_CHARGE 회귀 커버리지 증거가 아니며 별도 테스트가 필요하다.

P1-T1 시작, 2026-10-19

  • 사용자 승인: "승인 문서대로 구현 진행". 현재 test 브랜치에서 수행하며 worktree/브랜치를 생성하지 않는다.
  • 사전 대조: P1-T1의 신규 생성 경로 변경과 P1-GATE의 기존 저장 작업 보존 검증은 일치한다. 공용 enum/worker/admin/원 결제 API는 변경하지 않는다.
  • 기존 테스트 읽기: 일반 이벤트 지급/중복 방지/재시도를 다루며 첫 충전 신규 중단과 저장된 첫 충전 스냅샷의 회귀는 추가해야 한다.
  • 승인된 범위 외 운영 DB/결제 호출/배포는 수행하지 않는다. 실제 서비스 메서드를 실행하는 격리 QA로 변경 동작을 검증한다.

P1-T1 완료 및 P1-GATE 검증, 2026-10-19

  • 운영 코드 변경은 ChargeEventJobService.kt 한 파일, 회귀 테스트는 기존 세 파일에 추가했다.
  • 기준선: ./gradlew --no-daemon test --rerun --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest' — 기존 11개 PASS.
  • RED: 같은 focused 명령 — 35개 중 9개 의도한 실패. expected ACTIVE_CHARGE_EVENT but was FIRST_CHARGE 및 expected null but was ChargeEventJob 확인.
  • GREEN/정리: ./gradlew --no-daemon test --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest' ktlintCheck — 35개 PASS, ktlint PASS. 신규 테스트 줄바꿈 lint를 수정 후 재검증했다.
  • 최종 실행: ./gradlew --no-daemon -I "$RUN_TMP/qa.init.gradle" firstBonusManualQa build ktlintCheck — exit 0, BUILD SUCCESSFUL in 9m 11s. build가 전체 test를 실제 실행했고 322 suites / 2,291 tests / failures 0 / errors 0 / skipped 0이다.
  • 빌드 산출물: build/libs/sodalive-0.0.1-SNAPSHOT.jar. 테스트 결과: build/test-results/test/, build/reports/tests/test/index.html.
  • Kotlin LSP는 서버 미설치로 네 파일 모두 진단 불가다. 설치하지 않고 Gradle 컴파일·전체 테스트·ktlint로 보완했다. Markdown LSP도 미설정이다.
  • git diff --check PASS. 소스 변경은 승인된 한 파일이며 enum/worker/admin/원본 결제와 isFirstCharged는 변경하지 않았다.
  • 사용자 ULW 요청으로 전체 suite/build를 실행했다. 운영 DB/결제/푸시 호출 및 배포는 하지 않았다.

독립 실행 QA 증거

임시 BonusQa.java와 Gradle init task가 JUnit 밖에서 실제 createAndProcessImmediate, processJob, AdminChargeEventJobService.retry, ChargeEventJobWorker.runPendingJobs를 호출했다. repository/트랜잭션/알림 전송 경계는 격리 fixture로 대체했다. 이는 실제 서비스 로직 실행 증거이며, DB 커밋·운영 결제·푸시 수신의 검증은 아니다.

시나리오 이진 판정 관찰 결과
S1 신규 생성/일반 이벤트 활성 이벤트 없음은 작업·지급 0, 100캔·일반 이벤트 10%는 FIRST가 아닌 ACTIVE 10캔 한 번 네 gateway 모두 PASS
S2 한도/입력 경계 한도 소진은 작업 0, 결제 정보 누락은 기존 예외 네 gateway 모두 PASS
S3 저장된 첫 충전 PENDING/PROCESSING의 저장 15캔을 정확한 지갑에 1회 지급, 원본 유료 잔액 유지, 기록·알림 일치 네 gateway 모두 PASS
S4 재시도/중복 FAILED 관리자 재시도 후 worker 지급, DONE 재호출은 잔액·기록·알림 증가 없음 네 gateway 모두 PASS
  • 실행 출력: QA PASS: 28 isolated real-service scenarios; no external payment, DB or push calls.
  • 임시 로그의 핵심 결과를 이 문서에 보존한다. QA 스크립트·init task·task 전용 캐시와 로그는 최종 리뷰 후 삭제하며 저장소에 추가하지 않는다.

배포 후 별도 운영 체크리스트 (이번 실행 범위 밖)

  • 모든 구버전 생성 인스턴스 drain 이후 새 FIRST_CHARGE 작업이 생기지 않는지 확인한다. 문서 작성일/구매 시작일을 cutoff로 사용하지 않는다.
  • 이전에 생성된 FIRST_CHARGE의 저장 지급액·gateway·수신자·지급 기록을 대조하고 기존 재시도가 유지되는지 확인한다. 롤링 배포 중 구버전이 생성한 작업도 보존한다.
  • PROCESSING에 새 자동 복구를 추가하지 않았고, FAILED는 기존 관리자 재시도가 필요하다.

P1-GATE 최종 판정, 2026-10-19

  • 상태: 완료. 별도 구현자의 결과를 부모 세션에서 직접 읽고 실행 검증한 뒤 독립 리뷰를 수행했다.
  • 독립 리뷰: APPROVED, spec compliance와 코드 품질 모두 차단 결함 없음. S1~S4, 변경 diff, 저장 작업 enum/지급/알림/worker/admin 계약 및 RED/GREEN·전체 XML·QA 로그를 대조했다.
  • 검증 수치: focused 35개 PASS, 전체 2,291개 PASS, 실패·오류·skip 0, build·ktlint PASS, Java 드라이버 28개 시나리오 PASS.
  • 원본 결제/API/광고 추적·공용 enum/레거시 서비스 변경 없음. 배포 또는 데이터 migration을 수행하지 않았다.
  • 사전 판단: 추가 SDD 작업 디렉터리 대신 이 계획의 Progress에 결과를 누적하고 현재 test 브랜치를 유지했다. 제품 정책의 추가 변경은 없다.
  • 문서 최종 확인: ./gradlew --no-daemon tasks --all 성공. Markdown/Kotlin LSP 미설정·미설치 한계는 그대로 명시한다.
  • 임시 QA 자원은 task 소유 경로에만 생성했다. 검증·리뷰 결과를 이 문서에 남겼으며 최종 커밋 후 스크립트/init task/로그/캐시/메모를 제거하고 부재를 확인한다. 기존 공유 Gradle 캐시와 다른 작업의 프로세스는 건드리지 않는다.
  • 남은 운영 확인: 실제 DB 커밋·다중 인스턴스 부하·PG/스토어 결제·푸시 수신 및 배포 후 신규 생성 중단은 이번 검증에 포함하지 않았다. 위 배포 체크리스트에 따라 별도로 확인한다.