feat(charge): 첫 충전 보너스 신규 생성을 중단한다

This commit is contained in:
2026-10-19 18:33:34 +09:00
parent 2aef413adf
commit 45b97088ca
6 changed files with 777 additions and 34 deletions
@@ -0,0 +1,219 @@
# 첫 충전 보너스 신규 지급 중단 구현 계획
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 완료, 검증·독립 리뷰 통과, 미배포 |
| 작성일 | 2026-10-19 |
| 요구사항 기준 | [prd.md](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`만 반환한다.
- [x] **RED:** `ChargeEventJobServiceTest`에 인증 회원 첫 충전에서 신규 `FIRST_CHARGE`가 생기지 않으며, 인증 이력 조회를 하지 않고 활성 이벤트 스냅샷만 생기는 테스트를 추가한다. 15% 계산 대신 활성 이벤트 설정의 `addPercent`/제목/ID/횟수 제한으로 저장 내용을 검증한다.
- [x] **RED 확인:** 아래 focused test 명령을 실행해 새 회귀 assertion이 `FIRST_CHARGE` 분기 때문에 실패하는지 확인한다. 테스트 설정 오류나 fixture 오류는 RED로 인정하지 않는다.
- [x] **기존 동작 고정:** 활성 이벤트 테스트 픽스처로 `PG`, `PAYVERSE`, `APPLE_IAP`, `GOOGLE_IAP` 각각 gateway, container(`pg`/`ios`/`aos`), `additionalCan`, `methodSnapshot`, `ACTIVE_CHARGE_EVENT`와 event ID가 기존 규칙 그대로 저장되는지 확인한다. 프로세스 후 지급 알림의 제목/수신자/추가 캔 내용이 기존 규칙을 따르는지 검증한다. 기존 계약을 고정하는 테스트는 변경 전부터 통과해야 한다.
- [x] **RED:** 활성 이벤트 없음과 회원별 횟수 한도 소진에서 새 작업이 없고, 새로 첫 결제한 인증 회원도 일반 활성 이벤트를 적용받는 테스트를 추가한다.
- [x] **기존 동작 고정:** 과거 `FIRST_CHARGE` snapshot fixture로 활성 이벤트가 없어도 저장된 `PENDING`/`PROCESSING` 작업을 `processJob`가 저장 금액·gateway·container·method대로 한 번 처리하고 인증/자격 조회나 이벤트 조회를 다시 하지 않는지 확인한다. 네 gateway의 보너스 잔액과 첫 충전 알림을 검증하며, 별도 상태 자동 복구 동작은 기대하지 않는다.
- [x] **기존 동작 고정:** `FAILED`는 관리자 수동 retry가 `PENDING`으로 되돌린 뒤 worker에 처리되고, `DONE`은 재호출해도 재지급되지 않는 기존 동작을 검증한다.
- [x] **RED 확인:** `./gradlew test --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest'`를 실행한다. 신규 중단 assertion은 미구현 동작으로 실패하고, 저장된 작업/일반 이벤트 보존 테스트는 통과하는지 구분한다.
- [x] **GREEN:** `buildSnapshot`에서 gateway 선검증을 유지하고 `buildActiveEventSnapshot(charge, member, paymentGateway)`을 반환하도록 최소 변경한다. 미사용 `AuthRepository` 생성자 의존성/import, `buildFirstChargeSnapshot`, `ceil` import 및 더 이상 참조되지 않는 `FIRST_CHARGE_METHOD`만 제거한다.
- [x] **GREEN 확인:** 동일 focused test 명령으로 신규 차단 및 활성 이벤트/기존 작업 회귀가 통과하는지 확인한다.
- [x] **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`
- [x] `./gradlew test --tests '*ChargeEventJobServiceTest' --tests '*ChargeEventJobWorkerTest' --tests '*AdminChargeEventJobServiceTest'` 통과 확인.
- [x] `./gradlew ktlintCheck` 통과 확인.
- [x] 코드 diff가 PRD 포함 파일과만 일치하며 enum, `processJob`, worker, admin service, 결제 API 변경이 없는지 확인.
- [x] 읽기 전용 배포 체크리스트에 cutoff 이후 신규 `FIRST_CHARGE` 부재 및 기존 snapshot 처리 지속을 남긴다. 이는 이 계획에서 실행하지 않는다.
- [x] 전체 회귀는 공통 경계/여러 domain 영향, targeted 결과로 영향 판단 불가, release/final 전체 증거 요구 또는 사용자 요청일 때 실행한다. 생략 시 근거와 focused 명령을 Progress에 기록한다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 별도 구현 승인 | 아니요 | 승인이 없으면 문서 상태 유지 |
| 2 | `P1-GATE` | `P1-T1` 완료 | 아니요 | 실패 원인 소유 테스트/변경 확인 후 별도 회귀 goal 결정 |
```text
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로 활성화한다.
## 문서 검증 기록
- [x] `./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/스토어 결제·푸시 수신 및 배포 후 신규 생성 중단은 이번 검증에 포함하지 않았다. 위 배포 체크리스트에 따라 별도로 확인한다.
@@ -0,0 +1,81 @@
# 첫 충전 보너스 신규 지급 중단 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현·검증·독립 리뷰 완료, 미배포 |
| 작성일 | 2026-10-19 |
| 최종 수정일 | 2026-10-19 |
| 대상 제품 | 충전 이벤트 보너스 지급 |
| 작성자·결정권자 | 사용자 요청 |
| 관련 구현 계획 | [plan-task.md](plan-task.md) |
| 관련 review | [plan-task.md 최종 Gate 기록](plan-task.md#p1-gate-최종-판정-2026-10-19) |
## 1. 개요와 문제
변경 전에는 결제 완료 뒤 `ChargeEventJobService`가 인증 회원의 첫 충전을 판정해 원본 충전량의 15%를 올림 계산한 `FIRST_CHARGE` 작업을 만들었다. 이번 변경은 첫 충전 보너스 신규 생성을 중단하되, 이미 저장된 작업과 일반 활성 충전 이벤트의 지급 계약을 유지한다.
사용자 확인 정책: 기존 지급은 정상 동작 중이며, “이미 생성된 것은 기존대로 지급한다.” 이 문서는 정책과 구현 범위를 기록할 뿐이며, 문서 변경 자체는 실제 지급을 중단하지 않는다.
## 2. 목표와 제외 범위
### 목표
- 이후 코드가 만드는 새 보너스 작업은 기존 `ACTIVE_CHARGE_EVENT` 조건을 만족할 때만 생성한다.
- 신규 `FIRST_CHARGE` 작업을 만들지 않는다.
- 저장된 작업은 현재 worker와 관리자의 기존 계약대로 처리한다.
- 현재 `test` 브랜치에서 작업하며 별도 worktree나 브랜치를 생성하지 않는다. 문서 작성 후 사용자 승인에 따라 아래 범위만 구현한다.
### 제외
- 원본 유료 충전, 회원 정보 응답의 `chargeCount`, `isFirstCharged` 광고 추적, 클라이언트 API 변경.
- `FIRST_CHARGE` enum, 기존 작업 스냅샷, 지급·알림·현지화·재시도·멱등성 처리 제거 또는 변경.
- 과거 작업·보너스 데이터 수정/삭제, clawback, DB migration, feature flag, cutoff-date 설정.
- 새 의존성, 광범위한 정리, 사용처가 없는 레거시 서비스 제거.
## 3. 확정 요구사항과 수용 기준
| ID | 상태 | 요구사항 | 수용 기준 |
|---|---|---|---|
| STOP-001 | 확정 | 새 `FIRST_CHARGE` 작업을 생성하지 않는다. | 각 결제 게이트웨이의 검증을 통과한 신규 충전에서 생성되는 작업이 없거나 `ACTIVE_CHARGE_EVENT`뿐이다. 15% 계산 및 첫 충전 사유 스냅샷을 신규 생성 경로에서 사용하지 않는다. |
| STOP-002 | 확정 | 일반 활성 이벤트를 기존 기준으로 유지한다. | 인증 회원의 첫 결제도 같은 `buildActiveEventSnapshot` 경로를 따른다. 활성 이벤트 부재 또는 회원별 횟수 한도 소진이면 작업을 만들지 않는다. 결제 및 일반 충전 처리는 성공한다. |
| STOP-003 | 확정 | 이미 생성된 작업을 기존대로 처리한다. | 저장된 `PENDING`은 worker 대상이다. `FAILED`는 관리자 재시도 후 worker 대상이다. `PROCESSING`에는 자동 복구를 새로 추가하지 않는다. `DONE`은 재지급하지 않는다. 처리 과정은 인증/이벤트 자격을 재계산하지 않고 저장된 gateway, container, amount, method snapshot을 사용한다. |
| STOP-004 | 확정 | 원본 충전 기록과 외부 계약을 보존한다. | 유료 충전, 회원 충전 이력/광고 추적, 공개 API에는 변경이 없다. |
## 4. 기술 범위와 보존 경계
변경 지점은 [`ChargeEventJobService.kt`](../../src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobService.kt)의 `buildSnapshot(charge, member): ChargeEventJob?`이다. `buildActiveEventSnapshot`을 직접 반환하도록 하며, 결제 gateway 검증은 그 호출 전에 유지한다. 이후 인증회원 첫 결제 여부와 무관하게 활성 이벤트 조회 및 횟수 제한을 적용한다.
이 변경으로 더 이상 사용하지 않는 `AuthRepository` 생성자 의존성/import, `buildFirstChargeSnapshot`, `ceil` import를 제거한다. `FIRST_CHARGE_METHOD`는 이 파일에서만 참조되며 제거 대상이다. 단, `FIRST_CHARGE` enum과 `methodSnapshot`을 이용하는 기존 작업 처리 및 `publishPaidNotification` 분기는 보존한다.
[`ChargeEventJob.kt`](../../src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJob.kt), [`ChargeEventJobRepository.kt`](../../src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobRepository.kt), [`ChargeEventJobWorker.kt`](../../src/main/kotlin/kr/co/vividnext/sodalive/v2/can/charge/event/ChargeEventJobWorker.kt), [`AdminChargeEventJobService.kt`](../../src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/event/charge/AdminChargeEventJobService.kt)와 결제 완료 경로는 수정 대상이 아니다. `ChargeEventService.applyChargeEvent`에는 현재 호출자가 없지만 이를 포함한 레거시 서비스는 이 작업에서 제거하지 않는다. 레거시가 참조하는 `AuthRepository` 및 `ChargeRepository` 공용 메서드도 유지한다.
## 5. 배포와 운영 경계
- 적용 기준은 구매 시작일이나 별도 cutoff 설정이 아니라, 새 작업을 만들지 않도록 갱신된 코드가 실제 실행되는 시점이다.
- 구버전 인스턴스가 남아 있는 rolling 배포 중에는 `FIRST_CHARGE` 작업이 더 생길 수 있다. 모든 구버전 생성 인스턴스가 drain된 뒤에야 신규 생성 중단이 완결된다.
- 배포 시점/일정은 이 문서에서 정하지 않는다.
- 배포 후 운영 확인 항목은 cutoff 이후 신규 `FIRST_CHARGE` 작업 부재와 기존 스냅샷 작업의 정상 지속 처리다. 이는 향후 확인 체크리스트이며, 실행된 운영 검증이 아니다.
## 6. 추적과 의사결정
| 요구사항 | 계획 Goal | 검증 |
|---|---|---|
| STOP-001, STOP-002 | `P1-T1` | `ChargeEventJobServiceTest` 신규 생성/활성 이벤트 회귀 |
| STOP-003, STOP-004 | `P1-T1`, `P1-GATE` | worker/admin 회귀, 충전 불변조건 테스트 |
| 날짜 | ID | 상태 | 결정 |
|---|---|---|---|
| 2026-10-19 | DEC-001 | 확정, 사용자 정책 | 첫 충전 보너스 신규 지급 중단, 생성된 작업은 기존대로 지급한다. |
| 2026-10-19 | DEC-002 | 기술 접근 | 신규 스냅샷 선택을 `buildActiveEventSnapshot`으로 단순화하고 기존 저장 작업 처리 경로는 유지한다. |
| 2026-10-19 | DEC-003 | 확정, 사용자 작업 조건 | worktree를 사용하지 않고 현재 `test` 브랜치에서 문서만 작성한다. |
| 2026-10-19 | DEC-004 | 정정, 사용자 구현 승인 | 문서대로 구현 진행 승인. `test` 브랜치/worktree 미사용 조건은 유지하며 신규 생성 분기와 관련 테스트만 변경한다. |
## 7. 성공 기준
- [x] 신규 작업 생성 테스트에서 `FIRST_CHARGE`가 생성되지 않는다.
- [x] 활성 이벤트가 있으면 신규 일반 이벤트 스냅샷만 생성되고, 이벤트 부재/한도 소진이면 작업 없이 종료한다.
- [x] PG, PAYVERSE, APPLE_IAP, GOOGLE_IAP의 기존 활성 이벤트 snapshot과 잔액/알림 계약이 유지된다.
- [x] 저장된 첫 충전 `PENDING`/`PROCESSING` 작업은 현재 방식대로 한 번 처리되고, `FAILED` 수동 재시도와 `DONE` 중복 방지가 유지된다.
- [x] 사용자 승인 이후 구현하고 focused 35개·전체 2,291개 테스트, build·ktlint와 격리 실행 QA 28개 시나리오를 통과했다. 운영 배포·실지급 확인은 별도다.