Test #466
@@ -2,13 +2,13 @@
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 구현·테스트·리뷰 완료, 미배포 |
|
||||
| 상태 | Phase 1·2 구현·검증·리뷰 완료 |
|
||||
| 작성일 | 2026-11-03 (KST) |
|
||||
| 요구사항 기준 | [prd.md](prd.md) |
|
||||
| API 기준 | `prd.md` §4 API 계약 |
|
||||
| 현재 Phase | Phase 1: 결제 완료 정보의 안전한 판정과 전달 |
|
||||
| 현재 Phase | Phase 2: 완료 응답 chargeId 확장 |
|
||||
| 현재 활성 Goal | 없음 |
|
||||
| 다음 Goal | 없음; 운영 적용·클라이언트 검증은 별도 |
|
||||
| 다음 Goal | 없음; 운영 적용·클라이언트·CI 환경 확인은 별도 |
|
||||
|
||||
## 목표
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||
|---:|---|---:|---|---|
|
||||
| 1 | 구현 및 Gate 완료 | 4/4 | 없음 | 운영 DB rollout은 별도 승인 |
|
||||
| 2 | 구현 및 Gate 완료 | 1/1 | 없음 | 운영·클라이언트·CI 환경 검증은 별도 |
|
||||
|
||||
- 사용자 “문서대로 진행” 승인으로 P1-T1부터 실행한다. 완료 증거가 있는 Task만 체크하며 운영 DB 적용·실결제·배포는 하지 않는다.
|
||||
- `P1-T1 -> P1-T2 -> P1-T3 -> P1-T4 -> P1-GATE` 순서로 진행한다. 특히 `ChargeService` 변경은 한 번에 하나의 goal만 실행한다.
|
||||
@@ -238,6 +239,34 @@
|
||||
|
||||
**Expected:** focused tests 성공과 charge event 관련 직접 영향 회귀 성공. 위 조건이 전체 회귀 조건을 만족하지 않으면 전체 `test`/`build`를 관성적으로 실행하지 않고 생략 근거와 대체 검증을 기록한다. H2 test 통과는 MySQL 실제 컬럼 타입 및 운영 lock 격리를 검증하지 않는다.
|
||||
|
||||
### Phase 2: 완료 응답 chargeId 확장
|
||||
|
||||
- [x] **Task 2.1: 기존 충전 ID 반환**
|
||||
|
||||
**Goal 실행 `P2-T1`:** 다섯 완료 API가 기존 네 필드와 실제 저장된 `Charge.id`를 `chargeId: Long`으로 반환한다.
|
||||
|
||||
- **시작 조건:** 사용자 chargeId 추가 구현 승인, Phase 1 완료. 기존 완료 증거와 커밋은 유지한다.
|
||||
- **완료 증거:** 기존 실제 HTTP 테스트가 ID 누락으로 RED, 추가 후 모든 완료 경로·재조회·웹훅 후 verify에서 저장 ID와 일치한다.
|
||||
- **범위 밖:** DB·SQL·결제 처리·격리 수준·Jenkins/Gradle 메모리·Google 재시도 정책·SDK 연동 변경, 커밋·푸시.
|
||||
- **Files:** `src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeControllerIntegrationTest.kt`, 현재 PRD·계획.
|
||||
- **Interfaces:** `ChargeCompleteResponse(chargeId: Long, price: BigDecimal, currencyCode: String, isFirstCharged: Boolean, isIap: Boolean)`; 기존 `completeResponse(charge)`가 저장된 ID를 매핑한다.
|
||||
- [x] **RED:** HTTP 공통 검증을 다섯 key로 확장하고 예상 Charge.id의 정확한 정수 일치를 검사한다. Google은 실제 생성된 행에서 기대 ID를 읽는다. 재조회 및 웹훅 후 verify에서도 같은 ID를 검사한다.
|
||||
- [x] **RED 확인:** `./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.ChargeControllerIntegrationTest.shouldReturnChargeCompleteDataForAllFiveVerificationEndpoints' --offline --console=plain`에서 기존 응답의 chargeId 누락 assertion 실패를 확인한다.
|
||||
- [x] **GREEN:** DTO에 `chargeId: Long` 한 필드, 공통 mapper에 `chargeId = charge.id!!` 한 매핑만 추가한다.
|
||||
- [x] **GREEN 확인:** `./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.*' --offline --console=plain` 및 `./gradlew ktlintCheck --offline --console=plain`로 응답·연결된 충전 회귀를 검증한다.
|
||||
- [x] **REFACTOR:** 추가 추상화 없이 변경 diff와 PRD 현재 계약을 대조한다. 실제 HTTP ID·나머지 필드·생성/webhook 불변 증거를 기록한다.
|
||||
|
||||
#### Phase 2 Gate
|
||||
|
||||
**Goal 실행 `P2-GATE`:** chargeId 후속 변경의 계약·회귀·범위를 최종 검증한다.
|
||||
|
||||
- **시작 조건:** P2-T1 완료.
|
||||
- **완료 증거:** 저장 ID와 응답 ID의 실제 HTTP 일치, Kotlin compile/lint, 요청된 전체 회귀 및 독립 리뷰 결과.
|
||||
- **범위 밖:** Jenkins exit 137의 원인 추정 수정, 운영 DB·실결제·클라이언트 코드 변경.
|
||||
- **TDD 예외 사유/대체 검증:** read-only Gate다. P2-T1의 RED/GREEN과 diff를 검토하고 ULW 전체 검증 명령 `./gradlew test build ktlintCheck --continue --offline --console=plain`을 실행한다. Jenkins 환경을 재현하거나 해결했다고 주장하지 않는다.
|
||||
- [x] 전체 테스트·빌드·lint 및 실제 HTTP evidence를 확인한다.
|
||||
- [x] 독립 리뷰와 작업용 리소스 정리를 완료하고 문서를 갱신한다.
|
||||
|
||||
## 데이터베이스 rollout 참고, 실행 금지
|
||||
|
||||
Task 1.2에서 [20261103_charge_first_charged_ddl.sql](20261103_charge_first_charged_ddl.sql)을 작성했다. 아래 SQL은 해당 파일의 내용이다. Hibernate dialect의 정적 매핑은 테스트했지만 실제 MySQL 환경의 적용·검증은 별도 운영 승인 대상이다. 애플리케이션 배포 전에 컬럼을 먼저 반영해야 한다.
|
||||
@@ -251,6 +280,31 @@ NULL 과거 행은 그대로 둔다. backfill 문장은 없다. 선행 컬럼
|
||||
|
||||
## Progress
|
||||
|
||||
### P2 커밋 직전 재검증, 2026-11-03 KST
|
||||
|
||||
- 사용자 `/commit-with-validation` 요청으로 chargeId 후속 변경의 커밋을 승인받고 전역 공용 절차와 저장소 규칙을 확인했다.
|
||||
- `./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.*' ktlintCheck --offline --console=plain` exit 0, `BUILD SUCCESSFUL in 3m 27s`. 테스트는 실제 실행됐으며 XML 4 suites / 188 tests / failures=0 / errors=0 / skipped=0을 확인했다. compile·lint는 UP-TO-DATE다.
|
||||
- `git diff --check` exit 0. DTO·공통 mapper·HTTP 회귀 테스트를 하나의 변경 단위로, PRD·계획을 계약 및 검증 기록 단위로 커밋한다.
|
||||
- production/test 추가 수정이 없는 커밋 요청이므로 전체 회귀·build는 반복하지 않았다. 현재 XML/HTML은 위 focused 결과로 교체됐다. 운영 DB·실결제·클라이언트 호환성·Jenkins 환경은 이번에도 미검증이다.
|
||||
|
||||
### P2-T1 chargeId 후속 확장, 2026-11-03 KST
|
||||
|
||||
- 사용자 승인: chargeId 추가 제안에 대한 “수정하자”. 기존 PRD·계획에 PAY-006/P2-T1을 먼저 추가했으며 중복 계획 문서는 생성하지 않았다.
|
||||
- RED: 위 다섯 endpoint focused 명령 exit 1, 5 tests / 5 failures / 0 errors / 0 skipped. 실제 HTTP 응답의 key가 기존 네 개여서 chargeId 포함 다섯 key assertion이 실패했다. 테스트 설정/컴파일 오류가 아니다.
|
||||
- 구현: production 변경은 DTO의 `val chargeId: Long`과 공통 `completeResponse`의 `chargeId = charge.id!!` 두 줄뿐이다. 결제·잠금·최초 판정·DB·SQL·CI 설정은 변경하지 않았다.
|
||||
- GREEN: `./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.*' ktlintCheck --continue --offline --console=plain` exit 0, 1m 41s, 4 suites / 188 tests / failures=0 / errors=0 / skipped=0. Kotlin compile와 ktlintCheck를 실행했다. LSP 도구는 제공되지 않는다.
|
||||
- 실제 HTTP: 기존 RANDOM_PORT/TestRestTemplate/JWT/저장소 흐름에서 모든 chargeId는 JSON 정수이며 fixture 또는 Google 생성 행의 ID와 일치했다. true/false/null replay는 같은 ID를 두 번 반환했고 Payverse signed webhook 후 verify도 원래 chargeId를 반환했다. 생성/webhook 응답과 기존 네 필드는 그대로다.
|
||||
- 현재 증거: `build/test-results/test/TEST-kr.co.vividnext.sodalive.can.charge.ChargeControllerIntegrationTest.xml`의 `HTTP EVIDENCE`. 전체 검증 및 독립 리뷰는 P2-GATE에서 이어서 수행한다. Jenkins exit 137은 별도 미해결 환경 조사이며 로컬 성공을 Jenkins 해결로 간주하지 않는다.
|
||||
|
||||
### P2-GATE 후속 최종 검증, 2026-11-03 KST
|
||||
|
||||
- 전체 명령: `./gradlew test build ktlintCheck --continue --offline --console=plain` exit 0, `BUILD SUCCESSFUL in 20m 8s`. 최종 XML 341 suites / 3,128 tests / failures=0 / errors=0 / skipped=0. `test`는 실제 실행됐고 lint·compile은 직전 성공 결과의 UP-TO-DATE를 포함한다.
|
||||
- 실제 HTTP 증거: 기존 22개 HTTP 테스트에서 56회 POST했으며 Google 신규 생성 행, 기존 charge 재조회 및 Payverse 웹훅 선완료 뒤 verify의 ID가 기대 저장 ID와 정확히 일치했다. 예: `/charge/payverse/verify`는 `data={"chargeId":12,"price":9.9900,"currencyCode":"USD","isFirstCharged":true,"isIap":false}`를 반환했다. 같은 PG charge 재조회 두 번은 모두 chargeId 25를 반환했다. 금액·통화·두 boolean 및 생성/webhook shape는 기존 assertion을 유지했다.
|
||||
- 독립 리뷰: 실제 5개 파일 diff, 공통 생성자 및 HTTP assertion·문서 계약을 검토해 Spec PASS / Quality PASS, 차단 사항 없음. 최초 리뷰 결과 전달 오류 후 재검토 응답을 직접 수신해 확인했다. 리뷰 이후 production/test 변경은 없다.
|
||||
- 변경 범위: production 두 줄, 기존 HTTP 테스트의 ID 검증 확장, 기존 PRD·계획 갱신만 포함한다. 새 DB 컬럼·SQL·결제 처리·Jenkins/Gradle 메모리 설정 변경은 없으며 커밋·푸시는 하지 않았다.
|
||||
- 한계: LSP 도구 미제공으로 Kotlin compile·ktlint를 사용했다. 외부 provider는 테스트 대역이며 운영 DB·실결제·클라이언트 호환성·Jenkins 메모리 오류는 미검증이다. 전체 XML/HTML은 `build/test-results/test/`, `build/reports/tests/test/`에 있으며 후속 실행 시 덮어써질 수 있다.
|
||||
- 정리·범위 확인: test worker·Redis 잔류 없음, 작업 전용 Gradle PID 41633 및 자식 Kotlin PID 41668 TERM 종료 후 ps 부재 확인. 전용 `charge-id.qGHVkX` 경로·하위 캐시·임시 로그와 notepad를 제거했다. 공유 캐시/다른 프로세스는 유지했다. `git diff --check` exit 0이며 수정 파일은 DTO·mapper·기존 HTTP 테스트·PRD·계획 5개뿐이다.
|
||||
|
||||
### 커밋 직전 재검증, 2026-11-03 KST
|
||||
|
||||
- 사용자 `/commit-with-validation` 요청으로 전역 공용 절차와 저장소 커밋 규칙을 확인했다.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현·테스트·독립 리뷰 완료, 미배포 |
|
||||
| 문서 상태 | chargeId 후속 확장 포함 구현·검증 완료 |
|
||||
| 작성일 | 2026-11-03 (KST) |
|
||||
| 최종 수정일 | 2026-11-03 (KST) |
|
||||
| 대상 제품 | Sodalive 결제 완료 응답과 최초 유료 충전 판정 |
|
||||
@@ -42,6 +42,7 @@
|
||||
| PAY-003 | 확정 | `Charge.isFirstCharged: Boolean? = null`을 nullable로 저장한다. 요청에서 완료로 처음 전환될 때 회원 잠금 아래 한 번만 판정한다. 같은 회원의 다른 charge 중 다음 두 쌍만 유료 이력이다: `(ChargeStatus.CHARGE, PaymentStatus.COMPLETE)`, `(ChargeStatus.CANCEL, PaymentStatus.RETURN)`. 현재 charge는 제외하고 모든 PG/PAYVERSE/APPLE_IAP/GOOGLE_IAP를 포함한다. 결과 true는 다른 유료 이력이 없음을 뜻한다. 날짜와 ID 순서로 첫 충전을 정하지 않는다. 구현 승인 후 nullable 컬럼 추가 및 `COMMENT`를 포함한 SQL 파일을 작성하되, 과거 행은 NULL로 두고 backfill하지 않는다. `Boolean?`의 실제 MySQL 컬럼 타입 호환성은 rollout 전에 확인한다. | P1-T2, P1-T4 |
|
||||
| PAY-004 | 확정 | 완료 응답에 `isIap: Boolean`을 추가한다. `GOOGLE_IAP`와 `APPLE_IAP`이면 true, `PG`와 `PAYVERSE`이면 false다. 단말 OS와 무관하게 저장된 gateway만 사용한다. `price`, `currencyCode`의 타입·값 원천은 유지한다. | P1-T3, P1-T4 |
|
||||
| PAY-005 | 확정 | 소유권, 최초 판정·동시성·재시도·환불·webhook, 다섯 HTTP 응답 및 기존 추적 효과를 focused TDD와 통합 테스트로 검증한다. 모든 요구사항은 P1-GATE의 증거와 연결한다. | P1-T1~P1-T4, P1-GATE |
|
||||
| PAY-006 | 확정 | 완료 응답에 `chargeId: Long`을 추가한다. 다섯 완료 API에서 실제 저장된 `Charge.id`와 일치하는 non-null JSON 정수를 반환하고 동일 charge 재조회·Payverse 웹훅 선완료 뒤에도 같은 ID를 유지한다. DB·SQL·결제 처리는 변경하지 않는다. | P2-T1, P2-GATE |
|
||||
|
||||
## 4. API 계약
|
||||
|
||||
@@ -54,6 +55,7 @@
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {
|
||||
"chargeId": 123,
|
||||
"price": 9.99,
|
||||
"currencyCode": "USD",
|
||||
"isFirstCharged": true,
|
||||
@@ -65,6 +67,8 @@
|
||||
|
||||
`data.price`는 기존 `BigDecimal`의 JSON number 직렬화다. `currencyCode`는 기존 locale에서 가져오는 통화 문자열이다. `isFirstCharged`와 `isIap`은 non-null JSON boolean이다.
|
||||
|
||||
`data.chargeId`는 서버 충전 건의 `Long` ID이며 JSON 정수로 반환한다. 결제사 거래 ID가 아니고 신규 DB 컬럼도 필요 없다. 기존 Google API는 재요청마다 새 charge를 생성하므로 동일 purchase token의 멱등 키로 보장하지 않는다. 현재 완료 data 계약은 기존 네 필드에 chargeId를 더한 다섯 필드다.
|
||||
|
||||
### 완료 경로
|
||||
|
||||
| Method / path | 완료 결과 | 변경 여부 |
|
||||
@@ -121,9 +125,12 @@ AppsFlyer는 분석에 필요한 DATA를 논의한 맥락일 뿐이다. 서버
|
||||
- [x] PAY-003: PG/PAYVERSE/APPLE_IAP/GOOGLE_IAP 완료 이력, `CANCEL + RETURN`, legacy null, 현재 건 제외, `EVENT + COMPLETE`, replay, webhook-first, rollback을 검증했다. 기존 유료 이력 없는 회원의 동시 완료는 정확히 하나만 true, 이력이 있으면 둘 다 false이며 날짜/ID 순서에 의존하지 않는다. `20261103_charge_first_charged_ddl.sql`은 nullable 단일 BIT(1) 컬럼과 COMMENT만 포함하고 backfill하지 않는다. H2 및 정적 Hibernate 매핑 검증이며 운영 MySQL 적용·검증은 하지 않았다.
|
||||
- [x] PAY-004: 네 gateway에 대해 `isIap` 값이 각각 false/false/true/true이고 OS에 영향받지 않는다. 기존 price 및 currency sourcing, null -> false 응답 규칙을 유지한다.
|
||||
- [x] PAY-005: 기존 charge coupon test와 관리자 환불 test를 포함한 직접 영향 회귀를 통과하고 모든 변경 요구가 P1-GATE에 추적된다.
|
||||
- [x] PAY-006: 실제 HTTP에서 다섯 완료 API의 chargeId가 저장된 Charge.id와 일치한다. 동일 charge 재조회·웹훅 후 verify는 같은 ID이며 기존 금액·통화·첫 결제·IAP 및 생성/webhook 응답을 보존한다.
|
||||
|
||||
완료 근거: 전체 339 suites / 3,112 tests, failures=0, errors=0, skipped=0. `test build ktlintCheck` exit 0 및 독립 리뷰 PASS. 실제 HTTP 56회 증거와 환경 한계는 [plan-task.md](plan-task.md)의 최종 Gate 기록에 있다. 운영 배포 승인을 뜻하지 않는다.
|
||||
|
||||
chargeId 후속 검증: focused 188건 및 최신 전체 341 suites / 3,128 tests, failures/errors/skipped=0, `test build ktlintCheck` exit 0, 독립 리뷰 PASS. 이전 완료 증거는 보존하며 현재 결과는 plan의 P2 기록을 따른다. Jenkins exit 137 해결이나 운영 배포 검증을 의미하지 않는다.
|
||||
|
||||
## 8. 미검증 항목과 Decision Log
|
||||
|
||||
| ID | 상태 | 항목 | 처리 또는 재개 조건 |
|
||||
@@ -142,6 +149,7 @@ AppsFlyer는 분석에 필요한 DATA를 논의한 맥락일 뿐이다. 서버
|
||||
| 2026-11-03 | DEC-005 | 확정 | DEC-004의 문서 전용 승인 범위는 유지한다. 구현을 별도 승인한 뒤 nullable 컬럼 추가와 `COMMENT`를 담은 SQL 파일을 작성한다. SQL 적용·배포·운영 DB 확인은 별도 운영 승인 대상이며 현재 수행하지 않는다. | PAY-003, P1-T2, OPS-001 |
|
||||
| 2026-11-03 | DEC-006 | 정정, 사용자 구현 승인 | “문서대로 진행” 및 ULW 요청에 따라 P1-T1부터 구현·격리 테스트·전체 회귀·HTTP QA를 수행한다. SQL 파일 작성은 포함하며 운영 DB 적용·실결제·배포·커밋·푸시는 별도 승인 경계를 유지한다. | PAY-001~PAY-005, P1-GATE |
|
||||
| 2026-11-03 | DEC-007 | 구현 세부 결정 | 동시 판정의 최신 이력 가시성 확보를 위해 여섯 완료 writer만 READ_COMMITTED로 설정하고, Hibernate MySQL dialect에 맞춰 DDL을 BIT(1)로 작성한다. 추가 temp writer는 범위 밖 관찰로 기록하며 임의 확장하지 않는다. | PAY-003, OPS-001, EXT-003 |
|
||||
| 2026-11-03 | DEC-008 | 확정, 사용자 후속 구현 승인 | 완료 응답에 기존 Charge.id를 chargeId로 추가한다. PAY-001의 기존 네 필드 완료 증거는 유지하고 현재 계약은 다섯 필드로 확장한다. 별도 DB·SQL·결제·CI 설정 변경과 커밋·푸시는 제외한다. | PAY-006, P2-T1, P2-GATE |
|
||||
|
||||
## 9. 계획 추적
|
||||
|
||||
@@ -152,3 +160,4 @@ AppsFlyer는 분석에 필요한 DATA를 논의한 맥락일 뿐이다. 서버
|
||||
| PAY-003 | `ChargeRepositoryTest`, `ChargeServiceIntegrationTest` 이력·동시성·재시도 assertions 및 SQL 산출물의 정적·매핑 정합성 검토 | P1-T2, P1-T4, P1-GATE |
|
||||
| PAY-004 | `ChargeControllerIntegrationTest` gateway mapping 및 price/currency assertions | P1-T3, P1-T4, P1-GATE |
|
||||
| PAY-005 | focused, charge/event/admin 영향 회귀 | P1-T1~P1-T4, P1-GATE |
|
||||
| PAY-006 | `ChargeControllerIntegrationTest`의 저장 ID 일치·재조회·웹훅 후 verify assertions | P2-T1, P2-GATE |
|
||||
|
||||
Reference in New Issue
Block a user