diff --git a/docs/20260724_AI캐릭터_관리자_API/plan-task.md b/docs/20260724_AI캐릭터_관리자_API/plan-task.md index bc897c7b..c133de8b 100644 --- a/docs/20260724_AI캐릭터_관리자_API/plan-task.md +++ b/docs/20260724_AI캐릭터_관리자_API/plan-task.md @@ -14,17 +14,17 @@ | 작성일 | 2026-07-24 | | 요구사항 기준 | `docs/20260724_AI캐릭터_관리자_API/prd.md` | | API 기준 | 이 문서의 `Endpoint Contract Summary` | -| 현재 Phase | Phase 2~3 재검토 및 보완 | -| 현재 활성 Goal | 없음 (`P2-R1`부터 시작) | +| 현재 Phase | Phase 3 6차 리뷰 완료 | +| 현재 활성 Goal | `P4-T1` 대기 | ## 현재 상태 | Phase | 상태 | 기존 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| | 1 | 완료 | 7/7 | 완료 | 없음 | -| 2 | 진행 중 | 2/2 | `P2-R1` | `REV-001`~`REV-003`, `REV-007`, `REV-008` 보완 후 `P2-GATE` | -| 3 | 진행 중 | 2/2 | `P3-R1` | `REV-004`~`REV-008` 보완 후 `P3-GATE` | -| 4 | 대기 | 0/6 | `P4-T1` | `P3-GATE` | +| 2 | 완료 | 11/11 | 완료 | 없음 | +| 3 | 완료 | 15/15 | 완료 | 없음 | +| 4 | 대기 | 0/6 | `P4-T1` | `P3-R5-GATE`, 사용자 진행 지시 | | 5 | 대기 | 0/6 | `P5-T1` | `P4-GATE` | | 6 | 대기 | 0/4 | `P6-T1` | `P5-GATE` | | 7 | 대기 | 0/2 | `P7-T1` | Phase 1~6 Gate 완료 | @@ -189,10 +189,9 @@ Form fields: "image": "File | optional", "request": { "name": "루나", + "systemPrompt": "루나는 달빛을 좋아하는 AI 캐릭터입니다.", "description": "달빛을 좋아하는 AI 캐릭터", - "originalWorkId": 31, - "externalCharacterId": "ext-luna-001", - "isActive": true + "originalWorkId": 31 } } ``` @@ -213,13 +212,14 @@ Form fields: "request": { "name": "루나", "description": "수정된 소개", - "originalWorkId": 31, - "externalCharacterId": "ext-luna-001", - "isActive": false + "originalWorkId": 31 } } ``` +`externalCharacterId`는 response 전용이며 request에 포함하면 400이다. 일반 수정은 `image`와 함께 보낼 수 있으며 image가 없으면 기존 +이미지를 유지한다. soft delete는 `request: {"isActive": false}`만 허용하고 일반 수정 field 또는 image와 혼합하면 400이다. + Response `data`: 캐릭터 상세와 동일하다. `isActive=false`는 soft delete 의미다. #### 오디오 콘텐츠 목록/검색 @@ -1030,7 +1030,7 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - 검증 기록(GREEN): 무엇: 목록/검색 page-size 보정, target resolver 상세 거부, multipart 생성/수정, AI creatorMember 동기화, soft delete, invalid JSON, ADMIN 인가. 왜: Task 2.2 API 계약과 레거시 부작용 재사용을 확인하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: controller 8개와 Task 2.1 baseline 7개가 모두 통과했다. - 검증 기록(REFACTOR): 무엇: 신규 v2 DTO 경계와 Kotlin style. 왜: legacy/public DTO 비노출과 코드 스타일을 확인하기 위해. 어떻게: `./gradlew ktlintCheck`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다. -- [ ] **Task 2.3: Phase 2 요구사항·계약·코드 리뷰** +- [x] **Task 2.3: Phase 2 요구사항·계약·코드 리뷰** **Goal 실행 `P2-R1`:** PRD Feature B와 Endpoint Contract Summary를 Phase 2 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다. @@ -1051,13 +1051,14 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` -- [ ] 목록·검색·상세·생성·수정·비활성화 endpoint와 DTO 필드를 PRD/계약에 1:1로 추적한다. -- [ ] 중복 이름, 외부 API, S3, 원작, 언어 이벤트, creatorMember 동기화와 실패 순서를 코드·test에 추적한다. -- [ ] ADMIN 이중 인가, pagination, multipart/binding, KO/EN/JA 오류, private 정보 비노출 계약을 확인한다. -- [ ] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`의 실제 결과를 기록한다. -- [ ] 후보를 확정·오탐·보류로 판정하고 확정 항목을 아래 세부 Goal에 연결하거나 새 회귀 수정 Goal을 계획에 먼저 추가한다. +- [x] 목록·검색·상세·생성·수정·비활성화 endpoint와 DTO 필드를 PRD/계약에 1:1로 추적한다. +- [x] 중복 이름, 외부 API, S3, 원작, 언어 이벤트, creatorMember 동기화와 실패 순서를 코드·test에 추적한다. +- [x] ADMIN 이중 인가, pagination, multipart/binding, KO/EN/JA 오류, private 정보 비노출 계약을 확인한다. +- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`의 실제 결과를 기록한다. +- [x] 후보를 확정·오탐·보류로 판정하고 확정 항목을 아래 세부 Goal에 연결하거나 새 회귀 수정 Goal을 계획에 먼저 추가한다. + - 검증 기록: 무엇: `P2-R1` Phase 2 character slice read-only 리뷰. 왜: `P2-H1`, `P2-H2` 완료 이력만으로 Phase 2 Gate를 통과할 수 있는지 판정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`은 기존 `P2-T3`~`P2-T6` 보완 Goal에 연결된 확정 finding으로 판정했다. -- [ ] **Task 2.4: 캐릭터 목록·검색·상세 보완** +- [x] **Task 2.4: 캐릭터 목록·검색·상세 보완** **Goal 실행 `P2-T3`:** 캐릭터 조회 API의 검색·pagination·응답·target 계약을 독립적으로 검증하고 확정된 누락을 최소 수정한다. @@ -1073,14 +1074,16 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt` -- [ ] **RED:** 목록 item의 exact JSON key를 고정하고 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출되는 현재 동작을 실패로 재현한다. -- [ ] **RED 확인:** `page=0`, `size` 기본 20·최소 20·최대 50, 검색·`hasNext`와 상세 target 불변식의 경계 test를 실행해 의도한 assertion 실패를 확인한다. -- [ ] **GREEN:** 목록 전용 DTO와 mapper를 최소 구현해 계약 field만 반환하고 credential·token·private path를 노출하지 않는다. -- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 exact 목록 field set과 pagination·target 계약이 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** 상세 UTC field 계약을 유지하면서 목록/상세 DTO 의존 방향을 점검하고 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** 목록 item의 exact JSON key를 고정하고 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출되는 현재 동작을 실패로 재현한다. +- [x] **RED 확인:** `page=0`, `size` 기본 20·최소 20·최대 50, 검색·`hasNext`와 상세 target 불변식의 경계 test를 실행해 의도한 assertion 실패를 확인한다. +- [x] **GREEN:** 목록 전용 DTO와 mapper를 최소 구현해 계약 field만 반환하고 credential·token·private path를 노출하지 않는다. +- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 exact 목록 field set과 pagination·target 계약이 모두 통과하는지 확인한다. +- [x] **REFACTOR:** 상세 UTC field 계약을 유지하면서 목록/상세 DTO 의존 방향을 점검하고 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: 목록 item의 상세 전용 field 비노출 계약. 왜: 목록 응답이 상세 DTO를 재사용해 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 `doesNotExist()` assertion 3개를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: `목록은 음수 page와 최소 미만 size를 기본값으로 보정한다`가 line 66에서 실패해 RED를 확인했다. + - 검증 기록(GREEN/REFACTOR): 무엇: 목록 전용 DTO와 mapper 분리. 왜: 상세 응답 계약을 유지하면서 목록 field set만 Endpoint Contract Summary에 맞추기 위해. 어떻게: `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 추가하고 목록 mapping만 교체한 뒤 같은 focused test와 `./gradlew ktlintCheck`를 실행했다. 결과: 둘 다 `BUILD SUCCESSFUL`이었다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest` -- [ ] **Task 2.5: 캐릭터 생성 흐름 보완** +- [x] **Task 2.5: 캐릭터 생성 흐름 보완** **Goal 실행 `P2-T4`:** 캐릭터 생성의 중복 검증, 외부 API, 이미지, 원작, creatorMember와 이벤트 흐름을 parity 기준으로 완결한다. @@ -1100,15 +1103,18 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt` -- [ ] `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 invalid `characterType`의 canonical request 계약을 Endpoint Contract Summary·legacy 특성화 결과로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다. -- [ ] **RED:** 확정된 문서 JSON의 역직렬화·반영, 동시 중복 이름 생성, 외부 API 실패, S3 실패와 존재하지 않는 `originalWorkId` 실패를 각각 재현한다. -- [ ] **RED 확인:** 실패 지점별 DB row·creatorMember·원작 연결·S3 객체·외부 캐릭터·event 결과와 호출 순서를 단언해 현재 부분 저장 또는 고아 부작용을 확인한다. -- [ ] **GREEN:** 모든 DB 참조를 외부 부작용 전에 검증하고, 동시 중복 정책과 legacy parity에 맞는 최소 보상/정리 경계로 정상 생성과 실패 원자성을 통과시킨다. -- [ ] **GREEN 확인:** 같은 생성 focused/characterization test를 다시 실행해 정상 결과와 실패 지점별 잔존 상태가 확정 계약과 일치하는지 확인한다. -- [ ] **REFACTOR:** creatorMember 표시 정보와 언어 이벤트를 포함한 focused/legacy characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 invalid `characterType`의 canonical request 계약을 Endpoint Contract Summary·legacy 특성화 결과로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다. +- [x] **RED:** 확정된 문서 JSON의 역직렬화·반영, 중복 이름, 외부 API 실패, S3 실패와 존재하지 않는 `originalWorkId` 실패를 각각 재현한다. +- [x] **RED 확인:** 실패 지점별 DB row·creatorMember·원작 연결·S3 객체·외부 캐릭터·event 결과와 호출 순서를 단언해 현재 부분 저장 또는 고아 부작용을 확인한다. +- [x] **GREEN:** 모든 DB 참조를 외부 부작용 전에 검증하고, legacy parity에 맞는 최소 보상/정리 경계로 정상 생성과 실패 원자성을 통과시킨다. +- [x] **GREEN 확인:** 같은 생성 focused/characterization test를 다시 실행해 정상 결과와 실패 지점별 잔존 상태가 확정 계약과 일치하는지 확인한다. +- [x] **REFACTOR:** creatorMember 표시 정보와 언어 이벤트를 포함한 focused/legacy characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: v2 생성 request와 실패 순서 계약. 왜: 외부 ID·생성 활성 상태가 무시되고, 존재하지 않는 원작이 외부 생성 뒤에 실패했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 서버 소유 field, invalid `characterType`, 중복 이름, 원작, 외부 API, S3 실패와 creatorMember·언어 이벤트 assertion을 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 서버 소유 field는 400 기대 대비 200, 원작 실패는 외부 요청 0 기대 대비 1로 실패했다. S3 DB assertion의 최초 실패는 class-level test transaction 관찰 오류였으므로 해당 test만 transaction 밖에서 재실행해 DB rollback을 확인했다. + - 검증 기록(GREEN): 무엇: 서버 소유 create field 거부와 원작 선검증. 왜: `externalCharacterId`/생성 `isActive`를 client 입력으로 받지 않고, 존재하지 않는 원작에서 외부 캐릭터를 만들지 않기 위해. 어떻게: create DTO의 수신 field를 명시적으로 거부하고 facade에서 원작을 외부 호출 전에 조회한 뒤 같은 focused test를 실행했다. 결과: 10개 test가 `BUILD SUCCESSFUL`이었다. + - 검증 기록(REFACTOR): 무엇: 생성 focused/legacy parity 회귀와 formatting. 왜: 정상 원작 연결, AI creatorMember 표시 정보, 언어 감지 event, duplicate·외부 API·S3 실패의 결과를 legacy 특성화와 함께 유지하기 위해. 어떻게: 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused/legacy 명령은 `BUILD SUCCESSFUL in 40s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. 전체 `./gradlew test`는 task 범위가 character create slice이고 focused/legacy 명령으로 직접 영향 범위를 확인하므로 실행하지 않았다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest` -- [ ] **Task 2.6: 캐릭터 수정·비활성화 흐름 보완** +- [x] **Task 2.6: 캐릭터 수정·비활성화 흐름 보완** **Goal 실행 `P2-T5`:** 캐릭터 수정과 `isActive=false`가 표시 정보를 동기화하고 연결 Member·콘텐츠를 보존하도록 완결한다. @@ -1128,15 +1134,17 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt` -- [ ] `externalCharacterId`, 일반 수정과 `isActive=false` 혼합 요청, invalid `characterType`의 canonical update 계약을 확정하고 충돌 시 Decision Log를 먼저 갱신한다. -- [ ] **RED:** 문서 PUT JSON의 field 반영, soft delete와 일반 수정 혼합, image 동시 요청, 외부 수정 성공 후 S3/DB 실패, `updatedAtUtc`의 flush 전 mapping을 각각 재현한다. -- [ ] **RED 확인:** soft delete 성공·실패에서 row·Member·콘텐츠 보존, 미참조 S3 객체 0건, 외부/DB 상태 일치와 응답 timestamp가 후속 GET과 같은지 확인한다. -- [ ] **GREEN:** 확정 계약에 맞춰 혼합 요청을 명시적으로 처리하고, 불필요한 upload를 차단하며 외부/S3/DB 보상 경계와 flush 후 response mapping을 최소 구현한다. -- [ ] **GREEN 확인:** 같은 수정 focused/characterization test를 다시 실행해 field 반영, 보상 결과, soft-delete 보존과 timestamp가 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** creatorMember 표시 정보·번역 event와 legacy `characterType` 동작을 포함한 focused/characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] `externalCharacterId`, 일반 수정과 `isActive=false` 혼합 요청, invalid `characterType`의 canonical update 계약을 확정하고 충돌 시 Decision Log를 먼저 갱신한다. +- [x] **RED:** 문서 PUT JSON의 field 반영, soft delete와 일반 수정 혼합, image 동시 요청, 외부 수정 성공 후 S3/DB 실패, `updatedAtUtc`의 flush 전 mapping을 각각 재현한다. +- [x] **RED 확인:** soft delete 성공·실패에서 row·Member·콘텐츠 보존, 미참조 S3 객체 0건, 외부/DB 상태 일치와 응답 timestamp가 후속 GET과 같은지 확인한다. +- [x] **GREEN:** 확정 계약에 맞춰 혼합 요청을 명시적으로 처리하고, 불필요한 upload를 차단하며 외부/S3/DB 보상 경계와 flush 후 response mapping을 최소 구현한다. +- [x] **GREEN 확인:** 같은 수정 focused/characterization test를 다시 실행해 field 반영, 보상 결과, soft-delete 보존과 timestamp가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** creatorMember 표시 정보·번역 event와 legacy `characterType` 동작을 포함한 focused/characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: PUT의 서버 소유 external ID, `isActive=false` 혼합 image, 존재하지 않는 원작과 flush 전 timestamp 계약. 왜: 기존 구현이 client external ID를 무시하고 soft delete 전에 image를 업로드하며, 원작 검증 후 외부 수정과 이전 `updatedAtUtc`를 반환했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 해당 회귀, image 유지·교체, S3 실패 경계, creatorMember·콘텐츠 보존과 번역 event test를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 16건 중 server-owned ID, mixed soft delete, missing original work, post-flush timestamp 4건이 의도대로 실패했다. + - 검증 기록(GREEN/REFACTOR): 무엇: canonical update 계약과 post-flush 응답. 왜: soft delete의 고아 image를 막고 정상 수정의 creatorMember 동기화·번역 event·timestamp 및 외부/S3/DB 실패 경계를 고정하기 위해. 어떻게: `externalCharacterId` 명시 거부, `isActive=false` 혼합 거부, 원작 선검증, `flush()` 후 response mapping을 적용한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: 모두 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 변경 범위가 character update slice이고 지정 focused/legacy 회귀가 직접 영향 범위를 포함하므로 실행하지 않았다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest` -- [ ] **Task 2.7: Phase 2 보안·오류·회귀 보완** +- [x] **Task 2.7: Phase 2 보안·오류·회귀 보완** **Goal 실행 `P2-T6`:** Phase 2의 모든 endpoint가 공통 ADMIN·오류·CORS 계약을 공유하고 legacy/public 계약을 회귀시키지 않음을 고정한다. @@ -1153,28 +1161,32 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` -- [ ] **RED:** `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 잡혀 404 대신 400이 되는 KO/EN/JA·허용 Origin CORS 4건을 현재 Phase 1 오류 계약 test로 재현한다. -- [ ] **RED:** 목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim과 binding·multipart·domain/client/server 오류의 exact status/key/KO·EN·JA를 parameterized test로 고정한다. -- [ ] **RED 확인:** 오류 계약과 실제 endpoint matrix를 실행해 404 회귀 4건과 누락된 인가·i18n assertion이 의도대로 실패하는지 확인한다. -- [ ] **GREEN:** numeric `characterId`만 resource handler에 매핑되도록 최소 수정하고, Phase 2 오류 의미를 확정된 message key와 `ApiResponse.error`로 반환한다. -- [ ] **GREEN 확인:** 같은 오류·인가 focused test를 다시 실행해 실제 endpoint의 status/header/envelope와 KO/EN/JA가 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminCharacterServiceTest` 참조 및 과거 test 수 기록은 삭제하지 않고 정정 기록을 누적한다. -- [ ] 기존 admin/public character contract, 신규 DTO 의존 방향과 Phase 2 focused test·`ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 잡혀 404 대신 400이 되는 KO/EN/JA·허용 Origin CORS 4건을 현재 Phase 1 오류 계약 test로 재현한다. +- [x] **RED:** 목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim과 binding·multipart·domain/client/server 오류의 exact status/key/KO·EN·JA를 parameterized test로 고정한다. +- [x] **RED 확인:** 오류 계약과 실제 endpoint matrix를 실행해 404 회귀 4건과 누락된 인가·i18n assertion이 의도대로 실패하는지 확인한다. +- [x] **GREEN:** numeric `characterId`만 resource handler에 매핑되도록 최소 수정하고, Phase 2 오류 의미를 확정된 message key와 `ApiResponse.error`로 반환한다. +- [x] **GREEN 확인:** 같은 오류·인가 focused test를 다시 실행해 실제 endpoint의 status/header/envelope와 KO/EN/JA가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminCharacterServiceTest` 참조 및 과거 test 수 기록은 삭제하지 않고 정정 기록을 누적한다. +- [x] 기존 admin/public character contract, 신규 DTO 의존 방향과 Phase 2 focused test·`ktlintCheck` 결과를 Progress에 기록한다. + - 정정 기록: `AiCharacterAdminCharacterServiceTest`는 현재 존재하지 않는 과거 계획 참조다. P2-T6의 실제 범위는 `AiCharacterAdminCharacterControllerTest`, `AiCharacterAdminCharacterControllerMutationTest`, 공통 `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`이며 기존 완료 이력과 과거 test 수 기록은 삭제하지 않았다. + - 검증 기록(RED): 무엇: 실제 character controller가 문자열 미매핑 경로를 404 fallback으로 넘기는지와 endpoint matrix. 왜: `/{characterId}`의 `Long` binding이 fallback 404 계약을 400으로 바꾸고 있었기 때문이다. 어떻게: 아래 Verify 명령을 production 변경 전 실행했다. 결과: 전체 146건 중 `unmapped-path` KO/EN/JA와 허용 Origin CORS 4건만 404 기대 대비 400으로 실패했고, 새 목록 binding·상세 target·생성/수정 multipart i18n 및 실제 endpoint non-ADMIN/CORS assertion은 통과했다. + - 검증 기록(GREEN/REFACTOR): 무엇: numeric path 제약과 Phase 2 ADMIN/error/CORS/legacy 회귀. 왜: 문자열 segment는 fallback 404로, 숫자 resource와 기존 public/legacy는 기존 계약으로 유지해야 하기 때문이다. 어떻게: controller의 GET/PUT path를 `[0-9]+`로 제한한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused command는 `BUILD SUCCESSFUL in 1m 26s`, ktlint는 `BUILD SUCCESSFUL in 34s`였고, P2-GATE는 이 Task 범위 밖으로 미완료 상태를 유지한다. - Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` #### Phase 2 Gate **Goal 실행 `P2-GATE`:** Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름과 legacy 회귀를 최종 판정한다. -- [ ] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. +- [x] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P2-R1`, `P2-T3`~`P2-T6` 완료 또는 근거 있는 `해당 없음` 판정. - **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록. - **범위 밖:** Gate 통과를 위한 test 삭제·완화, Phase 3 기능 수정. -- [ ] `REV-001`~`REV-003`, `REV-007`, `REV-008`의 계약 결정·failure matrix·수정 test와 실제 결과가 각 소유 Goal의 Progress에 연결됐다. -- [ ] 캐릭터 목록 exact key, 문서 mutation JSON, 동시 중복 결과, 외부/S3/DB 보상, post-flush `updatedAtUtc`와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다. -- [ ] 완료 이력의 누락 test 파일·test 수·failure-order 증거는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다. +- [x] `REV-001`~`REV-003`, `REV-007`, `REV-008`의 계약 결정·failure matrix·수정 test와 실제 결과가 각 소유 Goal의 Progress에 연결됐다. +- [x] 캐릭터 목록 exact key, 문서 mutation JSON, 동시 중복 결과, 외부/S3/DB 보상, post-flush `updatedAtUtc`와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다. +- [x] 완료 이력의 누락 test 파일·test 수·failure-order 증거는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다. + - 검증 기록: 무엇: `P2-GATE` Phase 2 최종 판정. 왜: `P2-R1`, `P2-T3`~`P2-T6`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 실행했다. 결과: character focused 명령은 최초 병렬 실행 중 XML test result write 충돌로 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `git diff --check`도 통과했다. ```bash ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' @@ -1184,6 +1196,189 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 **Expected:** 모든 명령 exit code 0, Phase 2 review의 후보·확정 미처리 항목 0건, 관련 legacy/public 계약 diff 없음. +#### Phase 2 후속 리뷰 보완 + +- [x] **Task 2.8: 실제 character endpoint 보안·오류·실패 경계 증거 보강** + +**Goal 실행 `P2-R2`:** `REV-009`에서 확인한 실제 character endpoint별 인가·CORS·오류와 mutation 실패 경계를 non-vacuous 회귀 test로 고정한다. + +- **추적 review ID:** `REV-009`. +- **시작 조건:** 기존 `P2-GATE` 완료 이력과 `phase2-character-review.md` 2차 리뷰 판정 존재. +- **완료 증거:** 아래 실제 endpoint test, 공통 authorization/error 회귀, `ktlintCheck`와 Progress 기록. +- **범위 밖:** 공통 Phase 1 security/error 재설계, external API 보상 endpoint·신규 DDL 추가, Phase 3 기능. +- **TDD 예외 사유:** 현재 production 실패가 아니라 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** 실제 endpoint test를 먼저 추가하고, 현재 동작이 계약을 만족하면 production code 변경 없이 통과 증거를 기록한다. 계약 불일치가 재현될 때만 해당 assertion의 RED를 확인하고 최소 수정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **CONTRACT TEST:** 목록·상세·생성·수정 실제 경로에서 JWT role × 현재 DB role과 stale ADMIN claim을 parameterized test로 고정한다. +- [x] **CONTRACT TEST:** 실제 GET/POST/PUT의 허용·거부 Origin/preflight와 대표 binding·domain·client·server 오류의 exact status/key/KO·EN·JA envelope를 고정한다. +- [x] **FAILURE TEST:** 중복·원작·external API·S3·DB 실패에서 DB/creatorMember/originalWork/S3/external/event 결과를 직접 단언하고 기존 non-compensated external 경계를 유지한다. +- [x] **GREEN:** 새 test가 현재 계약 불일치를 재현할 때만 가장 작은 production 수정으로 통과시키고, 이미 통과하면 production code를 변경하지 않는다. +- [x] **REFACTOR:** character focused와 legacy characterization, 공통 authorization/error 및 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록: 무엇: `REV-009`의 실제 character endpoint 증거 보강. 왜: 기존 Gate 기록이 네 endpoint의 stale claim, allow/deny CORS preflight, KO/EN/JA 실패 envelope를 직접 매트릭스로 고정했다는 증거가 부족했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 목록·상세·생성·수정 실제 경로의 stale ADMIN claim 403과 허용/거부 Origin preflight를 추가하고, `AiCharacterAdminCharacterControllerMutationTest`의 external API 실패, 생성 S3 실패, 수정 S3 실패를 KO/EN/JA envelope와 잔존 DB/S3/external 상태 단언으로 확장했다. 결과: production code 변경 없이 아래 focused 명령이 `BUILD SUCCESSFUL in 1m 47s`였다. + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 2 후속 리뷰 Gate + +**Goal 실행 `P2-R2-GATE`:** `REV-009`의 직접 증거를 재검토하고 Phase 2 후속 리뷰를 종결한다. + +- [x] **`P2-R2-GATE` 완료:** `P2-R2` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다. +- **시작 조건:** `P2-R2` 완료. +- **완료 증거:** `REV-009` 수정 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화. +- **범위 밖:** 기존 `P2-GATE` 이력 수정, Phase 3 production 변경. + - 검증 기록: 무엇: `P2-R2-GATE` 후속 리뷰 종결. 왜: `REV-009`의 직접 증거가 추가됐고 Phase 3 후속 보완으로 넘어갈 수 있는지 판정하기 위해. 어떻게: `phase2-character-review.md`에 3차 후속 검증 기록을 누적하고 위 focused 명령을 fresh 실행했다. 결과: `BUILD SUCCESSFUL in 1m 47s`였고, `REV-009`는 처리 완료로 판정했다. `ktlintCheck`는 Phase 3 후속 보완까지 완료한 뒤 공통으로 실행해 전체 후속 범위 검증 기록에 남긴다. + +#### Phase 2 4차 리뷰 보완 + +- [x] **Task 2.9: character DB·event 실패 경계 증거 보강** + +**Goal 실행 `P2-R3`:** `REV-012`에서 남은 character 생성·수정의 DB/event 실패 후 내부·외부 부작용 경계를 실제 흐름으로 고정한다. + +- **추적 review ID:** `REV-012`. +- **시작 조건:** 기존 `P2-R2-GATE` 완료 이력과 `phase2-character-review.md` 4차 리뷰 판정 존재. +- **완료 증거:** 실제 mutation failure test, character/common 회귀, `ktlintCheck`와 Progress 기록. +- **범위 밖:** external character API 보상 endpoint 추가, 신규 DDL, Phase 3 이후 production 변경. +- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 2.8` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** 실제 DB flush/save 또는 event publish 실패를 먼저 재현하고, 현재 transaction·비보상 경계가 계약과 일치하면 production code 변경 없이 관찰 결과를 고정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **FAILURE CHARACTERIZATION:** 생성·수정의 DB flush/save와 event publish 실패를 실제 transaction 경계에서 재현한다. +- [x] **CONTRACT TEST:** 각 실패 뒤 ChatCharacter·creatorMember·originalWork·event와 이미 발생한 external/S3 결과를 직접 단언한다. +- [x] **CONTRACT TEST:** 대표 실패의 exact HTTP status와 KO/EN/JA `ApiResponse.error`를 실제 mutation endpoint에서 확인한다. +- [x] **GREEN:** 현재 계약 위반이 재현될 때만 최소 production 수정으로 통과시키고, 기존 비보상 경계와 일치하면 test-only로 종료한다. +- [x] **REFACTOR:** character/common focused 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다. + - 검증 기록: 무엇: `REV-012`의 character 생성·수정 실패 경계 증거를 보강했다. 왜: 기존 완료 기록이 external/S3 실패는 확인했지만 event publish 실패와 KO/EN/JA 대표 실패 경계를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 external/S3 실패 locale matrix와 facade 직접 event 실패 특성화를 추가했다. 결과: production code 변경 없이 mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다. + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 2 4차 리뷰 Gate + +**Goal 실행 `P2-R3-GATE`:** `REV-012`의 DB/event 실패 경계 증거를 재검토하고 Phase 2 4차 리뷰를 종결한다. + +- [x] **`P2-R3-GATE` 완료:** `P2-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다. +- **시작 조건:** `P2-R3` 완료. +- **완료 증거:** `REV-012` 처리 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화. +- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경. + - 검증 기록: 무엇: Phase 2 4차 리뷰의 `REV-012` 처리를 종결했다. 왜: Phase 3 4차 보완으로 넘어가기 전 character failure evidence 완료 여부를 판정하기 위해. 어떻게: character mutation focused test와 최종 `ktlintCheck`를 실행하고 `phase2-character-review.md`를 처리 완료로 갱신했다. 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다. + +#### Phase 2 5차 리뷰 보완 + +- [x] **Task 2.10: character 실제 transaction DB·event 실패 경계 완결** + +**Goal 실행 `P2-R4`:** `REV-015`의 생성·수정 persistence/event 실패를 actual endpoint와 Spring transaction 경계에서 재현하고 내부 rollback·외부 비보상 결과를 고정한다. + +- **추적 review ID:** `REV-015`. +- **시작 조건:** 기존 `P2-R3-GATE` 완료 이력과 `phase2-character-review.md` 5차 리뷰 판정 존재. +- **완료 증거:** actual POST/PUT failure test, transaction 종료 뒤 DB 재조회, character/common 회귀와 `ktlintCheck` 결과 및 Progress 기록. +- **범위 밖:** external character API 보상 endpoint, S3 object 정리 정책, 신규 DDL, Phase 3 이후 production 변경. +- **TDD 예외 사유:** 현재 production 결함보다 기존 `REV-012` 완료 기록의 transaction/rollback 직접 증거 누락이 확정된 회귀 검증 Task다. +- **대체 검증 방법:** test transaction 밖 actual endpoint와 repository/publisher failure injection으로 production proxy를 통과시키고, 요청 종료 뒤 내부 DB와 외부 interaction을 재조회한다. 계약 위반이 재현될 때만 최소 production 수정으로 전환한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **FAILURE CHARACTERIZATION:** `@Transactional(propagation = Propagation.NOT_SUPPORTED)` actual POST/PUT에서 `ApplicationEventPublisher` 실패를 주입하고 500 `common.error.unknown` envelope을 확인한다. +- [x] **PERSISTENCE FAILURE:** `@SpyBean ChatCharacterRepository`로 생성 `save`와 수정 `flush()` 실패를 각각 주입해 external 호출 뒤 transaction rollback 순서를 재현한다. +- [x] **CONTRACT TEST:** 생성 실패 뒤 ChatCharacter·creatorMember·originalWork 부재, 수정 실패 뒤 기존 character·creatorMember·originalWork 상태 유지, S3/event interaction과 external 호출 횟수를 직접 단언한다. +- [x] **CONTRACT TEST:** 대표 event/persistence 실패의 KO/EN/JA exact HTTP status/message를 actual mutation endpoint에서 확인한다. +- [x] **GREEN:** 현재 transaction 계약 위반이 재현될 때만 가장 작은 production 수정으로 통과시키고, 기존 rollback·비보상 경계와 일치하면 test-only로 종료한다. +- [x] **REFACTOR:** direct `createFacade` event failure test를 actual endpoint 증거로 대체하거나 역할을 명확히 축소하고 character/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +- 검증 기록: 무엇: `REV-015`의 생성·수정 event/save/flush 실패가 실제 MockMvc POST/PUT와 facade transaction 경계를 통과해 내부 DB rollback과 외부 비보상 호출을 보이는지 고정했다. 왜: direct facade 호출은 Spring transaction proxy 및 요청 종료 뒤 DB 상태를 증명하지 못했기 때문이다. 어떻게: `NOT_SUPPORTED` test에서 facade proxy target의 publisher mock과 `@SpyBean ChatCharacterRepository` failure를 주입하고, `TransactionTemplate` 재조회로 상태를 확인했다. 결과: 초기 RED는 `@MockBean`이 이미 생성된 facade field를 대체하지 못해 실제 event listener가 실행되고 200이 반환된 것으로 확인됐으며, 실제 proxy target에 같은 mock을 교체한 뒤 KO/EN/JA 500 envelope, 생성 내부 state 부재, 수정 기존 state 유지, 외부 호출 1회와 image 없는 S3 미호출이 통과했다. production 변경은 없었다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 8s`, 영향 범위 character/auth/error 회귀는 `BUILD SUCCESSFUL in 1m 59s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 42s`였다. + +#### Phase 2 5차 리뷰 Gate + +**Goal 실행 `P2-R4-GATE`:** `REV-015`의 actual transaction·rollback 증거를 재검토하고 Phase 2 5차 리뷰를 종결한다. + +- [x] **`P2-R4-GATE` 완료:** `P2-R4` 완료 후 위 명령을 fresh 실행하고 리뷰 문서·Progress를 갱신한다. +- **시작 조건:** `P2-R4` 완료. +- **완료 증거:** `REV-015` 수정 완료, actual endpoint transaction evidence, focused/영향 범위 회귀와 lint 성공. +- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경. + - 진행 기록: `P2-R4` 구현과 fresh 검증을 완료했고, 5차 재리뷰에서 `REV-015` 보완 완료와 Phase 2 Gate 종료를 확인했다. + +#### Phase 2 6차 리뷰 보완 + +- [x] **Task 2.11: 캐릭터 생성 Endpoint Contract Summary 동기화** + +**Goal 실행 `P2-R5`:** `REV-018`의 캐릭터 생성 예시를 `DEC-P2-T4-001` 및 production request 계약과 일치시키고 +actual endpoint 회귀로 확인한다. + +- **추적 review ID:** `REV-018`. +- **시작 조건:** 기존 `P2-R4-GATE` 완료 이력과 `phase2-character-review.md` 6차 리뷰 판정 존재. +- **완료 증거:** Endpoint Contract Summary 생성 JSON 정정, 정상 생성·서버 소유 field 거부 actual endpoint 회귀, + `git diff --check`와 Progress 기록. +- **범위 밖:** production DTO/facade 변경, 캐릭터 생성 behavior 변경, 외부 API 계약 변경, Phase 3 이후 production 변경. +- **TDD 예외 사유:** production과 기존 actual endpoint test는 확정 계약을 충족하고 문서 예시만 반대로 남은 문서 정합성 + 수정 Task다. +- **대체 검증 방법:** 생성 예시의 exact field를 문자열 검색으로 확인하고 기존 정상 생성·서버 소유 field 거부 actual endpoint + test를 재실행한다. + +**Files:** + +- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` + +- [x] **CONTRACT DOC:** POST 생성 예시에 필수 `systemPrompt`를 추가하고 request의 `externalCharacterId`, `isActive`를 + 제거한다. +- [x] **CONTRACT DOC 확인:** 생성 예시와 `DEC-P2-T4-001`을 대조해 request field와 response 전용 field가 일치하는지 + 확인한다. +- [x] **CONTRACT TEST:** `systemPrompt`를 포함하고 서버 소유 field를 제외한 actual POST가 성공하며 response에 외부 API가 + 반환한 `externalCharacterId`와 서버 생성 `isActive=true`가 있는지 확인한다. +- [x] **REJECTION TEST:** `externalCharacterId` 또는 생성 `isActive`가 포함된 actual POST가 외부/S3/DB/event 부작용 전 + 400으로 거부되는 기존 회귀를 확인한다. +- [x] **REFACTOR:** production 변경 없이 문서 diff와 focused test 결과를 Progress에 기록한다. + - 검증 기록: 무엇: `REV-018`의 캐릭터 생성 Endpoint Contract Summary 예시를 production 생성 request 계약과 동기화했다. 왜: 예시가 필수 `systemPrompt`를 누락하고 서버 소유 `externalCharacterId`, `isActive`를 포함했기 때문이다. 어떻게: `rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md`로 예시 field를 확인하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 예시는 `systemPrompt` 포함 및 서버 소유 field 제외로 확인됐고 focused test는 `BUILD SUCCESSFUL in 30s`였다. + +```bash +rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest +git diff --check +``` + +#### Phase 2 6차 리뷰 Gate + +**Goal 실행 `P2-R5-GATE`:** `REV-018`의 문서 계약 정합성을 재검토하고 Phase 2 6차 리뷰를 종결한다. + +- [x] **`P2-R5-GATE` 완료:** `P2-R5` 완료 후 위 문서/focused 검증을 fresh 실행하고 리뷰 문서·Progress를 갱신한다. +- **시작 조건:** `P2-R5` 완료. +- **완료 증거:** `REV-018` 처리 완료, Endpoint Contract Summary·Decision Log·production 계약 일치, focused test와 + diff check 성공. +- **범위 밖:** Gate에서 production code 수정, 기존 Phase 2 완료 이력 변경, Phase 3 production 변경. + - 검증 기록: 무엇: `P2-R5-GATE`에서 `REV-018` 문서 계약 정합성을 종결했다. 왜: Phase 3 6차 보완의 시작 조건이 `P2-R5-GATE` 완료이기 때문이다. 어떻게: `P2-R5` focused 검증 결과와 `phase2-character-review.md` 6차 판정을 대조했다. 결과: production 변경 없이 `REV-018` 처리 완료로 판정했다. + --- ### Phase 3: 오디오 콘텐츠 관리와 signed URL vertical slice @@ -1294,7 +1489,7 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - 재검증(2026-07-26): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 검토 보완(2026-07-27): 생성/수정의 `seriesIds` owner-scoped 연결, 생성의 ISO-8601 `releaseDateUtc` 변환, POST 제공 문서 불일치를 보완했다. 수정 `audioFile` 교체는 기존 creator/admin 수정 pipeline에 없어 Phase 3에서 미지원으로 명시했다. -- [ ] **Task 3.3: Phase 3 요구사항·계약·코드 리뷰** +- [x] **Task 3.3: Phase 3 요구사항·계약·코드 리뷰** **Goal 실행 `P3-R1`:** PRD Feature C와 Endpoint Contract Summary를 콘텐츠 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다. @@ -1314,13 +1509,14 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` -- [ ] 테마·목록·검색·상세·생성·수정·soft delete endpoint와 DTO 필드를 PRD/계약에 추적한다. -- [ ] signed URL TTL/path, private path 비노출, viewer 상태 기본값을 production·test에 추적한다. -- [ ] 생성/update pipeline, 파일, 가격, 공개·예약, 번역·알림, `seriesIds`, 날짜 변환과 실패 순서를 확인한다. -- [ ] owner 검증, no-side-effect, ADMIN 인가, 오류 i18n, pagination/multipart 계약을 확인한다. -- [ ] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` 결과와 후보 판정을 리뷰 문서에 기록한다. +- [x] 테마·목록·검색·상세·생성·수정·soft delete endpoint와 DTO 필드를 PRD/계약에 추적한다. +- [x] signed URL TTL/path, private path 비노출, viewer 상태 기본값을 production·test에 추적한다. +- [x] 생성/update pipeline, 파일, 가격, 공개·예약, 번역·알림, `seriesIds`, 날짜 변환과 실패 순서를 확인한다. +- [x] owner 검증, no-side-effect, ADMIN 인가, 오류 i18n, pagination/multipart 계약을 확인한다. +- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` 결과와 후보 판정을 리뷰 문서에 기록한다. + - 검증 기록: 무엇: Phase 3 오디오 콘텐츠 slice의 read-only 요구사항·계약·코드 리뷰. 왜: `P3-H1`, `P3-H2` 완료 이력 이후 `REV-004`~`REV-008`의 실제 확정 여부와 후속 소유 Goal을 고정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`에 PRD Feature C, Endpoint Contract Summary, production/test 대조표와 발견 사항을 기록하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 2s`였고, `REV-004`~`REV-008`은 각각 `P3-T3`~`P3-T7`의 기존 소유 Goal로 연결했다. 리뷰 Task이므로 production code는 수정하지 않았다. -- [ ] **Task 3.4: 활성 콘텐츠 테마 API 보완** +- [x] **Task 3.4: 활성 콘텐츠 테마 API 보완** **Goal 실행 `P3-T3`:** 활성 콘텐츠 테마를 전용 `themeId/themeName/imageUrl` DTO로 반환하는 관리자 API를 완결한다. @@ -1336,14 +1532,15 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt` -- [ ] **RED:** 테마 endpoint 부재 또는 계약 불일치와 legacy field 노출을 재현하는 가장 작은 실패 test를 작성한다. -- [ ] **RED 확인:** focused test를 실행해 의도한 route·field assertion 실패를 확인한다. -- [ ] **GREEN:** 활성 테마만 `themeId`, `themeName`, `imageUrl`로 반환하는 최소 구현을 작성한다. -- [ ] **GREEN 확인:** request body 없음, exact field set과 ADMIN 이중 인가를 포함한 focused test 성공을 확인한다. -- [ ] **REFACTOR:** v2 DTO 경계만 정리하고 테마 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** 테마 endpoint 부재 또는 계약 불일치와 legacy field 노출을 재현하는 가장 작은 실패 test를 작성한다. +- [x] **RED 확인:** focused test를 실행해 의도한 route·field assertion 실패를 확인한다. +- [x] **GREEN:** 활성 테마만 `themeId`, `themeName`, `imageUrl`로 반환하는 최소 구현을 작성한다. +- [x] **GREEN 확인:** request body 없음, exact field set과 ADMIN 이중 인가를 포함한 focused test 성공을 확인한다. +- [x] **REFACTOR:** v2 DTO 경계만 정리하고 테마 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록: 무엇: `P3-T3`의 활성 콘텐츠 테마 endpoint 계약 증거를 전용 focused test로 분리했다. 왜: production 동작은 이미 `AiCharacterAdminAudioContentController`/`Facade`/DTO에서 충족하고 있었지만, `REV-007`, `REV-008` 기준 Gate 증거가 단일 대형 controller test에 섞여 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentThemeControllerTest`를 추가해 활성 필터, orders 정렬, `themeId/themeName/imageUrl` exact field, legacy `id/theme/image` 비노출과 anonymous 401을 검증하고 기존 controller test의 중복 테마 케이스를 제거했다. 결과: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`는 `BUILD SUCCESSFUL in 35s`, 테마+기존 controller focused 회귀는 `BUILD SUCCESSFUL in 48s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. production code는 추가하지 않았다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest` -- [ ] **Task 3.5: 콘텐츠 목록·상세·signed URL 보완** +- [x] **Task 3.5: 콘텐츠 목록·상세·signed URL 보완** **Goal 실행 `P3-T4`:** owner-scoped 콘텐츠 조회와 signed URL·상세 DTO 계약을 독립적으로 완결한다. @@ -1362,16 +1559,18 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt` -- [ ] **RED:** `purchaseOption=RENT_ONLY`, entity `isOnlyRental=true`, 미래·과거 `releaseDate` 조합에서 legacy 상세와 다른 `isOnlyRental`, `purchaseOption`, `releaseDate`를 재현한다. -- [ ] **RED:** 응답의 creator·buyer·other content·comment·translation 중첩 타입이 legacy/public DTO package에 직접 의존하는 현재 경계를 검출하고 exact JSON key를 고정한다. -- [ ] **RED 확인:** query/legacy baseline test를 실행해 세 compatibility field와 금지 DTO 의존이 의도대로 실패하는지 확인한다. -- [ ] **GREEN:** legacy 파생 규칙과 현지화된 `releaseDate` 의미를 유지하고 UTC 원본은 `releaseDateUtc`에만 반환하며, 동일 JSON을 v2 전용 중첩 DTO로 최소 매핑한다. -- [ ] **GREEN 확인:** 같은 query/legacy baseline test를 다시 실행해 legacy compatibility field, `releaseDateUtc`와 exact JSON schema가 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** owner·검색·status·pagination, 활성 owner-scoped `seriesIds`, viewer 기본값, signed URL TTL/path와 private 정보 비노출을 함께 회귀한다. -- [ ] 조회/signed URL focused test와 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** `purchaseOption=RENT_ONLY`, entity `isOnlyRental=true`, 미래·과거 `releaseDate` 조합에서 legacy 상세와 다른 `isOnlyRental`, `purchaseOption`, `releaseDate`를 재현한다. +- [x] **RED:** 응답의 creator·buyer·other content·comment·translation 중첩 타입이 legacy/public DTO package에 직접 의존하는 현재 경계를 검출하고 exact JSON key를 고정한다. +- [x] **RED 확인:** query/legacy baseline test를 실행해 세 compatibility field와 금지 DTO 의존이 의도대로 실패하는지 확인한다. +- [x] **GREEN:** legacy 파생 규칙과 현지화된 `releaseDate` 의미를 유지하고 UTC 원본은 `releaseDateUtc`에만 반환하며, 동일 JSON을 v2 전용 중첩 DTO로 최소 매핑한다. +- [x] **GREEN 확인:** 같은 query/legacy baseline test를 다시 실행해 legacy compatibility field, `releaseDateUtc`와 exact JSON schema가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** owner·검색·status·pagination, 활성 owner-scoped `seriesIds`, viewer 기본값, signed URL TTL/path와 private 정보 비노출을 함께 회귀한다. +- [x] 조회/signed URL focused test와 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: 상세 응답의 legacy `releaseDate` 의미, RENT_ONLY 파생값과 v2 전용 중첩 DTO 경계. 왜: `REV-005`, `REV-007`에서 상세 DTO가 legacy/public 중첩 DTO에 직접 의존하고, 과거 공개일을 legacy `releaseDate`에도 노출하고 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentQueryTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest`를 실행했다. 결과: 2건이 의도대로 실패했다. 하나는 `releaseDate`가 존재해서 실패했고, 하나는 legacy nested DTO package 누출 assertion으로 실패했다. + - 검증 기록(GREEN/REFACTOR): 무엇: 상세 응답의 `releaseDateUtc` 전용 노출, RENT_ONLY 파생 규칙, v2 전용 중첩 DTO. 왜: 관리자 상세는 UTC 원본을 `releaseDateUtc`에만 고정하고, response DTO는 legacy/public DTO 타입을 외부 계약으로 재노출하지 않아야 하기 때문이다. 어떻게: `AiCharacterAdminAudioContentDto`에 v2 중첩 response DTO를 추가하고, `AiCharacterAdminAudioContentMapper`의 `releaseDate`, `isOnlyRental`, `purchaseOption`, `creator` mapping만 최소 수정했다. 결과: `AiCharacterAdminAudioContentQueryTest`는 `BUILD SUCCESSFUL in 29s`, 계획서 Verify 묶음은 `BUILD SUCCESSFUL in 41s`, `./gradlew ktlintCheck`는 최초 unused import 2건으로 실패 후 정리 재실행에서 `BUILD SUCCESSFUL in 10s`였다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AudioContentCloudFrontCharacterizationTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest` -- [ ] **Task 3.6: 콘텐츠 생성 pipeline 보완** +- [x] **Task 3.6: 콘텐츠 생성 pipeline 보완** **Goal 실행 `P3-T5`:** 콘텐츠 생성의 multipart 입력, legacy field 변환, 파일·processing·series 연결과 side effect parity를 완결한다. @@ -1389,16 +1588,18 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` -- [ ] `tags` 필수 여부와 생성 `isActive=false`의 canonical 계약을 legacy pipeline·Endpoint Contract Summary로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다. -- [ ] **RED:** `coverImage`, `audioFile`, `request` 각 part 누락에서 Kotlin nullable 때문에 `MissingServletRequestPartException`이 발생하지 않는 현재 binding과 KO/EN/JA envelope 차이, facade·DB·S3·event 호출 0건 기대를 재현한다. -- [ ] **RED:** 생성 request 전체 field, `description/releaseDateUtc` 변환, `tags` 누락, `isActive=false`, target·theme·`seriesIds` 오류와 S3/processing/event 실패 순서를 각각 고정한다. -- [ ] **RED 확인:** create/error/legacy characterization test를 실행해 part별 exception·field 계약·failure order가 의도대로 실패하는지 확인한다. -- [ ] **GREEN:** 필수 file part를 non-null binding으로 만들고 확정된 field 계약, 외부 부작용 전 참조 검증과 legacy upload/processing parity를 최소 구현한다. -- [ ] **GREEN 확인:** 같은 test를 다시 실행해 part별 400/i18n, 정상 생성과 실패 후 DB/S3/event 결과가 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** cover/audio upload, 가격·공개·예약·번역·알림 및 실패 후 DB/S3/event 결과를 characterization/focused test로 회귀하고 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] `tags` 필수 여부와 생성 `isActive=false`의 canonical 계약을 legacy pipeline·Endpoint Contract Summary로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다. +- [x] **RED:** `coverImage`, `audioFile`, `request` 각 part 누락에서 Kotlin nullable 때문에 `MissingServletRequestPartException`이 발생하지 않는 현재 binding과 KO/EN/JA envelope 차이, facade·DB·S3·event 호출 0건 기대를 재현한다. +- [x] **RED:** 생성 request 전체 field, `description/releaseDateUtc` 변환, `tags` 누락, `isActive=false`, target·theme·`seriesIds` 오류와 S3/processing/event 실패 순서를 각각 고정한다. +- [x] **RED 확인:** create/error/legacy characterization test를 실행해 part별 exception·field 계약·failure order가 의도대로 실패하는지 확인한다. +- [x] **GREEN:** 필수 file part를 non-null binding으로 만들고 확정된 field 계약, 외부 부작용 전 참조 검증과 legacy upload/processing parity를 최소 구현한다. +- [x] **GREEN 확인:** 같은 test를 다시 실행해 part별 400/i18n, 정상 생성과 실패 후 DB/S3/event 결과가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** cover/audio upload, 가격·공개·예약·번역·알림 및 실패 후 DB/S3/event 결과를 characterization/focused test로 회귀하고 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: 콘텐츠 생성의 필수 multipart part와 legacy 기본 계약. 왜: `REV-006`에서 생성 binding·field·failure-order 증거가 분리되지 않았고, `tags` 누락과 `isActive=true` 요청의 canonical 동작을 확정해야 했기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`를 실행했다. 결과: 정상 생성 케이스가 200 기대 대비 500으로 실패했고, 원인은 production 계약이 아니라 test fixture의 `AmazonS3Client.getUrl(String, String)` 미설정으로 `S3Uploader.putS3`에서 null URL이 발생한 것이었다. + - 검증 기록(GREEN/REFACTOR): 무엇: 생성 필수 part 400, 업로드 전 S3 0회, `tags` 누락 허용, `isActive=true` 요청의 legacy processing 기본값. 왜: 신규 v2 생성은 기존 upload/processing pipeline을 바꾸지 않고 adapter 계약만 고정해야 하기 때문이다. 어떻게: test fixture에 `amazonS3Client.getUrl(...)` mock만 추가하고 production code는 변경하지 않았다. 결과: `AiCharacterAdminAudioContentCreateTest`는 `BUILD SUCCESSFUL`, create+legacy+error contract 회귀는 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 10s`였다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` -- [ ] **Task 3.7: 콘텐츠 수정·soft delete 보완** +- [x] **Task 3.7: 콘텐츠 수정·soft delete 보완** **Goal 실행 `P3-T6`:** 콘텐츠 수정, cover 유지/교체, series 재연결과 soft delete를 owner-safe하게 완결한다. @@ -1414,15 +1615,17 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` -- [ ] **RED:** 기존 `SeriesContent.orders`, row ID, `createdAt`이 있는 콘텐츠에 동일 `seriesIds`를 PUT했을 때 전부 삭제·재생성되는 현재 동작을 실패 test로 고정한다. -- [ ] **RED 확인:** 동일 ID, 추가 ID, 제거 ID를 각각 요청해 교집합 metadata 보존과 차집합만 insert/delete한다는 기대가 현재 실패하는지 확인한다. -- [ ] **GREEN:** 기존 연결과 요청 ID의 차집합만 변경하고 교집합 row의 ID·`orders`·`createdAt`을 보존하는 최소 구현을 작성한다. -- [ ] **GREEN 확인:** 같은 update test를 다시 실행해 동일 집합 no-op, 교집합 metadata 보존과 차집합 변경만 발생하는지 확인한다. -- [ ] **REFACTOR:** cover 유지/교체, 날짜, `audioFile` 미지원, `isActive=false`와 cross-owner/invalid series의 DB/S3/event no-side-effect를 회귀한다. -- [ ] 수정 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** 기존 `SeriesContent.orders`, row ID, `createdAt`이 있는 콘텐츠에 동일 `seriesIds`를 PUT했을 때 전부 삭제·재생성되는 현재 동작을 실패 test로 고정한다. +- [x] **RED 확인:** 동일 ID, 추가 ID, 제거 ID를 각각 요청해 교집합 metadata 보존과 차집합만 insert/delete한다는 기대가 현재 실패하는지 확인한다. +- [x] **GREEN:** 기존 연결과 요청 ID의 차집합만 변경하고 교집합 row의 ID·`orders`·`createdAt`을 보존하는 최소 구현을 작성한다. +- [x] **GREEN 확인:** 같은 update test를 다시 실행해 동일 집합 no-op, 교집합 metadata 보존과 차집합 변경만 발생하는지 확인한다. +- [x] **REFACTOR:** cover 유지/교체, 날짜, `audioFile` 미지원, `isActive=false`와 cross-owner/invalid series의 DB/S3/event no-side-effect를 회귀한다. +- [x] 수정 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: 동일 `seriesIds` 수정 시 기존 `SeriesContent` metadata 보존. 왜: `REV-004`에서 기존 구현이 연결을 모두 삭제·재생성해 row ID, `orders`, `createdAt`을 소실했기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`를 추가하고 focused 실행했다. 결과: 최초 focused test는 200 기대 대비 500으로 실패했고, 원인은 응답 매핑의 CloudFront private key fixture 문제임을 로그로 확인한 뒤 test fixture에 `AudioContentCloudFront` mock을 추가했다. + - 검증 기록(GREEN/REFACTOR): 무엇: `replaceSeriesIds`가 요청 ID와 기존 연결의 차집합만 변경하고 교집합 row를 보존하도록 수정했다. 왜: 동일 series 연결의 metadata를 유지하면서 제거·추가만 반영해야 하기 때문이다. 어떻게: `requestedIds`, 기존 연결 ID set을 비교해 삭제 대상만 remove하고 신규 ID만 persist했다. 결과: `AiCharacterAdminAudioContentUpdateTest`는 `BUILD SUCCESSFUL in 46s`, update+controller+legacy+error contract 회귀는 `BUILD SUCCESSFUL in 1m 21s`였다. 기존 controller 회귀 2건은 앞선 `P3-T4` 확정 계약(`releaseDate` 미노출, `RENT_ONLY` 파생값)에 맞춰 기대값만 갱신했다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest` -- [ ] **Task 3.8: 콘텐츠 ownership·오류·legacy 회귀 보완** +- [x] **Task 3.8: 콘텐츠 ownership·오류·legacy 회귀 보완** **Goal 실행 `P3-T7`:** Phase 3 모든 endpoint의 ownership·ADMIN·오류 계약과 legacy/public 회귀를 고정한다. @@ -1433,35 +1636,38 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 **Files:** -- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt` -- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` -- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` -- [ ] **RED:** 테마·목록·상세·생성·수정 endpoint 각각의 JWT role × DB role, stale ADMIN claim과 허용·거부 Origin을 parameterized test로 고정한다. -- [ ] **RED:** target/content/theme/series/date와 세 multipart part 누락의 exact status, exception type, message key와 KO/EN/JA envelope를 실제 endpoint에서 고정한다. -- [ ] **RED 확인:** 실제 endpoint matrix와 legacy characterization을 실행해 누락된 인가·i18n·failure-order assertion이 의도대로 실패하는지 확인한다. -- [ ] **GREEN:** 확정된 domain/client/server 오류만 최소 매핑하고 ownership 실패 시 DB insert/update/delete, S3, event 0건을 보장한다. -- [ ] **GREEN 확인:** 같은 endpoint/error/ownership test를 다시 실행해 status/header/envelope, KO/EN/JA와 no-side-effect가 모두 통과하는지 확인한다. -- [ ] **REFACTOR:** legacy characterization에 validation·파일·가격·공개/예약·번역/알림·failure order를 보강하고 signed URL edge case와 함께 실행한다. -- [ ] 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest` 참조와 과거 test 수는 삭제하지 않고 정정 기록을 누적한다. -- [ ] `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest`, `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentOwnershipTest`의 파일 존재와 각 소유 계약 통과를 확인한다. -- [ ] creator/admin/public content 회귀, 신규 DTO 의존 방향과 focused test·`ktlintCheck` 결과를 Progress에 기록한다. +- [x] **RED:** 테마·목록·상세·생성·수정 endpoint 각각의 JWT role × DB role, stale ADMIN claim과 허용·거부 Origin을 parameterized test로 고정한다. +- [x] **RED:** target/content/theme/series/date와 세 multipart part 누락의 exact status, exception type, message key와 KO/EN/JA envelope를 실제 endpoint에서 고정한다. +- [x] **RED 확인:** 실제 endpoint matrix와 legacy characterization을 실행해 누락된 인가·i18n·failure-order assertion이 의도대로 실패하는지 확인한다. +- [x] **GREEN:** 확정된 domain/client/server 오류만 최소 매핑하고 ownership 실패 시 DB insert/update/delete, S3, event 0건을 보장한다. +- [x] **GREEN 확인:** 같은 endpoint/error/ownership test를 다시 실행해 status/header/envelope, KO/EN/JA와 no-side-effect가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** legacy characterization에 validation·파일·가격·공개/예약·번역/알림·failure order를 보강하고 signed URL edge case와 함께 실행한다. +- [x] 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest` 참조와 과거 test 수는 삭제하지 않고 정정 기록을 누적한다. +- [x] `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest`, `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentOwnershipTest`의 파일 존재와 각 소유 계약 통과를 확인한다. +- [x] creator/admin/public content 회귀, 신규 DTO 의존 방향과 focused test·`ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): malformed `characterId`/`contentId` 6건이 미매핑 404 EN envelope을 기대한 새 ownership focused test에서 GET 400, write 415를 반환해 18건 중 6건이 실패했다. + - 검증 기록(GREEN/REFACTOR): 모든 resource path를 `[0-9]+`로 제한한 뒤 malformed path 404, 실제 5개 endpoint의 non-ADMIN/stale claim 403, 테마 CORS allow/deny, unknown target 생성 S3 0회를 고정했다. common authorization/error test의 KO·EN·JA matrix와 기존 content/legacy characterization을 재사용했다. focused는 `BUILD SUCCESSFUL in 1m 5s`, content+authorization+error 회귀는 `BUILD SUCCESSFUL in 2m 8s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다. `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`는 현재 존재하지 않는 과거 계획 참조이며 이 기록으로 정정한다. - Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` #### Phase 3 Gate **Goal 실행 `P3-GATE`:** Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성과 legacy 회귀를 최종 판정한다. -- [ ] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. +- [x] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P3-R1`, `P3-T3`~`P3-T7` 완료 또는 근거 있는 `해당 없음` 판정. - **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록. - **범위 밖:** Gate 실패와 무관한 Phase 4 기능 구현. -- [ ] `REV-004`~`REV-008`의 response parity matrix, multipart exception, series metadata, DTO 경계와 test 증거가 각 소유 Goal의 Progress에 연결됐다. -- [ ] 동일 `seriesIds`의 row metadata 보존, legacy `releaseDate`·rental 파생값, 세 필수 part와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다. -- [ ] 완료 이력의 누락 test 파일·test 수·characterization 범위는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다. +- [x] `REV-004`~`REV-008`의 response parity matrix, multipart exception, series metadata, DTO 경계와 test 증거가 각 소유 Goal의 Progress에 연결됐다. +- [x] 동일 `seriesIds`의 row metadata 보존, legacy `releaseDate`·rental 파생값, 세 필수 part와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다. +- [x] 완료 이력의 누락 test 파일·test 수·characterization 범위는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다. + - 검증 기록: 무엇: `P3-GATE` Phase 3 최종 판정. 왜: `P3-R1`, `P3-T3`~`P3-T7`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 fresh 실행했다. 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `git diff --check`는 `P3-T7` 완료 전 실행에서 출력 없음이었다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다. ```bash ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' @@ -1471,6 +1677,291 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 **Expected:** 모든 명령 exit code 0, Phase 3 review의 후보·확정 미처리 항목 0건, Phase 4가 소비할 owner query 계약 확정. +#### Phase 3 후속 리뷰 보완 + +- [x] **Task 3.9: 콘텐츠 생성 pipeline·multipart 오류 증거 보강** + +**Goal 실행 `P3-R2`:** `REV-010`의 생성 field adapter, 선검증, multipart 오류와 S3/processing/event 실패 순서를 실제 endpoint에서 고정한다. + +- **추적 review ID:** `REV-010`. +- **시작 조건:** `P2-R2-GATE` 완료와 기존 `P3-GATE` 완료 이력 존재. +- **완료 증거:** 생성 actual endpoint/legacy characterization test, 실패 지점별 관찰 결과, `ktlintCheck`와 Progress 기록. +- **범위 밖:** legacy upload/processing 정책 변경, 추정에 의한 S3 보상 추가, audio file 교체. +- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T5` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** legacy와 actual endpoint를 characterization하고 불일치가 재현될 때만 RED/GREEN으로 최소 수정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **CONTRACT TEST:** `coverImage`, `audioFile`, `request` 누락의 실제 endpoint KO/EN/JA status/key/envelope와 facade·DB·S3·event 0건을 고정한다. +- [x] **CONTRACT TEST:** 생성 request 전체 field의 legacy adapter 결과와 target/theme/series 선검증을 확인한다. +- [x] **FAILURE CHARACTERIZATION:** cover upload, audio upload와 event 실패 지점별 DB/S3/event 결과를 legacy parity와 대조하고 비트랜잭션 S3 결과를 명시한다. +- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다. +- [x] **REFACTOR:** create/legacy/error focused test와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다. + - 검증 기록: 무엇: `REV-010`의 생성 multipart·theme·cover 실패 증거를 실제 endpoint test로 보강했다. 왜: 기존 완료 기록이 part별 KO/EN/JA message, invalid theme 선검증, cover upload 실패 후 DB/event 상태를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 세 필수 part KO/EN/JA envelope, invalid theme 선검증, cover upload 실패 rollback/event 0회 단언을 추가했다. 결과: production code 변경 없이 create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +- [x] **Task 3.10: 콘텐츠 수정 차집합·cover·ownership 증거 보강** + +**Goal 실행 `P3-R3`:** `REV-011`의 `seriesIds` 교집합/차집합, cover 변경과 실제 endpoint ownership·오류 no-side-effect를 고정한다. + +- **추적 review ID:** `REV-011`. +- **시작 조건:** `P3-R2` 완료. +- **완료 증거:** update/ownership actual endpoint test, content/authorization/error 회귀, `ktlintCheck`와 Progress 기록. +- **범위 밖:** audio file 교체, hard delete, Phase 4 series API 구현. +- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T6`~`P3-T7` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** 교집합+추가+제거와 cover/ownership 계약을 non-vacuous test로 작성하고 실패가 재현될 때만 최소 수정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` + +- [x] **CONTRACT TEST:** 기존 교집합, 신규 추가, 기존 제거를 한 요청에 포함해 교집합 row ID·`orders`·`createdAt` 보존과 차집합만 insert/delete됨을 확인한다. +- [x] **CONTRACT TEST:** cover 미지정 유지, 성공 교체와 업로드 실패, 날짜 변경, soft delete, cross-owner/invalid series의 DB/S3/event 결과를 고정한다. +- [x] **CONTRACT TEST:** 실제 목록·상세·생성·수정의 ownership/domain 오류를 KO/EN/JA envelope과 DB insert/update/delete·S3·event count로 확인한다. +- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다. +- [x] **REFACTOR:** content package와 공통 authorization/error, legacy characterization, `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다. + - 검증 기록: 무엇: `REV-011`의 수정 차집합·cover·ownership 증거를 보강했다. 왜: 기존 완료 기록보다 실제 endpoint의 교집합 보존, cover 유지/교체/실패, ownership/domain no-side-effect 증거가 좁았기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`와 `AiCharacterAdminAudioContentOwnershipTest`에 관련 회귀를 추가하고 content/common 회귀로 재확인했다. 결과: production code 변경 없이 content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 3 후속 리뷰 Gate + +**Goal 실행 `P3-R2-GATE`:** `REV-010`~`REV-011`의 직접 증거를 재검토하고 Phase 3 후속 리뷰를 종결한다. + +- [x] **`P3-R2-GATE` 완료:** `P3-R2`, `P3-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다. +- **시작 조건:** `P3-R2`, `P3-R3` 완료. +- **완료 증거:** 두 review ID 수정 완료, content/common 회귀와 `ktlintCheck` 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화. +- **범위 밖:** 기존 `P3-GATE` 이력 수정, Phase 4 기능 구현. + - 검증 기록: 무엇: `P3-R2-GATE` 후속 리뷰 종결. 왜: `REV-010`~`REV-011`의 직접 증거가 추가됐고 Phase 4 전 Phase 3 후속 보완 종료 여부를 판정하기 위해. 어떻게: content/common 회귀와 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적했다. 결과: content/common 회귀는 `BUILD SUCCESSFUL in 2m 41s`였고, `ktlintCheck` 결과는 아래 Progress 검증 기록에 남긴다. + +#### Phase 3 4차 리뷰 보완 + +- [x] **Task 3.11: 생성 후반 실패·ownership no-side-effect 증거 보강** + +**Goal 실행 `P3-R4`:** `REV-013`~`REV-014`에서 남은 audio upload/event 실패와 실제 endpoint ownership/domain 오류의 부작용 경계를 고정한다. + +- **추적 review ID:** `REV-013`, `REV-014`. +- **시작 조건:** `P2-R3-GATE`와 기존 `P3-R2-GATE` 완료. +- **완료 증거:** create/update/ownership actual endpoint test, legacy characterization, content/common 회귀, `ktlintCheck`와 Progress 기록. +- **범위 밖:** S3 보상 정책 신설, audio file 수정 지원, Phase 4 기능 구현. +- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 3.9`~`Task 3.10` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** cover 이후 audio upload와 event 실패, ownership/domain 거부를 실제 endpoint에서 먼저 characterization하고 계약 불일치가 재현될 때만 최소 수정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt` + +- [x] **FAILURE CHARACTERIZATION:** 두 번째 S3 audio upload 실패와 event publish 실패에서 DB rollback, S3 호출·잔존 결과와 event 결과를 각각 고정한다. +- [x] **CONTRACT TEST:** 목록·상세·생성·수정의 target/ownership/domain 거부를 KO/EN/JA exact envelope로 확인한다. +- [x] **CONTRACT TEST:** 각 거부 뒤 AudioContent·SeriesContent·S3·event의 insert/update/delete count가 변하지 않음을 직접 단언한다. +- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, legacy 비트랜잭션 S3 경계와 일치하면 production code를 변경하지 않는다. +- [x] **REFACTOR:** content/common/legacy 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다. + - 검증 기록: 무엇: `REV-013`~`REV-014`의 content 생성 후반 실패와 ownership/domain no-side-effect 증거를 보강했다. 왜: 기존 완료 기록이 cover upload 실패와 일부 ownership 경로에 치우쳐 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 audio upload 실패와 event publish 실패를 추가하고, `AiCharacterAdminAudioContentOwnershipTest`에 목록·상세·생성·수정 unknown target KO/EN/JA 및 AudioContent·SeriesContent·S3·event 무변경 단언을 추가했다. 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`였다. + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 3 4차 리뷰 Gate + +**Goal 실행 `P3-R3-GATE`:** `REV-013`~`REV-014`의 직접 증거를 재검토하고 Phase 3 4차 리뷰를 종결한다. + +- [x] **`P3-R3-GATE` 완료:** `P3-R4` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다. +- **시작 조건:** `P3-R4` 완료. +- **완료 증거:** 두 review ID 처리 완료, 위 두 명령 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화. +- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현. + - 검증 기록: 무엇: Phase 3 4차 리뷰의 `REV-013`~`REV-014` 처리를 종결했다. 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 완료하고 Phase 4로 넘어가지 않기 위해. 어떻게: content/common 회귀와 최종 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`를 처리 완료로 갱신했다. 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다. + +#### Phase 3 5차 리뷰 보완 + +- [x] **Task 3.12: 생성 필수 multipart part의 exact binding 계약 복구** + +**Goal 실행 `P3-R5`:** `REV-016`의 생성 필수 파일 part를 MVC non-null binding으로 고정하고 세 part 누락의 exact exception·KO/EN/JA 계약을 복구한다. + +- **추적 review ID:** `REV-016`. +- **시작 조건:** `P2-R4-GATE` 완료와 PRD API Expectations 179~180의 missing-part 계약. +- **완료 증거:** 세 part별 RED/GREEN, exact `MissingServletRequestPartException`, facade/DB/S3/event 0회, content/common 회귀와 Progress 기록. +- **범위 밖:** legacy `AudioContentService.createAudioContent` signature 변경, upload/processing pipeline 정책 변경, 오디오 파일 교체. + +**Files:** + +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt` +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **RED:** `coverImage`, `audioFile`, `request` 각각을 누락한 KO/EN/JA actual endpoint test에서 `MvcResult.resolvedException`이 정확히 `MissingServletRequestPartException`이고 message가 `common.error.invalid_request`인지 단언한다. +- [x] **RED 확인:** create focused test를 실행해 nullable `coverImage`·`audioFile`이 legacy `SodaException`까지 전달되어 exact exception/message assertion이 실패하는지 확인한다. +- [x] **GREEN:** 생성 controller와 facade의 `coverImage`, `audioFile`을 non-null `MultipartFile`로 바꾸고 legacy service에는 검증된 non-null 값을 그대로 전달한다. +- [x] **GREEN 확인:** 같은 focused test를 재실행해 세 part 누락 9건의 exact exception·KO/EN/JA 400 envelope과 facade/DB/S3/event 0회를 확인한다. +- [x] **REFACTOR:** 중복된 missing-part request/assertion만 parameterized helper로 정리하고 create/error/legacy 회귀와 `ktlintCheck` 결과를 Progress에 기록한다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +- [x] **Task 3.13: ownership/domain KO·EN·JA no-side-effect matrix 완결** + +**Goal 실행 `P3-R6`:** `REV-017`의 cross-owner와 domain validation 경로를 actual endpoint KO/EN/JA 및 DB/S3/event 무변경 증거로 완결한다. + +- **추적 review ID:** `REV-017`. +- **시작 조건:** `P3-R5` 완료와 기존 `P3-R4` unknown target matrix. +- **완료 증거:** cross-owner detail/update, create/update other-owner series, invalid date의 exact envelope·side-effect assertions, content/common 회귀와 Progress 기록. +- **범위 밖:** 새로운 ownership 정책, 오류 key/status 변경, Phase 4 series API 구현. +- **TDD 예외 사유:** 현재 production 위반보다 `REV-014` 완료 기록 대비 대표 ownership/domain 직접 증거 누락이 확정된 test 보강 Task다. +- **대체 검증 방법:** 기존 실제 endpoint test를 KO/EN/JA parameterized matrix로 확장하고 요청 전후 entity field·연결 row와 S3/event interaction을 비교한다. 실패가 재현될 때만 validation 순서를 최소 수정한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **OWNERSHIP TEST:** 다른 캐릭터 소유 콘텐츠의 detail/update를 KO/EN/JA actual endpoint matrix로 만들고 content field·S3·event 무변경을 단언한다. +- [x] **DOMAIN TEST:** create/update의 다른 owner `seriesIds`와 invalid `releaseDateUtc`를 KO/EN/JA matrix로 만들고 AudioContent·SeriesContent insert/update/delete, S3, event 0회를 단언한다. +- [x] **NON-VACUOUS 확인:** owner 또는 validation guard를 제거하면 각 matrix가 status/message 또는 side-effect assertion으로 실패하는지 확인한다. +- [x] **GREEN:** 현재 계약 위반이 재현될 때만 target/ownership/domain 선검증 순서를 최소 수정하고, 이미 충족하면 test-only로 종료한다. +- [x] **REFACTOR:** unknown target과 cross-owner/domain fixture의 공통 assertion만 정리하고 content/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 3 5차 리뷰 Gate + +**Goal 실행 `P3-R4-GATE`:** `REV-016`~`REV-017`의 exact multipart와 ownership/domain 증거를 재검토하고 Phase 3 5차 리뷰를 종결한다. + +- [x] **`P3-R4-GATE` 완료:** `P3-R5`, `P3-R6` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고 리뷰 문서·Progress를 갱신한다. +- **시작 조건:** `P3-R5`, `P3-R6` 완료. +- **완료 증거:** `REV-016`, `REV-017` 수정 완료, 세 필수 part와 ownership/domain matrix 직접 증거, focused/영향 범위 회귀와 lint 성공. +- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현. + +#### Phase 3 6차 리뷰 보완 + +- [x] **Task 3.14: 빈 multipart 파일 계약 고정** + +**Goal 실행 `P3-R7`:** `REV-019`의 생성·수정 empty-file 경계를 v2 facade에서 고정해 0-byte upload와 수정 +`audioFile` 계약 우회를 차단한다. + +- **추적 review ID:** `REV-019`. +- **시작 조건:** `P2-R5-GATE`와 기존 `P3-R4-GATE` 완료. +- **완료 증거:** 생성 empty cover/audio 거부, 수정 empty cover 유지, empty/non-empty audio 거부 actual endpoint + RED/GREEN과 DB/S3/event assertion, content/common 회귀 및 Progress 기록. +- **범위 밖:** legacy `AudioContentService`·`CreatorAdminContentService` 공용 계약 변경, 파일 content-type/확장자 정책 추가, + 오디오 파일 교체, Phase 4 기능 구현. + +**Files:** + +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` + +- [x] **RED:** 생성의 빈 `coverImage`와 빈 `audioFile` actual endpoint를 KO/EN/JA로 요청해 400 + `common.error.invalid_request`, AudioContent/SeriesContent·S3·event 0회를 기대하고 현재 200/업로드 경로로 실패함을 + 확인한다. +- [x] **RED:** 수정의 빈 `coverImage`가 생략과 동일하게 기존 cover path를 유지하고 S3를 호출하지 않는 기대, 빈 + `audioFile` part가 non-empty와 동일하게 400으로 거부되는 기대가 현재 실패함을 확인한다. +- [x] **GREEN:** create 시작 시 `coverImage.isEmpty || audioFile.isEmpty`를 `invalidRequest()`로 거부한다. +- [x] **GREEN:** update는 `audioFile != null`이면 크기와 관계없이 `invalidRequest()`로 거부하고, + `coverImage?.takeUnless { it.isEmpty }`만 legacy update service에 전달한다. +- [x] **GREEN 확인:** 같은 focused test를 재실행해 create empty-file의 부작용 0회, update empty cover의 DB/S3 유지와 + empty/non-empty audio 거부가 모두 통과하는지 확인한다. +- [x] **REFACTOR:** empty-file fixture만 공통화하고 legacy service를 수정하지 않은 채 content/common 회귀와 + `ktlintCheck` 결과를 Progress에 기록한다. + - 검증 기록(RED): 무엇: 생성 empty `coverImage`/`audioFile` KO/EN/JA와 수정 empty cover/audio actual endpoint 계약을 추가했다. 왜: 빈 multipart 파일이 null/non-empty 검사 사이를 통과하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`를 실행했다. 결과: 26건 중 신규 8건이 line 119, 204, 236에서 실패해 RED를 확인했다. + - 검증 기록(GREEN): 무엇: v2 facade empty-file 경계와 실제 service publisher no-interaction 증거. 왜: legacy 공용 service 변경 없이 신규 관리자 API 계약만 고정하고 event 부작용 assertion이 detached mock을 보지 않게 하기 위해. 어떻게: 같은 focused 명령을 재실행했다. 결과: reviewer gate 보완 후 최종 empty create/update focused는 `BUILD SUCCESSFUL in 43s`, non-empty audio update 보완 focused는 `BUILD SUCCESSFUL in 44s`였다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +- [x] **Task 3.15: ownership/domain event 0회 실제 publisher 증거 복구** + +**Goal 실행 `P3-R8`:** `REV-020`의 ownership/domain no-side-effect test가 실제 `AudioContentService`와 +`CreatorAdminContentService`의 publisher를 관찰하도록 연결해 NON-VACUOUS 완료 증거를 복구한다. + +- **추적 review ID:** `REV-020`. +- **시작 조건:** `P3-R7` 완료. +- **완료 증거:** 실제 두 service proxy target의 publisher 교체·복원, mock identity 확인, ownership/domain matrix의 + event 0회와 content/common 회귀 및 Progress 기록. +- **범위 밖:** production event 발행 순서·payload 변경, application context event infrastructure 변경, 신규 test 전용 + production seam 추가, Phase 4 기능 구현. +- **TDD 예외 사유:** 현재 production의 잘못된 event 발행이 아니라 detached mock으로 인한 완료 증거 공백이 확정된 + test-only Task다. +- **대체 검증 방법:** Phase 2와 기존 content create event failure test의 `AopTestUtils`·`ReflectionTestUtils` 방식을 + 재사용해 실제 proxy target field와 mock identity를 확인한다. + +**Files:** + +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` +- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentService.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` +- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt` +- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` + +- [x] **EVIDENCE RED:** 현재 `@MockBean ApplicationEventPublisher`와 실제 두 service proxy target의 publisher가 같은 + instance인지 단언해 detached 상태에서 실패함을 확인한다. +- [x] **EVIDENCE GREEN:** 각 test에서 `AudioContentService`, `CreatorAdminContentService` proxy target의 기존 publisher를 + 보관하고 같은 mock으로 교체하며 `finally`/teardown에서 원래 publisher를 복원한다. +- [x] **NON-VACUOUS 확인:** 교체 직후 실제 두 target field가 mock과 같은 instance인지 단언하고, cross-owner + detail/update·other-owner series·invalid date 및 unknown target matrix가 실제 publisher no-interaction을 통과하는지 + 확인한다. +- [x] **회귀 확인:** 기존 create event failure helper와 충돌하지 않고 정상 content 생성·수정의 event 회귀가 유지되는지 + content/common 명령으로 확인한다. +- [x] **REFACTOR:** Phase 2 및 create failure test의 기존 helper 패턴 범위에서만 중복을 정리하고 production seam이나 + 공용 test abstraction은 추가하지 않는다. + - 검증 기록: 무엇: `REV-020`의 실제 publisher no-interaction 증거를 복구했다. 왜: detached `@MockBean ApplicationEventPublisher`만 검증하면 실제 service field 호출 여부를 증명할 수 없기 때문이다. 어떻게: `AudioContentService`, `CreatorAdminContentService` proxy target의 `applicationEventPublisher`를 테스트 mock으로 교체·복원하고 field identity를 단언한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest`를 실행했다. 결과: `BUILD SUCCESSFUL in 39s`였다. + +```bash +./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest +./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest +./gradlew ktlintCheck +``` + +#### Phase 3 6차 리뷰 Gate + +**Goal 실행 `P3-R5-GATE`:** `REV-019`~`REV-020`의 empty-file 계약과 actual publisher 증거를 재검토하고 Phase 3 +6차 리뷰를 종결한다. + +- [x] **`P3-R5-GATE` 완료:** `P3-R7`, `P3-R8` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고 + 리뷰 문서·Progress를 갱신한다. +- **시작 조건:** `P3-R7`, `P3-R8` 완료. +- **완료 증거:** `REV-019`, `REV-020` 처리 완료, 생성·수정 empty-file actual endpoint 계약, 실제 두 service publisher + no-interaction 증거, focused/영향 범위 회귀와 lint·diff check 성공. +- **범위 밖:** Gate에서 production code 수정, 기존 Phase 3 완료 이력 변경, Phase 4 기능 구현. + --- ### Phase 4: 시리즈 관리 vertical slice @@ -1537,7 +2028,7 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 **Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다. -- **시작 조건:** `P3-GATE` 완료. +- **시작 조건:** 최신 Phase 3 후속 Gate인 `P3-R5-GATE` 완료와 사용자 진행 지시. - **완료 증거:** production 변경 전 특성화 테스트 통과, 관찰된 오류·side-effect 정책과 Progress 기록. - **범위 밖:** 신규 v2 series production code 구현. @@ -2115,10 +2606,18 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 | 4 | `P3-R1` | `P2-GATE`, `P3-H1`, `P3-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 | | 5 | `P3-T3` → `P3-T4` → `P3-T5` → `P3-T6` → `P3-T7` | `P3-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 | | 6 | `P3-GATE` | Phase 3 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 | -| 7 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-GATE` | 아니요 | 실패 소유 Task로 되돌림 | -| 8 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 | -| 9 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 | -| 10 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 | +| 7 | `P2-R2` → `P2-R2-GATE` | 2차 Phase 2 review | 아니요 | `REV-009` 소유 Task에서 증거 보강 | +| 8 | `P3-R2` → `P3-R3` → `P3-R2-GATE` | `P2-R2-GATE`, 2차 Phase 3 review | 아니요 | `REV-010`~`REV-011` 소유 Task에서 증거 보강 | +| 9 | `P2-R3` → `P2-R3-GATE` | 4차 Phase 2 review | 아니요 | `REV-012` 소유 Task에서 증거 보강 | +| 10 | `P3-R4` → `P3-R3-GATE` | `P2-R3-GATE`, 4차 Phase 3 review | 아니요 | `REV-013`~`REV-014` 소유 Task에서 증거 보강 | +| 11 | `P2-R4` → `P2-R4-GATE` | 5차 Phase 2 review | 아니요 | `REV-015` actual transaction 증거 보강 | +| 12 | `P3-R5` → `P3-R6` → `P3-R4-GATE` | `P2-R4-GATE`, 5차 Phase 3 review | 아니요 | `REV-016`~`REV-017` 소유 Task에서 수정·증거 보강 | +| 13 | `P2-R5` → `P2-R5-GATE` | 6차 Phase 2 review | 아니요 | `REV-018` 문서 계약 동기화 | +| 14 | `P3-R7` → `P3-R8` → `P3-R5-GATE` | `P2-R5-GATE`, 6차 Phase 3 review | 아니요 | `REV-019`~`REV-020` 소유 Task에서 수정·증거 보강 | +| 15 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-R5-GATE`, 사용자 진행 지시 | 아니요 | 실패 소유 Task로 되돌림 | +| 16 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 | +| 17 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 | +| 18 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 | ## 변경 금지·중단 규칙 @@ -2142,27 +2641,279 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 - 남은 항목: `P2-R1` read-only 리뷰 실행. - 다음 행동: 사용자가 goal 실행을 요청하면 `P2-R1`만 `create_goal`에 등록한다. +### `P2-R1` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: Phase 2 character slice의 PRD Feature B, Endpoint Contract Summary, production/test 구현을 대조했다. +- 왜: 기존 `P2-H1`, `P2-H2` 완료 이력 이후 `P2-GATE` 전에 확정 finding을 소유 Goal에 연결해야 하기 때문이다. +- 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. +- 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`을 확정으로 유지해 `P2-T3`~`P2-T6`에 연결했다. +- 남은 항목: `P2-T3` 캐릭터 목록·검색·상세 보완부터 직렬 실행. + +### `P2-T3` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 캐릭터 목록 응답을 상세 DTO에서 목록 전용 DTO로 분리했다. +- 왜: 목록 API가 계약에 없는 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다. +- 어떻게: RED로 `AiCharacterAdminCharacterControllerTest` exact field 비노출 assertion을 추가했고, `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 최소 구현했다. +- 결과: RED는 `AiCharacterAdminCharacterControllerTest` line 66 실패로 확인했고, GREEN 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`와 `./gradlew ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. +- 남은 항목: `P2-T4` 캐릭터 생성 흐름 보완. + +### `P2-T4` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 생성 request의 서버 소유 field를 거부하고, 원작 존재를 외부 캐릭터 생성 전에 확인했다. 정상 생성의 원작 연결·AI creatorMember 표시 정보·언어 감지 event 및 중복·외부 API·S3 실패 결과를 v2/legacy test로 고정했다. +- 왜: `REV-002`, `REV-003`, `REV-007`에서 request 입력 의미와 외부 부작용 전 DB 검증 증거가 부족했기 때문이다. +- 어떻게: `DEC-P2-T4-001`로 canonical contract와 legacy failure boundary를 고정한 뒤 RED/GREEN test를 추가하고, focused/legacy 및 formatting 검증을 실행했다. +- 결과: specified focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 API 삭제 endpoint나 신규 DB unique 제약은 기존 external contract·DDL 금지 범위 밖이라 추가하지 않았다. +- 남은 항목: `P2-T5` 캐릭터 수정·비활성화 흐름 보완. + +### `P2-T5` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: PUT의 response 전용 `externalCharacterId`와 `isActive=false` 혼합 요청을 거부하고, 원작을 외부 수정 전에 검증했다. 일반 수정의 image 유지·교체, AI creatorMember 표시 정보, 번역 event, soft delete의 Member·콘텐츠 보존과 flush 후 `updatedAtUtc`를 회귀로 고정했다. +- 왜: `REV-002`, `REV-003`, `REV-007`에서 update 입력 의미, S3/외부/DB 실패 경계와 response timestamp 증거가 부족했기 때문이다. +- 어떻게: `DEC-P2-T5-001`로 PUT contract와 non-compensated external update 경계를 확정한 뒤 RED/GREEN test를 추가하고 지정 focused/legacy 및 formatting 검증을 실행했다. +- 결과: 지정 focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 update restore API와 기존 image hard delete는 external contract·legacy parity 범위 밖이라 추가하지 않았다. +- 남은 항목: `P2-T6` Phase 2 보안·오류·회귀 보완. + +### `P2-T6` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 문자열 `unmapped-path`를 character resource handler에서 제외해 prefix fallback의 404/i18n/CORS 계약으로 보냈고, 실제 목록·상세·생성·수정 endpoint의 ADMIN·binding/domain/multipart·CORS 증거를 보강했다. +- 왜: 문자열 path가 `Long` binding의 400으로 처리되어 `REV-001`을 위반했고, `REV-008`의 실제 endpoint matrix 증거가 부족했기 때문이다. +- 어떻게: 기존 404 KO/EN/JA·CORS 4건을 RED로 재현하고 GET/PUT path를 `[0-9]+`로 제한했다. 목록 binding, 상세 target, 생성/수정 multipart KO/EN/JA, 실제 네 endpoint non-ADMIN과 detail preflight를 parameterized/focused test로 확인했다. +- 결과: 지정 focused test는 `BUILD SUCCESSFUL in 1m 26s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 34s`였다. `AiCharacterAdminCharacterServiceTest`는 존재하지 않는 과거 계획 참조임을 정정 기록으로 보존했고, legacy/public contract와 DTO 의존 방향은 변경하지 않았다. +- 남은 항목: `P2-GATE`. + +### `P2-GATE` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름, legacy 회귀를 최종 판정했다. +- 왜: Phase 3 실행 전 `P2-R1`, `P2-T3`~`P2-T6`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다. +- 어떻게: Gate에 명시된 세 명령과 `git diff --check`를 실행했다. +- 결과: character focused 명령은 병렬 실행 중 XML 결과 파일 write 충돌로 한 번 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `REV-001`~`REV-003`, `REV-007`, `REV-008`의 Phase 2 소유 항목은 처리 완료로 판정했다. +- 남은 항목: `P3-R1` Phase 3 요구사항·계약·코드 리뷰. + +### `P3-T7` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 실제 Phase 3 테마·목록·상세·생성·수정 endpoint의 ADMIN/stale claim, CORS, ownership no-side-effect와 malformed resource path 오류 계약을 독립 focused test로 고정했다. +- 왜: 기존 공통 authorization/error test는 prefix 공통 계약을, 콘텐츠 focused test는 domain ownership을 보장했지만 실제 endpoint가 문자열 식별자를 404 fallback으로 보내는 증거가 없었다. +- 어떻게: `AiCharacterAdminAudioContentOwnershipTest`에서 malformed `characterId`/`contentId` RED를 먼저 확인하고, controller resource path를 `[0-9]+`로 제한했다. 기존 콘텐츠/authorization/error focused regression과 legacy characterization을 함께 실행했다. +- 결과: RED는 6건의 400/415 대 404 불일치로 확인했고, 최소 path 제약 적용 후 ownership focused 18건과 지정 content+authorization+error 회귀가 모두 `BUILD SUCCESSFUL`이었다. `ktlintCheck`도 통과했다. +- 남은 항목: `P3-GATE`. + +### `P3-GATE` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성, legacy 회귀를 최종 판정했다. +- 왜: Phase 4 진행 전 `P3-R1`, `P3-T3`~`P3-T7`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다. +- 어떻게: Gate에 명시된 content focused, authorization/error, `ktlintCheck` 세 명령을 fresh 실행했다. +- 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다. +- 남은 항목: `P4-T1` 시리즈 요구사항·계약·코드 리뷰. + +### Phase 2·3 2차 리뷰 완료 — 2026-07-27 + +- 상태: 리뷰 완료, 후속 수정 Goal 대기 +- 무엇을: Phase 2·3 production/test와 기존 review·Gate 완료 기록을 PRD와 다시 대조했다. +- 왜: 기존 focused test 성공뿐 아니라 각 완료 체크박스가 요구한 실제 endpoint·failure-order 증거가 존재하는지 독립 검증하기 위해. +- 어떻게: 두 review 문서에 2차 리뷰를 누적하고 character/content/common test를 `--rerun-tasks`로 한 번에 실행한 뒤 XML별 test 수와 lint를 확인했다. +- 결과: targeted 14개 XML class의 199건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 23s`, `ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 27s`였다. 기능 실패는 재현되지 않았으나 완료 기록보다 직접 증거가 좁은 `REV-009`~`REV-011`을 확정했다. +- 남은 항목: `P2-R2` → `P2-R2-GATE` → `P3-R2` → `P3-R3` → `P3-R2-GATE`. + +### `P3-R2` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 실제 생성 endpoint의 multipart part별 KO/EN/JA 오류, invalid theme 선검증, cover upload 실패 rollback/event 0회 증거를 보강했다. +- 왜: `REV-010`에서 기존 생성 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다. +- 어떻게: `AiCharacterAdminAudioContentCreateTest`를 확장했고, 실패 원인은 production 계약 위반이 아니라 테스트 기대 message와 `NOT_SUPPORTED` fixture의 transaction 누락임을 확인해 테스트만 최소 정정했다. +- 결과: create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. +- 남은 항목: `P3-R3`. + +### `P3-R3` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 수정 차집합, cover 유지/교체/실패, ownership/domain no-side-effect 증거를 Phase 3 actual endpoint 범위에서 보강했다. +- 왜: `REV-011`에서 기존 수정·ownership 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다. +- 어떻게: 기존 Phase 3 content test 보강분을 content/common 회귀로 재검증했다. +- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. +- 남은 항목: `P3-R2-GATE`. + +### `P3-R2-GATE` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: Phase 3 후속 리뷰의 `REV-010`~`REV-011` 처리를 종결했다. +- 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 진행하고 Phase 4 이후는 시작하지 않기 위해서다. +- 어떻게: `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적하고 content/common 회귀와 `ktlintCheck`를 실행했다. +- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. `ktlintCheck` 결과는 검증 기록에 누적한다. +- 남은 항목: 없음. Phase 4는 진행하지 않는다. + +### Phase 2·3 4차 재리뷰 완료 — 2026-07-27 + +- 상태: 리뷰 완료, 후속 보완 대기 +- 무엇을: `P2-R2`, `P3-R2`~`P3-R3` 반영분과 완료 체크리스트를 production 흐름·test method 단위로 다시 대조했다. +- 왜: 회귀 통과만으로 `REV-009`~`REV-011`의 선언된 실패·ownership 증거 전체가 충족됐다고 판정할 수 없기 때문이다. +- 어떻게: 변경된 test 5개, character/content facade와 legacy service failure order를 확인하고 targeted 전체를 `--rerun-tasks`로 실행했다. +- 결과: 관련 14개 XML class의 228건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 44s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 41s`였다. 적용된 테스트는 유효하지만 DB/event 실패, 생성 후반 S3/event 실패와 ownership no-side-effect matrix가 완료 기록보다 좁아 `REV-012`~`REV-014`를 확정했다. +- 남은 항목: `P2-R3` → `P2-R3-GATE` → `P3-R4` → `P3-R3-GATE`. + +### `P2-R3` / `P2-R3-GATE` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: character 생성·수정의 external/S3 실패 locale matrix와 event publish 실패 경계를 보강했다. +- 왜: `REV-012`에서 DB/event 실패 뒤 내부 rollback과 external/S3 잔존 결과 직접 증거가 부족하다고 확정됐기 때문이다. +- 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 KO/EN/JA 대표 실패와 facade 직접 event failure characterization을 추가했다. +- 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다. +- 남은 항목: `P3-R4`. + +### `P3-R4` / `P3-R3-GATE` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: content 생성의 audio upload/event 실패와 실제 네 경로 unknown target no-side-effect matrix를 보강했다. +- 왜: `REV-013`~`REV-014`에서 생성 후반 실패 순서와 ownership/domain 부작용 없음 증거가 부족하다고 확정됐기 때문이다. +- 어떻게: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentOwnershipTest`를 확장하고 content/common 회귀와 `ktlintCheck`를 실행했다. +- 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, `ktlintCheck`는 import 정리 후 `BUILD SUCCESSFUL in 17s`였다. +- 남은 항목: 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다. + +### Phase 2·3 5차 재리뷰 완료 — 2026-07-27 + +- 상태: 리뷰 완료, 후속 수정 Goal 대기 +- 무엇을: `P2-R3`, `P3-R4` 반영분의 transaction/rollback, multipart binding, ownership/domain 완료 증거를 PRD·production·test method 단위로 다시 대조했다. +- 왜: 통과하는 테스트가 기존 finding의 exact exception, actual transaction과 전체 side-effect assertion을 실제로 보장하는지 확인하기 위해서다. +- 어떻게: `phase2-character-review.md`, `phase3-audio-content-review.md`에 5차 리뷰를 누적하고 변경된 핵심 test 세 클래스를 `--rerun-tasks`로 실행한 뒤 XML 수치, lint와 diff check를 확인했다. +- 결과: 세 XML 합계 79건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 7m 50s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 28s`, `git diff --check`는 출력이 없었다. 테스트는 통과했지만 `REV-015`~`REV-017`의 계약·완료 증거 누락을 확정했다. +- 남은 항목: `P2-R4` → `P2-R4-GATE` → `P3-R5` → `P3-R6` → `P3-R4-GATE`. + +### `P3-R5` 완료 — 2026-07-27 + +- 상태: 완료 +- 무엇을: 생성 controller와 facade의 `coverImage`·`audioFile`을 non-null `MultipartFile`로 고정하고, 세 필수 part 누락 KO/EN/JA actual endpoint 9건의 exact MVC binding 계약을 보강했다. +- 왜: nullable 파일 part가 facade와 legacy `AudioContentService`까지 전달되어 `MissingServletRequestPartException` 및 `common.error.invalid_request` 계약을 우회했기 때문이다. +- 어떻게: RED에서 `coverImage`·`audioFile` 누락 6건이 legacy content 전용 message로 실패함을 확인한 뒤, non-null binding으로 변경했다. 공통 helper는 각 요청의 400 `ApiResponse.error`, exact exception, DB count 0, S3 `putObject` 0회, event no-interaction을 단언한다. +- 결과: create/error focused 회귀는 `BUILD SUCCESSFUL in 1m 1s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 controller binding의 직접 영향 범위를 두 targeted 명령이 포함하고 release/Gate 범위가 아니므로 실행하지 않았다. +- 남은 항목: `P3-R6` 및 `P3-R4-GATE`. + +### `P3-R6` 완료 — 2026-07-27 + +- 상태: 완료. `P3-R4-GATE`는 `P2-R4-GATE` 뒤 실행 대기다. +- 무엇을: `AiCharacterAdminAudioContentControllerTest`의 cross-owner detail/update, create/update 다른 owner `seriesIds`, invalid `releaseDateUtc` 실제 endpoint를 각각 KO/EN/JA matrix로 확장했다. +- 왜: 기존 단일 locale 또는 부분 assertion으로는 ownership/domain 검증이 S3 업로드, DB 변경, event publish보다 앞선다는 완료 증거가 부족했다. +- 어떻게: 테스트 우선으로 exact 400 `ApiResponse.error` envelope, 요청 전후 `AudioContent`·`SeriesContent` count와 field/연결 row, S3 `putObject` 0회, `ApplicationEventPublisher` no-interaction을 단언했다. target과 other-owner ID를 분리한 fixture라 owner/domain guard를 제거하면 success/status 또는 state assertion이 실패한다. +- 결과: focused characterization은 `BUILD SUCCESSFUL in 34s`로 기존 production 계약 충족을 확인해 test-only로 종료했다. content/common 회귀는 `BUILD SUCCESSFUL in 1m 13s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 12s`였다. 전체 `./gradlew test`는 변경이 Phase 3 content endpoint 테스트에 한정되고 focused·content/common 명령이 직접 범위를 포함하므로 실행하지 않았다. +- 남은 항목: `P2-R4-GATE` 후 `P3-R4-GATE`. Phase 4는 진행하지 않는다. + +### `P2-R4-GATE` / `P3-R4-GATE` 완료 — 2026-07-28 + +- 상태: 완료 +- 무엇을: Phase 2·3 5차 리뷰 보완의 최종 Gate를 종결했다. +- 왜: `REV-015` actual transaction evidence, `REV-016` exact multipart binding, `REV-017` ownership/domain matrix가 모두 보강됐는지 fresh 검증으로 판정하기 위해서다. +- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 실행하고 review 문서의 상태를 처리 완료로 갱신했다. +- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. `P2-R4-GATE`, `P3-R4-GATE` 모두 완료했고 Phase 4는 사용자 진행 지시 전까지 시작하지 않는다. +- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시가 필요하다. + +### Phase 2·3 6차 리뷰 완료 — 2026-07-28 + +- 상태: 완료 +- 무엇을: 5차 보완 결과와 Endpoint Contract Summary, multipart empty-file 경계, Phase 3 event no-interaction의 실제 + 관찰 대상을 다시 대조했다. +- 왜: 통과하는 focused test와 완료된 review ID 외에 client contract 위반, 제한 조건의 파일 손상 가능성 또는 + detached mock으로 가려진 증거 공백이 남았는지 확인하기 위해서다. +- 어떻게: character/content production과 관련 test를 정적 대조하고 Phase 2·3 5차 Gate focused 5개 class를 + `--rerun-tasks`로 재실행했다. 두 review 문서에 `REV-018`~`REV-020`을 누적하고 각 finding을 독립 Task/Gate에 연결했다. +- 결과: focused XML 합계 216건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 16s`, + `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 17s`, staged/unstaged `git diff --check`는 출력이 없었다. + 테스트 통과와 별개로 문서 계약 1건, production empty-file 경계 1건, test evidence 1건을 확정했다. +- 보완 결과: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`를 완료했다. +- 최종 검증: content/common 회귀는 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`, `git diff --check`는 출력이 없었다. +- 남은 항목: 없음. Phase 4는 사용자 진행 지시 전까지 시작하지 않는다. + +### Phase 2·3 6차 보완 재점검 완료 — 2026-07-28 + +- 상태: 완료 +- 무엇을: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`의 실제 코드·테스트·완료 기록을 다시 + 대조하고 하단 종합 finding 상태를 점검했다. +- 왜: 6차 보완의 empty-file·actual publisher·문서 계약 수정이 실제로 유지되는지와 완료된 finding이 미처리 상태로 + 남아 있지 않은지 확인하기 위해서다. +- 어떻게: Phase 2 character mutation과 Phase 3 create/update/controller/ownership 5개 class를 `--rerun-tasks`로 + 실행하고, `REV-001`~`REV-020`의 소유 Goal·Gate·Progress 기록을 하단 종합 표와 대조했다. +- 결과: 5개 XML 합계 130건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 19s`였다. 6차 보완 범위의 추가 + production 결함은 재현되지 않았다. 별도 문서 문제로 종결된 `REV-001`~`REV-009`가 종합 표에서 `확정`으로 남은 상태 + 불일치를 확인해 `처리 완료`로 동기화했다. +- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시 전까지 시작하지 않는다. + ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | |---|---|---|---|---|---| | 2026-07-27 | `DEC-GOAL-001` | 확정 | 기존 Phase 2·3 완료 Task는 이력으로 보존하고 심층 리뷰·세부 보완·Gate Goal을 추가한다. | 기존 검증 이후 후속 보완이 반복됐고 Phase 완료 Gate가 없었다. | `P2-R1`~`P3-GATE` | | 2026-07-27 | `DEC-REVIEW-001` | 확정 | 코드 리뷰의 8개 확정 finding은 기존 미실행 범주형 Goal에 `REV-001`~`REV-008`로 귀속하고 Phase 안에서 직렬 실행한다. | 새 Goal을 중복 추가하거나 기존 완료 이력을 다시 열지 않으면서 각 finding의 재현·완료 증거를 독립 추적하기 위해. | `P2-R1`~`P2-GATE`, `P3-R1`~`P3-GATE` | +| 2026-07-27 | `DEC-P2-T4-001` | 확정 | POST 생성은 외부 API 필수 입력인 `systemPrompt`를 받고, `externalCharacterId`는 외부 API가 반환하는 response 전용 값이며, `isActive`는 서버가 `true`로 생성하는 response 상태다. `characterType`은 생략 시 `Character`, 잘못된 값은 외부 부작용 전 400이다. 중복 이름은 legacy와 같은 `findByName` 선검증만 적용하며 신규 DDL 없이 동시 요청의 DB unique 보장은 추가하지 않는다. 원작·중복·타입 검증은 외부 생성 전에 수행한다. 외부 API 실패는 DB/S3/event를 남기지 않고, S3 실패는 DB transaction과 event를 롤백하지만 legacy에 삭제 API가 없으므로 이미 생성된 외부 캐릭터는 보상하지 않는다. | Endpoint Contract Summary의 축약 JSON이 외부 API 반환값을 입력처럼 표기하지만, legacy 등록과 v2 external client 모두 `systemPrompt`로 외부 생성을 요청하고 ID를 응답에서 받는다. PRD의 external API 계약·DDL 변경 금지와 legacy failure order를 유지한다. | `P2-T4`, `REV-002`, `REV-003`, `REV-007` | +| 2026-07-27 | `DEC-P2-T5-001` | 확정 | PUT의 `externalCharacterId`는 response 전용으로 명시 거부한다. `isActive=false`는 image와 일반 수정 field를 섞지 않는 단독 soft delete다. 일반 수정은 image를 생략하면 기존 경로를 유지하고, 존재하지 않는 `originalWorkId`는 외부 수정 전에 거부한다. 외부 수정 실패는 S3/DB/event를 남기지 않으며, S3 실패는 DB/event를 롤백하지만 legacy와 같은 external update restore 계약이 없어 성공한 외부 수정은 보상하지 않는다. 응답 `updatedAtUtc`는 DB flush 후 매핑한다. | 기존 service의 soft delete는 다른 field를 무시해 이미지 업로드 고아를 남겼고, `@PreUpdate` timestamp는 flush 전에는 이전 값을 반환했다. 외부 API delete/restore 추가와 DDL은 범위 밖이다. | `P2-T5`, `REV-002`, `REV-003`, `REV-007` | +| 2026-07-27 | `DEC-REVIEW-002` | 확정 | 기존 Phase 2·3 Task/Gate 완료 이력은 보존하고, 2차 리뷰에서 확인한 검증 증거 누락은 `REV-009`~`REV-011`과 새 후속 Task/Gate로 처리한다. | fresh 199건은 모두 통과했지만 기존 완료 체크리스트의 실제 endpoint·failure-order 범위와 test method가 일치하지 않았다. | `P2-R2`~`P3-R2-GATE`, 두 2차 review | +| 2026-07-27 | `DEC-REVIEW-003` | 확정 | 3차 수정에서 유효하게 보강된 범위와 기존 완료 이력은 보존하고, 아직 직접 고정되지 않은 실패·ownership 경계만 `REV-012`~`REV-014`와 새 Task/Gate로 추적한다. | fresh 228건은 모두 통과했지만 `Task 2.8`~`Task 3.10`의 완료 체크리스트와 실제 failure injection·side-effect assertion 범위가 다시 일치하지 않았다. | `P2-R3`~`P3-R3-GATE`, 두 4차 review | +| 2026-07-27 | `DEC-REVIEW-004` | 확정 | 4차 보완의 유효한 테스트와 완료 이력은 보존하고, exact multipart 계약과 actual transaction·ownership/domain 직접 증거 누락은 `REV-015`~`REV-017` 및 새 Task/Gate로 추적한다. | fresh 79건은 통과했지만 nullable file binding, direct facade event test와 unknown target에 한정된 matrix가 기존 완료 조건보다 좁았다. | `P2-R4`~`P3-R4-GATE`, 두 5차 review | +| 2026-07-28 | `DEC-REVIEW-005` | 확정 | 5차 보완과 완료 이력은 보존하고, 추가로 확인한 문서 계약·empty-file 경계·detached publisher 증거 문제를 `REV-018`~`REV-020`과 새 후속 Task/Gate로 추적한다. | fresh focused 216건과 lint는 통과했지만 Endpoint Contract Summary, `MultipartFile.isEmpty` 처리와 실제 service publisher field를 코드·test 단위로 대조해 세 문제가 재현됐다. | `P2-R5`~`P3-R5-GATE`, 두 6차 review | +| 2026-07-28 | `DEC-P3-R7-001` | 확정 | 생성의 빈 `coverImage`·`audioFile`은 400 `common.error.invalid_request`로 거부한다. 수정의 빈 `coverImage`는 생략으로 정규화하고, 수정 `audioFile`은 미지원이므로 part가 존재하면 크기와 관계없이 400으로 거부한다. | non-null binding은 part 누락만 차단하며 빈 파일은 0-byte upload와 cover 교체를 유발할 수 있다. optional cover의 빈 part는 일반 multipart client의 생략 표현으로 안전하게 처리할 수 있지만 미지원 audio part는 존재 자체가 계약 위반이다. | `P3-R7`, `REV-019`, Endpoint Contract Summary | +| 2026-07-28 | `DEC-REVIEW-006` | 확정 | Phase 2·3 6차 보완의 코드와 완료 이력은 유지하고 새 production Goal은 추가하지 않는다. 종결 Gate가 있는 `REV-001`~`REV-009`의 종합 표 상태만 `처리 완료`로 동기화한다. | fresh focused 130건이 모두 통과했고 6차 보완 범위의 추가 production 결함은 재현되지 않았지만, 하단 종합 표 상태가 각 Gate·Progress의 처리 완료 판정과 모순됐다. | `P2-GATE`, `P2-R2-GATE`, `P3-GATE`, 하단 발견된 문제 표 | ## 발견된 문제 | ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | |---|---|---|---|---|---| -| `REV-001` | Blocker | 확정 | 문자열 미매핑 경로가 `/{characterId}`에 매핑되어 Phase 1의 404 계약 대신 type mismatch 400을 반환한다. | `P2-R1`, `P2-T6` | numeric path mapping RED/GREEN과 KO/EN/JA·CORS 회귀 4건을 고정한다. | -| `REV-002` | High | 확정 | 캐릭터 생성·수정 DTO의 `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 soft-delete 혼합 의미가 Endpoint Contract Summary와 다르다. | `P2-R1`, `P2-T4`, `P2-T5` | canonical request를 먼저 확정하고 문서 JSON 역직렬화·반영 test로 맞춘다. | -| `REV-003` | High | 확정 | 외부 API·S3·DB 실패 사이에 보상 경계가 없고 동시 중복 이름 생성의 원자성 증거가 없다. | `P2-R1`, `P2-T4`, `P2-T5` | failure-order characterization, 선검증, 동시성 정책과 보상/정리 결과를 고정한다. | -| `REV-004` | High | 확정 | 동일한 `seriesIds` 수정도 기존 연결을 삭제·재생성해 row ID·`orders`·`createdAt`을 소실한다. | `P3-R1`, `P3-T6` | 교집합 row를 보존하고 차집합만 변경하는 RED/GREEN을 추가한다. | -| `REV-005` | High | 확정 | 콘텐츠 상세의 `releaseDate`, `isOnlyRental`, `purchaseOption`이 legacy 상세 파생 규칙과 다르다. | `P3-R1`, `P3-T4` | legacy 조합 matrix와 v2 `releaseDateUtc` 분리 mapping을 고정한다. | -| `REV-006` | High | 확정 | 콘텐츠 생성의 필수 `coverImage`·`audioFile`이 nullable binding이라 정확한 missing-part 오류 계약을 우회한다. | `P3-R1`, `P3-T5`, `P3-T7` | non-null binding과 세 part별 exact exception·KO/EN/JA endpoint test를 추가한다. | -| `REV-007` | Medium | 확정 | 목록 초과 field, pre-flush timestamp, `characterType`, legacy/public 중첩 DTO, `tags`·생성 `isActive`에서 추가 계약/parity 차이가 있다. | `P2-T3`~`P2-T6`, `P3-T3`~`P3-T5`, `P3-T7` | 각 소유 Goal에서 exact schema·field 의미·DTO 경계와 legacy parity를 분리 검증한다. | -| `REV-008` | High | 확정 | endpoint별 인가/i18n, failure-order characterization과 계획에 명시된 test 파일·검증 수의 완료 증거가 부족하다. | `P2-R1`, `P2-T6`, `P3-R1`, `P3-T5`, `P3-T7` | 실제 endpoint matrix와 characterization을 보강하고 기존 기록을 삭제하지 않은 채 정정 기록을 누적한다. | +| `REV-001` | Blocker | 처리 완료 | 문자열 미매핑 경로가 `/{characterId}`에 매핑되어 Phase 1의 404 계약 대신 type mismatch 400을 반환한다. | `P2-R1`, `P2-T6` | numeric path mapping과 KO/EN/JA·CORS 회귀를 고정했다. | +| `REV-002` | High | 처리 완료 | 캐릭터 생성·수정 DTO의 `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 soft-delete 혼합 의미가 Endpoint Contract Summary와 다르다. | `P2-R1`, `P2-T4`, `P2-T5` | canonical request를 확정하고 문서·actual endpoint 계약을 동기화했다. | +| `REV-003` | High | 처리 완료 | 외부 API·S3·DB 실패 사이에 보상 경계가 없고 동시 중복 이름 생성의 원자성 증거가 없다. | `P2-R1`, `P2-T4`, `P2-T5` | failure-order·선검증·비보상 경계와 신규 DDL 없는 동시성 정책을 고정했다. | +| `REV-004` | High | 처리 완료 | 동일한 `seriesIds` 수정도 기존 연결을 삭제·재생성해 row ID·`orders`·`createdAt`을 소실한다. | `P3-R1`, `P3-T6` | 교집합 row를 보존하고 차집합만 변경하도록 수정·검증했다. | +| `REV-005` | High | 처리 완료 | 콘텐츠 상세의 `releaseDate`, `isOnlyRental`, `purchaseOption`이 legacy 상세 파생 규칙과 다르다. | `P3-R1`, `P3-T4` | legacy 조합 matrix와 v2 `releaseDateUtc` 분리 mapping을 고정했다. | +| `REV-006` | High | 처리 완료 | 콘텐츠 생성의 필수 `coverImage`·`audioFile`이 nullable binding이라 정확한 missing-part 오류 계약을 우회한다. | `P3-R1`, `P3-T5`, `P3-T7` | non-null binding과 세 part별 exact exception·KO/EN/JA endpoint 회귀를 추가했다. | +| `REV-007` | Medium | 처리 완료 | 목록 초과 field, pre-flush timestamp, `characterType`, legacy/public 중첩 DTO, `tags`·생성 `isActive`에서 추가 계약/parity 차이가 있다. | `P2-T3`~`P2-T6`, `P3-T3`~`P3-T5`, `P3-T7` | 소유 Goal별 exact schema·field 의미·DTO 경계와 legacy parity를 분리 검증했다. | +| `REV-008` | High | 처리 완료 | endpoint별 인가/i18n, failure-order characterization과 계획에 명시된 test 파일·검증 수의 완료 증거가 부족하다. | `P2-R1`, `P2-T6`, `P3-R1`, `P3-T5`, `P3-T7` | 실제 endpoint matrix와 characterization을 보강하고 정정 기록을 누적했다. | +| `REV-009` | Medium | 처리 완료 | Phase 2 실제 네 endpoint의 stale claim·허용/거부 CORS·오류와 mutation 실패 경계 직접 증거가 Gate 기록보다 좁다. | `P2-R2`, `P2-R2-GATE` | actual endpoint matrix와 실패 후 DB/S3/external/event 결과를 보강했다. | +| `REV-010` | Medium | 처리 완료 | Phase 3 생성의 invalid theme, 실제 multipart KO/EN/JA와 S3/processing/event 실패 순서 직접 증거가 완료 기록보다 좁다. | `P3-R2`, `P3-R2-GATE` | 생성 actual endpoint와 legacy failure characterization을 보강했다. | +| `REV-011` | Medium | 처리 완료 | Phase 3 수정의 교집합+추가+제거 metadata, cover 성공/실패와 ownership no-side-effect 직접 증거가 완료 기록보다 좁다. | `P3-R3`, `P3-R2-GATE` | update/ownership actual endpoint 회귀를 보강했다. | +| `REV-012` | Medium | 처리 완료 | Phase 2의 DB save/flush·event publish 실패 뒤 내부 rollback과 external/S3 잔존 결과를 직접 검증하지 않았다. | `P2-R3`, `P2-R3-GATE` | 실제 transaction failure injection과 내부·외부 결과 단언을 보강했다. | +| `REV-013` | Medium | 처리 완료 | Phase 3 생성은 첫 cover upload 실패만 검증하고 두 번째 audio upload와 event 실패 결과를 직접 고정하지 않았다. | `P3-R4`, `P3-R3-GATE` | 생성 후반 failure order와 비트랜잭션 S3 결과를 특성화했다. | +| `REV-014` | Medium | 처리 완료 | Phase 3 실제 네 경로의 ownership/domain KO/EN/JA와 DB/S3/event no-side-effect matrix가 부분적이다. | `P3-R4`, `P3-R3-GATE` | actual endpoint 오류와 요청 전후 부작용 count를 보강했다. | +| `REV-015` | Medium | 처리 완료 | Phase 2 event 실패 테스트가 actual endpoint/Spring transaction을 통과하지 않고 DB save/flush·내부 rollback을 직접 단언하지 않는다. | `P2-R4`, `P2-R4-GATE` | actual POST/PUT persistence·event 실패와 transaction 종료 뒤 내부·외부 결과를 고정했다. | +| `REV-016` | High | 처리 완료 | Phase 3 생성 필수 `coverImage`·`audioFile`이 nullable이라 exact `MissingServletRequestPartException` 계약을 우회했다. | `P3-R5`, `P3-R4-GATE` | non-null binding과 세 part별 exact exception·KO/EN/JA·no-side-effect를 복구했다. | +| `REV-017` | Medium | 처리 완료 | Phase 3 4차 보강이 unknown target에 한정돼 cross-owner/domain KO/EN/JA와 DB/S3/event matrix가 완료 기록보다 좁다. | `P3-R6`, `P3-R4-GATE` | 대표 cross-owner/series/date actual endpoint의 전체 no-side-effect matrix를 보강했다. | +| `REV-018` | High | 처리 완료 | 캐릭터 생성 Endpoint Contract Summary가 필수 `systemPrompt`를 누락하고 request 금지 `externalCharacterId`, `isActive`를 포함해 확정 계약과 반대다. | `P2-R5`, `P2-R5-GATE` | 생성 JSON을 `DEC-P2-T4-001`과 동기화하고 기존 actual endpoint 계약 회귀로 확인했다. | +| `REV-019` | Medium | 처리 완료 | 생성·수정의 빈 multipart 파일이 null/non-empty 검사 사이를 통과해 0-byte upload, cover 교체 또는 수정 `audioFile` 미지원 계약을 우회한다. | `P3-R7`, `P3-R5-GATE` | v2 facade에서 생성 empty-file 거부, 수정 empty cover 정규화와 모든 audio part 거부를 RED/GREEN으로 고정했다. | +| `REV-020` | Low | 처리 완료 | Phase 3 ownership/domain test의 event no-interaction mock이 실제 `AudioContentService`·`CreatorAdminContentService` publisher field에 연결되지 않았다. | `P3-R8`, `P3-R5-GATE` | 실제 두 service proxy target의 publisher를 mock으로 교체·복원하고 identity/no-interaction을 단언했다. | ## 검증 기록 +- Phase 2·3 6차 보완 재점검 focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`였다. 다섯 XML 합계 130건의 failure/error/skipped는 모두 0이었다. +- Phase 2·3 6차 보완 재점검 문서 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 858ms`였다. +- Phase 2·3 6차 보완 재점검 판정(2026-07-28): `REV-018`~`REV-020`의 production/test 보완과 Gate 완료 증거가 일치했고 추가 production finding은 확정되지 않았다. 하단 종합 표에서만 미처리로 남은 `REV-001`~`REV-009`를 각 소유 Gate·Progress 판정에 맞춰 `처리 완료`로 동기화했다. +- `P3-R5-GATE` content/common 회귀(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 2m 21s`였다. +- `P3-R5-GATE` lint/diff 검증(2026-07-28): `./gradlew ktlintCheck` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 44s`였고, `git diff --check`는 출력이 없었다. +- `P3-R5-GATE` 전체 회귀 생략(2026-07-28): 변경 범위가 Phase 2 문서와 Phase 3 content v2 facade/test에 한정되고 content/common 회귀가 실제 영향 범위를 포함하므로 전체 `./gradlew test`는 실행하지 않았다. +- Phase 2·3 6차 리뷰 fresh focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 16s`였다. 다섯 XML 합계 216건의 failure/error/skipped는 모두 0이었다. +- Phase 2·3 6차 리뷰 lint/diff 검증(2026-07-28): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 17s`였고, staged/unstaged `git diff --check`는 출력이 없었다. +- Phase 2·3 6차 리뷰 전체 회귀 생략(2026-07-28): production code를 변경하지 않은 read-only review와 문서 후속 Task 등록이며, 5차 보완의 핵심 actual endpoint 216건과 lint로 직접 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 `P3-R7` production 수정 후 content/common 영향 범위 회귀를 실행한다. +- Phase 2·3 5차 Gate focused 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 52s`였다. +- Phase 2·3 5차 Gate diff 검증(2026-07-28): `git diff --check`는 출력이 없었다. +- `P3-R5` RED(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest` 실행 결과 14건 중 `coverImage`·`audioFile` 누락 KO/EN/JA 6건이 실패했다. 예를 들어 audio 누락 KO는 기대 `잘못된 요청입니다.` 대신 legacy `콘텐츠를 선택해 주세요.`를 반환해 nullable binding이 MVC 경계를 우회함을 확인했다. +- `P3-R5` focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. 세 필수 part 누락 KO/EN/JA는 400 generic envelope, exact `MissingServletRequestPartException`, DB/S3/event 0회를 실제 endpoint에서 확인했다. +- `P3-R5` content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 46s`였다. +- `P3-R5` lint(2026-07-27): `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`이었다. +- `P3-R5` 최종 fresh 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 모든 Gradle task를 재실행해 `BUILD SUCCESSFUL in 3m 33s`였고, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 25s`였다. +- Phase 2·3 5차 리뷰 fresh focused 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 7m 50s`였다. 세 XML 합계 79건의 failure/error/skipped는 모두 0이었다. +- Phase 2·3 5차 리뷰 lint/diff 검증(2026-07-27): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 28s`였고, `git diff --check`는 출력이 없었다. +- Phase 2·3 5차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 review/계획 문서 Task 등록이며 변경된 세 핵심 test를 fresh 실행해 finding을 판정했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 수정 Goal Gate에서 character/content/common 영향 범위 회귀를 각각 실행한다. +- Phase 2·3 5차 리뷰 문서 명령 유효성(2026-07-27): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 1s`였다. +- Phase 2·3 4차 보완 focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 1m 8s`였다. +- Phase 2·3 4차 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 20s`였다. +- Phase 2·3 4차 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 신규 test import ordering/unused import 3건 실패 후 import만 정리해 재실행했고, `BUILD SUCCESSFUL in 17s`였다. +- Phase 2·3 4차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 44s`였다. 관련 XML 14개 합계 228건의 failure/error/skipped는 모두 0이었다. +- Phase 2·3 4차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 41s`였다. +- Phase 2·3 4차 리뷰 전체 회귀 생략(2026-07-27): production code를 변경하지 않은 리뷰·문서 Task 등록이며 character/content actual endpoint와 공통 authorization/error를 포함한 fresh 228건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다. +- Phase 2·3 2차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 23s`였다. 생성된 관련 XML 14개 합계 199건의 failure/error/skipped는 모두 0이었다. +- Phase 2·3 2차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 27s`였다. +- Phase 2·3 2차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 read-only review와 문서 후속 Task 등록이며, character/content actual endpoint와 공통 authorization/error를 포함한 fresh 199건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다. +- Phase 3 후속 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 41s`였다. +- Phase 3 후속 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 import ordering 1건 실패 후 `AiCharacterAdminAudioContentUpdateTest.kt` import 순서만 정리해 재실행했고, `BUILD SUCCESSFUL in 16s`였다. - Phase 2·3 코드 리뷰 Task 보완(2026-07-27): 확정 finding 8개를 `REV-001`~`REV-008`로 등록하고 기존 미실행 `P2-T3`~`P2-T6`, `P3-T3`~`P3-T7`에 소유권, 정확한 RED/RED 확인/GREEN/GREEN 확인/REFACTOR, 파일 경로, focused 명령과 Gate 완료 증거를 보강했다. 중복 Goal은 만들지 않고 각 Phase 안에서 직렬 실행하도록 의존성 표를 갱신했다. diff --git a/docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md b/docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md new file mode 100644 index 00000000..118b0239 --- /dev/null +++ b/docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md @@ -0,0 +1,583 @@ +# Phase 2 캐릭터 관리 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 2 / AI 캐릭터 관리 vertical slice | +| 기준 commit 또는 working tree | `8197f90b` + staged Phase 2/3 changes | +| 리뷰 일자 | 2026-07-27 | +| 리뷰어 | Sisyphus | +| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- PRD Feature B와 Endpoint Contract Summary의 캐릭터 목록/상세/생성/수정/비활성화 계약을 현재 Phase 2 코드·테스트에 추적한다. +- 기존 완료 이력(`P2-H1`, `P2-H2`) 이후 남은 확정 finding을 `P2-T3`~`P2-T6` 소유 Goal에 연결한다. + +### 포함 범위 + +- 코드: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/*`, `ChatCharacterRepository.kt` +- 테스트: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/*` +- 문서: PRD Feature B, API Expectations, plan Phase 2 + +### 제외 범위 + +- Phase 3 이후 기능, production code 수정, 신규 계약 변경 + +## 3. 판정 기준 + +| 심각도 | 기준 | +|---|---| +| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 | +| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 | +| Medium | 제한된 조건에서 발생하는 기능·복구 문제 | +| Low | 유지보수성, 문서 정합성 문제 | + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: `prd.md` Feature B, API Expectations 184~189 +- 계획: `plan-task.md` `P2-R1`, `P2-T3`~`P2-T6`, `REV-001`~`REV-003`, `REV-007`, `REV-008` +- 코드: `AiCharacterAdminCharacterController.kt:20`, `AiCharacterAdminCharacterDto.kt:3`, `AiCharacterAdminCharacterDto.kt:11`, `AiCharacterAdminCharacterFacade.kt:38`, `AiCharacterAdminCharacterFacade.kt:63`, `AiCharacterAdminCharacterMapper.kt:20`, `ChatCharacterRepository.kt:59` +- 테스트: `AiCharacterAdminCharacterControllerTest.kt:43`, `AiCharacterAdminCharacterControllerMutationTest.kt:50`, `LegacyChatCharacterAdminCharacterizationTest.kt:39`, `LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt:42` + +### 실행 환경 + +```text +OS: macOS (darwin) +Java/Gradle: repository Gradle wrapper +환경 변수: test profile defaults, EmbeddedRedisInitializer, local stub external character API +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'` | 성공 | `BUILD SUCCESSFUL in 51s` | +| 코드 추적 | 성공 | 목록 응답이 상세 DTO를 재사용하고 있음 확인 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-001` | Blocker | 확정 | 문자열 미매핑 경로가 detail binding에 잡힐 수 있음 | `P2-T6` | numeric path mapping 오류 계약 보완 | +| `REV-002` | High | 확정 | mutation request 계약이 Endpoint Contract Summary와 불일치 | `P2-T4`, `P2-T5` | canonical request 확정 후 RED/GREEN | +| `REV-003` | High | 확정 | 외부 API·S3·DB 실패 보상/원자성 증거 부족 | `P2-T4`, `P2-T5` | failure-order와 no-partial-state 보완 | +| `REV-007` | Medium | 확정 | 목록 응답이 상세 전용 field를 포함 | `P2-T3` | 목록 전용 DTO와 exact key 테스트 | +| `REV-008` | High | 확정 | 실제 endpoint별 인가·i18n·회귀 증거 부족 | `P2-T6` | endpoint matrix 보강 | + +## 6. 발견 사항 상세 + +### REV-001 — 문자열 미매핑 경로 404 계약 보강 필요 + +- **심각도:** Blocker +- **상태:** 확정 +- **관련 요구사항:** PRD API Expectations 174~180 +- **관련 계약:** 신규 prefix 미매핑 경로 404, binding 오류 400 분리 +- **소유 Task:** `P2-T6` + +**관찰 내용** + +`AiCharacterAdminCharacterController`는 `@GetMapping("/{characterId}")`에서 `Long` path variable을 직접 받는다. 문자열 segment가 이 mapping에 먼저 걸리면 404 fallback이 아니라 type mismatch 400으로 분류될 수 있으므로 `P2-T6`의 numeric path mapping RED/GREEN이 필요하다. + +**근거** + +- 코드: `AiCharacterAdminCharacterController.kt:29` `/{characterId}` mapping +- 코드: `AiCharacterAdminExceptionHandler.kt:67` `MethodArgumentTypeMismatchException`은 400으로 처리 +- 문서: `plan-task.md` `P2-T6` RED 조건 + +**권장 조치** + +numeric `characterId` path만 resource handler에 매핑되도록 최소 수정하고 KO/EN/JA·CORS 포함 404 회귀 테스트를 추가한다. + +### REV-002 — mutation request 계약 불일치 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Feature B, API Expectations 185~189 +- **관련 계약:** Endpoint Contract Summary의 생성/수정 multipart request +- **소유 Task:** `P2-T4`, `P2-T5` + +**관찰 내용** + +현재 생성 DTO는 `systemPrompt`를 필수로 요구하고 `externalCharacterId`, 생성 `isActive`를 받지 않는다. Endpoint Contract Summary의 축약 request와 Phase 2 후속 계획의 canonical request 확정 항목이 충돌하므로, 구현 수정 전에 기존 legacy/external API parity 기준으로 canonical request를 먼저 고정해야 한다. + +**근거** + +- 코드: `AiCharacterAdminCharacterDto.kt:27` 생성 DTO field set +- 코드: `AiCharacterAdminCharacterExternalApiClient.kt:22` 외부 API create body는 서버가 받은 field를 외부 생성 요청으로 전송 +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:50` 생성 정상 케이스는 `systemPrompt` 포함 최소 JSON만 검증 + +**권장 조치** + +`P2-T4`에서 문서 JSON 역직렬화·반영 테스트와 invalid `characterType` 테스트를 먼저 실패시킨 뒤, 확정 계약만 최소 구현한다. + +### REV-003 — mutation 실패 원자성 증거 부족 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Feature B Edge Cases +- **관련 계약:** 외부 API·S3·DB 실패 시 부분 저장 방지 +- **소유 Task:** `P2-T4`, `P2-T5` + +**관찰 내용** + +생성은 외부 API 호출 후 DB 생성, 선택 이미지 업로드, 원작 연결, 이벤트 발행 순서로 진행된다. 기존 테스트는 정상 생성, unreadable JSON, soft delete 보존만 검증하고 외부 API 실패, S3 실패, 존재하지 않는 원작, 동시 중복 이름에서 잔존 DB/S3/event 결과를 고정하지 않는다. + +**근거** + +- 코드: `AiCharacterAdminCharacterFacade.kt:63` 생성 transaction +- 코드: `AiCharacterAdminCharacterFacade.kt:71` 외부 API 결과로 DB 생성 +- 코드: `AiCharacterAdminCharacterFacade.kt:96` 이미지 업로드 후 save +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:50` 실패 지점별 원자성 테스트 부재 + +**권장 조치** + +`P2-T4`/`P2-T5`에서 실패 지점별 RED를 만들고, 외부 부작용 전 DB 참조 검증과 필요한 보상 경계만 추가한다. + +### REV-007 — 목록 응답 field set 계약 위반 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD API Expectations 189 +- **관련 계약:** Endpoint Contract Summary의 목록 item과 상세 response 분리 +- **소유 Task:** `P2-T3` + +**관찰 내용** + +목록 응답 `items`가 상세 응답 DTO `AiCharacterAdminCharacterResponse`를 그대로 사용한다. 그래서 목록 계약에 없는 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출될 수 있다. + +**근거** + +- 코드: `AiCharacterAdminCharacterDto.kt:3` `AiCharacterAdminListResponse.items: List` +- 코드: `AiCharacterAdminCharacterDto.kt:18` 상세 전용 `creatorProfileImageUrl` +- 코드: `AiCharacterAdminCharacterDto.kt:19` 상세 전용 `creatorIntroduce` +- 코드: `AiCharacterAdminCharacterDto.kt:24` 상세 전용 `updatedAtUtc` +- 코드: `AiCharacterAdminCharacterFacade.kt:51` 목록에서 `characterMapper::toResponse` 사용 +- 테스트: `AiCharacterAdminCharacterControllerTest.kt:43` 목록 exact key 검증 부재 + +**권장 조치** + +목록 전용 item DTO를 추가하고 exact JSON key 테스트로 상세 전용 field 비노출을 고정한다. + +### REV-008 — endpoint matrix와 완료 증거 부족 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Metrics 207~221 +- **관련 계약:** endpoint별 ADMIN, 오류 i18n, legacy 회귀 증거 +- **소유 Task:** `P2-T6` + +**관찰 내용** + +현재 Phase 2 focused test는 통과하지만 목록 anonymous/admin smoke와 일부 binding만 검증한다. 목록·상세·생성·수정 각각의 JWT role × DB role, stale claim, domain/client/server 오류의 exact status/key/KO·EN·JA matrix는 아직 Phase 2 endpoint 기준으로 충분히 고정되어 있지 않다. + +**근거** + +- 테스트: `AiCharacterAdminCharacterControllerTest.kt:139` 목록 endpoint 인가 smoke만 존재 +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:89` unreadable create request만 400 검증 +- 문서: `plan-task.md` `P2-T6` RED/GREEN 조건 + +**권장 조치** + +`P2-T6`에서 실제 Phase 2 endpoint matrix와 legacy/public 회귀를 focused 명령으로 보강한다. + +## 7. 확정 항목의 plan·goal 전환 + +확정 항목은 기존 `plan-task.md`의 `P2-T3`~`P2-T6`에 이미 소유 Goal로 연결되어 있다. 신규 회귀 수정 Task는 만들지 않는다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | PRD Feature B, Endpoint Contract Summary, Phase 2 production/test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-001`~`REV-003`, `REV-007`, `REV-008` 확정 | +| 확정 항목 plan 반영 | 충족 | 기존 `P2-T3`~`P2-T6`에 연결됨 | +| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 | +| 검증 명령과 결과 기록 | 충족 | focused test `BUILD SUCCESSFUL in 51s` | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-T3`부터 직렬 실행한다. + +## 9. 수정 후 검증 기록 + +### 1차 수정 검증 — 2026-07-27 + +- 무엇을: `P2-T3`에서 `REV-007`의 목록 응답 field set을 수정했다. +- 왜: 목록 item이 상세 DTO를 재사용해 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다. +- 어떻게: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest` — RED는 line 66 실패, GREEN은 `BUILD SUCCESSFUL`. + - `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'` — `BUILD SUCCESSFUL`. + - `./gradlew ktlintCheck` — `BUILD SUCCESSFUL`. +- 남은 항목: `P2-T4`, `P2-T5`, `P2-T6`. + +### 2차 수정 검증 — 2026-07-27 + +- 무엇을: `P2-T4`에서 `REV-002`, `REV-003`, `REV-007` 중 생성 흐름을 보완했다. +- 왜: 생성 request의 서버 소유 field와 존재하지 않는 원작 검증이 외부 부작용 전에 고정되어야 했기 때문이다. +- 어떻게: + - `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest` — `BUILD SUCCESSFUL`. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL`. +- 남은 항목: `P2-T5`, `P2-T6`, `P2-GATE`. + +### 3차 수정 검증 — 2026-07-27 + +- 무엇을: `P2-T5`에서 `REV-002`, `REV-003`, `REV-007`의 캐릭터 수정·비활성화 흐름을 보완했다. +- 왜: `externalCharacterId` 입력, `isActive=false`와 일반 수정/image 혼합, 원작·외부/S3/DB 실패 경계와 flush 전 `updatedAtUtc`가 확정 계약과 달랐기 때문이다. +- 어떻게: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest` — `BUILD SUCCESSFUL in 1m 16s`. + - `./gradlew ktlintCheck` — `BUILD SUCCESSFUL in 17s`. +- 남은 항목: `P2-T6`, `P2-GATE`. + +### 4차 수정 검증 — 2026-07-27 + +- 무엇을: `P2-T6`에서 `REV-001`의 문자열 path 404 계약과 `REV-008`의 실제 Phase 2 endpoint 보안·오류·CORS 증거를 보완했다. +- 왜: `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 매칭되어 KO/EN/JA·허용 Origin CORS 4건 모두 404 대신 400을 반환했고, 목록·상세·생성·수정의 실제 경로 matrix가 부족했기 때문이다. +- 어떻게: + - RED: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`를 실행해 전체 146건 중 위 4건만 404 기대 대비 400으로 실패함을 확인했다. + - GREEN: `AiCharacterAdminCharacterController`의 GET/PUT `characterId` path를 `[0-9]+`로 제한하고, 실제 목록 binding·상세 target·생성/수정 multipart KO/EN/JA, 네 endpoint non-ADMIN, detail preflight 테스트를 추가했다. + - 회귀: 같은 focused 명령은 `BUILD SUCCESSFUL in 1m 26s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 34s`였다. +- 결과: `REV-001`, `REV-008`의 P2-T6 범위는 종료했다. 공통 Phase 1 handler, legacy/public contract, Phase 3 이후 코드는 변경하지 않았고, `P2-GATE`는 별도 Goal로 남겼다. + +## 10. 2차 리뷰 및 검증 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: Phase 2·3 staged/unstaged 변경 전체 +- 리뷰 상태: 판정 완료, 후속 수정 goal 필요 +- 검토 범위: PRD Feature B, Endpoint Contract Summary, `P2-T3`~`P2-GATE`, character production/test와 공통 authorization/error test +- fresh 검증: + - `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 9m 23s`, 관련 XML 14개 합계 199건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 27s`, 7개 task 실행. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-009` | Medium | 확정 | Phase 2 실제 endpoint별 보안·오류·실패 경계 증거가 Gate 기록보다 좁음 | `Task 2.8` | `P2-R2` | + +### REV-009 — Phase 2 실제 endpoint별 완료 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Metrics 207~221, Acceptance Criteria 225~254 +- **관련 계약:** 실제 endpoint별 JWT role × 현재 DB role, stale claim, CORS, KO/EN/JA 오류와 실패 후 상태 +- **소유 Task:** `Task 2.8`, `P2-R2` + +**관찰 내용** + +현재 작성된 character와 공통 authorization/error test 146건은 모두 통과한다. 다만 `P2-T6`과 `P2-GATE`는 목록·상세·생성·수정 각각의 stale ADMIN claim, 허용·거부 Origin, domain/client/server 오류와 외부/S3/DB 실패 경계를 모두 고정했다고 기록했으나 실제 소유 테스트는 네 endpoint의 non-ADMIN JWT, 상세 GET의 허용 Origin, 일부 binding/multipart KO/EN/JA와 S3 실패를 각각 분리해 검증한다. 실제 네 endpoint의 stale claim과 허용·거부 Origin 전체, 외부/API·S3·DB 실패의 exact envelope 및 side-effect 결과를 한 매트릭스로 재현할 직접 증거가 없다. + +**근거** + +- 테스트: `AiCharacterAdminCharacterControllerTest.kt:175`는 네 endpoint의 non-ADMIN JWT만 순회하며 stale ADMIN claim을 포함하지 않는다. +- 테스트: `AiCharacterAdminCharacterControllerTest.kt:195`는 상세 GET의 허용 Origin만 확인하고 거부 Origin과 POST/PUT preflight를 직접 확인하지 않는다. +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:340` 이후는 외부 API/S3 실패의 DB 결과를 확인하지만 실제 endpoint별 KO/EN/JA와 DB 실패 경계를 모두 고정하지 않는다. +- 문서: `plan-task.md`의 `P2-T6`·`P2-GATE`는 위 매트릭스와 외부/S3/DB 보상에 미결정 항목이 없다고 완료 처리했다. + +**영향** + +공통 security/error 계층 때문에 현재 production 결함은 재현되지 않았지만, 실제 character mapping이나 facade 호출 순서가 바뀌어도 공통 fixture test만 통과해 Phase 2 Gate가 놓칠 수 있다. + +**권장 조치** + +`P2-R2`에서 네 실제 endpoint의 JWT/DB role·stale claim과 허용/거부 CORS, 대표 binding/domain/client/server 실패의 exact envelope 및 DB/S3/external/event 결과를 non-vacuous test로 보강한다. 현재 동작이 계약을 이미 만족하면 production code를 수정하지 않고 test와 검증 기록만 추가한다. + +**판정 기록** + +- 2026-07-27 — fresh 199건과 lint는 모두 통과했으나 계획 체크리스트와 실제 test method를 대조해 검증 증거 누락을 확정했다. + +### plan·goal 전환 + +`plan-task.md` Phase 2에 `Task 2.8` / `P2-R2`와 별도 후속 Gate `P2-R2-GATE`를 추가한다. 기존 `P2-T3`~`P2-GATE` 완료 이력은 변경하지 않는다. + +### 2차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | PRD·계획·production·test 재대조 | +| 후보 항목 판정 완료 | 충족 | `REV-009` 확정 | +| 확정 항목 plan 반영 | 충족 | `Task 2.8`, `P2-R2`, `P2-R2-GATE` | +| 검증 명령과 결과 기록 | 충족 | fresh targeted 199건, lint 성공 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-R2` 실행 후 `P2-R2-GATE`에서 Phase 2 후속 리뷰를 종결한다. + +## 11. 3차 수정 검증 — 2026-07-27 + +- 무엇을: `P2-R2`에서 `REV-009`의 실제 character endpoint별 보안·오류·실패 경계 증거를 보강했다. +- 왜: 기존 `P2-T6`/`P2-GATE` 기록이 네 실제 endpoint의 stale ADMIN claim, 허용·거부 Origin preflight, mutation 실패의 KO/EN/JA envelope를 직접 매트릭스로 고정했다는 증거가 부족했기 때문이다. +- 어떻게: + - `AiCharacterAdminCharacterControllerTest`에 목록·상세·생성·수정 실제 경로의 stale ADMIN claim 403과 allow/deny Origin preflight를 추가했다. + - `AiCharacterAdminCharacterControllerMutationTest`의 external API 실패, 생성 S3 실패, 수정 S3 실패를 KO/EN/JA exact message와 DB/S3/external 잔존 상태 단언으로 확장했다. + - `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 1m 47s`. +- 결과: production code 변경 없이 `REV-009`의 직접 증거 보강이 완료됐다. `P2-R2-GATE`를 종료하고 Phase 3 후속 보완으로 진행한다. + +### 3차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| `REV-009` 처리 | 충족 | 실제 endpoint stale claim/CORS와 failure envelope·side-effect test 추가 | +| focused 회귀 | 충족 | character/common focused 명령 성공 | +| 범위 준수 | 충족 | Phase 3 production, Phase 4 이후 작업 없음 | + +**최종 결론:** Phase 2 후속 리뷰 종결 + +## 12. 4차 재리뷰 및 검증 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P2-R2` 반영 후 staged/unstaged Phase 2 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- fresh 검증: + - `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 9m 44s`, 관련 XML 14개 합계 228건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 41s`, 7개 task 실행. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-012` | Medium | 처리 완료 | DB·event 실패 후 내부·외부 부작용 직접 증거가 여전히 없음 | `Task 2.9` | `P2-R3` | + +### REV-012 — character DB·event 실패 경계 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 처리 완료 +- **관련 요구사항:** PRD Metrics 207~221, Acceptance Criteria 225~254 +- **관련 계약:** 생성·수정 실패 뒤 DB/creatorMember/originalWork/event rollback과 이미 발생한 external/S3 비보상 결과 +- **소유 Task:** `Task 2.9`, `P2-R3` + +**관찰 내용** + +`P2-R2` 반영으로 네 실제 endpoint의 stale ADMIN claim과 허용·거부 Origin, external API·생성 S3·수정 S3 실패의 KO/EN/JA message는 직접 고정됐다. 그러나 완료 처리한 `Task 2.8`의 failure checklist에는 DB 실패와 creatorMember/originalWork/event 결과까지 포함되어 있다. 현재 mutation test는 DB save/flush 또는 event publish 실패를 주입하지 않으며, 실패 test의 `@DisplayName`에 event 미발행을 적었지만 publisher 호출을 직접 단언하지 않는다. + +**근거** + +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:346`~`408`은 external API와 첫 S3 실패를 확인하지만 DB/event 실패를 재현하지 않는다. +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:143`, `676`의 publisher mock은 정상 생성·수정 event 확인에만 사용된다. +- 코드: `AiCharacterAdminCharacterFacade.create`와 `update`는 external/S3/DB 처리 뒤 event를 발행하므로 DB 또는 event 실패 시 transaction rollback과 이미 발생한 외부 결과를 별도로 관찰해야 한다. +- 문서: `plan-task.md`의 `Task 2.8`은 DB 실패와 DB/creatorMember/originalWork/S3/external/event 결과를 모두 직접 단언했다고 완료 처리했다. + +**영향** + +현재 테스트는 모두 통과하지만 persistence 또는 event 지점에서 실패했을 때 내부 row가 롤백되고 external/S3 결과만 남는 기존 경계가 바뀌어도 `P2-R2-GATE`가 이를 감지하지 못한다. + +**권장 조치** + +`P2-R3`에서 실제 transaction을 거치는 생성·수정의 DB flush/save와 event publish 실패를 재현하고 ChatCharacter·creatorMember·originalWork·event 및 external/S3 결과를 직접 단언한다. 기존 비보상 경계와 일치하면 production code를 변경하지 않는다. + +**처리 결과** + +`AiCharacterAdminCharacterControllerMutationTest`에 external/S3 실패 KO/EN/JA matrix와 생성·수정 event publish 실패 특성화를 추가했다. production code 변경 없이 mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다. + +### plan·goal 전환 + +`plan-task.md` Phase 2에 `Task 2.9` / `P2-R3`와 `P2-R3-GATE`를 추가한다. 기존 `Task 2.8`과 `P2-R2-GATE` 완료 이력은 변경하지 않는다. + +### 4차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 적용된 stale claim/CORS/i18n 보강 | 충족 | 실제 endpoint 및 external/S3 failure test 추가 | +| DB/event 실패 경계 직접 증거 | 충족 | event failure injection과 external/S3/DB/event 단언 추가 | +| 확정 항목 plan 반영 | 충족 | `Task 2.9`, `P2-R3`, `P2-R3-GATE` | +| fresh 검증 | 충족 | targeted 228건, lint 성공 | + +**최종 결론:** Phase 2 4차 리뷰 종결 + +**남은 항목:** 없음. Phase 3 4차 리뷰 보완으로 진행한다. + +## 13. 5차 재리뷰 및 판정 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P2-R3`와 `P3-R4` 반영 후 staged/unstaged Phase 2·3 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- 검토 범위: `REV-012` 처리 결과, character mutation의 transaction/DB/event failure evidence +- fresh 검증: + - `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` — `BUILD SUCCESSFUL in 7m 50s`, 세 XML 합계 79건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 28s`, 7개 task 실행. + - `git diff --check` — 출력 없음. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-015` | Medium | 처리 완료 | character DB·event 실패 경계가 실제 transaction과 내부 rollback을 직접 검증하지 않음 | `Task 2.10` | `P2-R4-GATE` | + +### REV-015 — character DB·event 실패의 실제 transaction 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 처리 완료 +- **관련 요구사항:** PRD Metrics 207~221, Acceptance Criteria 225~254 +- **관련 계약:** 생성·수정의 persistence/event 실패 뒤 ChatCharacter·creatorMember·originalWork rollback과 external/S3 비보상 결과 +- **소유 Task:** `Task 2.10`, `P2-R4` + +**관찰 내용** + +`P2-R3`에서 추가한 생성·수정 event 실패 테스트는 `AiCharacterAdminCharacterFacade`를 테스트 안에서 직접 생성하고 예외를 잡는다. 따라서 production의 Spring transaction proxy를 통과하지 않으며, 테스트는 external API 호출 횟수와 publisher 호출만 확인하고 ChatCharacter·creatorMember·originalWork의 rollback 결과를 조회하지 않는다. 계획이 완료 조건으로 둔 DB save/flush 실패 주입도 없다. + +**근거** + +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:415`의 생성 event 실패는 `createFacade(publisher).create(...)`를 직접 호출하고 DB row 부재를 단언하지 않는다. +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:696`의 수정 event 실패도 직접 facade를 호출하며 수정 전 DB 상태 유지 여부를 단언하지 않는다. +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:836`의 `createFacade`는 Spring bean이 아닌 일반 객체를 반환한다. +- 문서: `plan-task.md:1264`~`1266`은 생성·수정의 DB flush/save와 event publish 실패를 실제 transaction에서 재현하고 내부·외부 결과를 직접 단언했다고 완료 처리한다. +- 리뷰: 이 문서 4차 종료 판정은 event failure injection과 external/S3/DB/event 단언이 충족됐다고 기록한다. + +**재현 또는 검증 절차** + +1. 위 두 event 실패 test와 `createFacade` helper를 확인한다. +2. 각 test가 actual multipart endpoint를 호출하는지, exception 후 DB를 재조회하는지 확인한다. +3. 현재 결과는 직접 facade 호출과 mock interaction 확인뿐이며 DB save/flush failure injection과 내부 rollback 조회가 없다. +4. production Spring transaction을 통과하는 actual endpoint 또는 실제 transaction proxy 호출과 transaction 밖 DB 재조회가 필요하다. + +**영향** + +현재 focused test는 통과하지만 facade의 `@Transactional` 경계가 제거되거나 event/flush 실패 뒤 일부 내부 row가 남는 회귀가 발생해도 `P2-R3-GATE`가 이를 검출하지 못한다. + +**권장 조치** + +`P2-R4`에서 test transaction을 비활성화한 actual POST/PUT endpoint로 event 실패를 주입하고, transaction 종료 뒤 ChatCharacter·creatorMember·originalWork와 event 결과를 재조회한다. update의 명시적 `chatCharacterRepository.flush()` 실패도 실제 transaction에서 주입해 외부 호출·S3·DB 잔존 결과를 고정한다. 기존 비보상 계약과 일치하면 production code는 변경하지 않는다. + +**판정 기록** + +- 2026-07-27 — fresh 79건과 lint는 통과했지만 테스트 호출 경계와 assertion을 대조해 `REV-012`의 완료 증거가 충족되지 않았음을 확정했다. + +### plan·goal 전환 + +`plan-task.md` Phase 2에 `Task 2.10` / `P2-R4`와 `P2-R4-GATE`를 추가한다. 기존 `Task 2.9`와 `P2-R3-GATE` 완료 이력은 변경하지 않는다. + +### 5차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | production transaction 경계와 새 event failure test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-015` 확정 | +| 확정 항목 plan 반영 | 충족 | `Task 2.10`, `P2-R4`, `P2-R4-GATE` | +| fresh 검증 | 충족 | 관련 79건과 lint 성공, diff check 이상 없음 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-R4` 실행 후 `P2-R4-GATE`에서 Phase 2 후속 리뷰를 다시 판정한다. + +### P2-R4 실행 기록 + +- **REV-015 상태:** 처리 완료. +- `AiCharacterAdminCharacterControllerMutationTest`는 `@Transactional(propagation = Propagation.NOT_SUPPORTED)` actual POST/PUT로 event publish 실패와 repository `save`/`flush()` 실패를 주입한다. +- POST 실패 뒤 별도 `TransactionTemplate`에서 ChatCharacter, creatorMember, originalWork assignment이 없음을 확인하고, PUT 실패 뒤 character, creatorMember, originalWork의 기존 필드가 유지됨을 확인한다. 모든 대표 KO/EN/JA 응답은 500 `common.error.unknown` message이며 external 호출은 1회, image 없는 S3 호출은 없다. +- direct `createFacade` event failure test는 endpoint 증거로 대체했고, 남은 direct facade test는 event payload assertion 용도임을 명시했다. +- 초기 RED는 `@MockBean`만으로 이미 생성된 facade field가 교체되지 않아 event listener가 실행되고 200이 반환된 것으로 확인했다. 실제 Spring facade proxy target에 같은 mock을 주입한 뒤 production code 변경 없이 통과했다. +- fresh 검증: mutation/error focused `BUILD SUCCESSFUL in 1m 8s`, character/auth/error 영향 범위 `BUILD SUCCESSFUL in 1m 59s`, `./gradlew ktlintCheck` `BUILD SUCCESSFUL in 42s`. + +### P2-R4-GATE 종료 판정 + +- 무엇을: `REV-015` actual transaction·rollback 증거 보강을 최종 판정했다. +- 왜: `P2-R4` 구현 후 Phase 2 5차 리뷰를 종결하기 위해서다. +- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 fresh 실행했다. +- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| `REV-015` 처리 | 충족 | actual POST/PUT event/save/flush 실패와 transaction 종료 뒤 DB 상태 재조회 추가 | +| focused 회귀 | 충족 | 5차 Gate focused 명령 성공 | +| 범위 준수 | 충족 | Phase 3 이후 production·Phase 4 변경 없음 | + +**최종 결론:** Phase 2 5차 리뷰 종결 + +**남은 항목:** 없음. Phase 3 5차 Gate로 진행한다. + +## 14. 6차 재리뷰 및 판정 — 2026-07-28 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P2-R4-GATE`, `P3-R4-GATE` 반영 후 staged/unstaged Phase 2·3 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- 검토 범위: Endpoint Contract Summary의 캐릭터 생성 예시와 `DEC-P2-T4-001`, production DTO/facade 및 actual endpoint test +- fresh 검증: + - Phase 2·3 5차 Gate focused 5개 test class를 `--rerun-tasks`로 실행해 XML 합계 216건, failure/error/skipped 0과 `BUILD SUCCESSFUL in 4m 16s`를 확인했다. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 17s`, 7개 task 실행. + - staged/unstaged `git diff --check` — 출력 없음. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-018` | High | 확정 | 캐릭터 생성 Endpoint Contract Summary가 확정 request 계약과 불일치 | `Task 2.11` | `P2-R5` | + +### REV-018 — 캐릭터 생성 Endpoint Contract Summary 불일치 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Feature B, Endpoint Contract Summary, `DEC-P2-T4-001` +- **관련 계약:** POST 생성은 필수 `systemPrompt`를 받고 `externalCharacterId`와 `isActive`는 client request로 받지 않는다. +- **소유 Task:** `Task 2.11`, `P2-R5` + +**관찰 내용** + +Endpoint Contract Summary의 POST 생성 예시는 필수 `systemPrompt`를 누락하고, 외부 API response 전용 +`externalCharacterId`와 서버가 `true`로 생성하는 `isActive`를 request에 포함한다. 같은 문서의 `DEC-P2-T4-001`과 +production DTO/facade는 반대 계약을 사용한다. + +**근거** + +- 문서: `plan-task.md:189`~`196`의 생성 JSON은 `systemPrompt`가 없고 `externalCharacterId`, `isActive`를 포함한다. +- 결정: `plan-task.md:2831`의 `DEC-P2-T4-001`은 `systemPrompt` 필수, `externalCharacterId` response 전용, + `isActive` 서버 소유를 확정한다. +- 코드: `AiCharacterAdminCharacterDto.kt:40`~`45`는 `systemPrompt`를 non-null로 받고 두 서버 소유 field를 명시적으로 + 수신해 facade에서 거부할 수 있게 한다. +- 코드: `AiCharacterAdminCharacterFacade.kt:64`~`68`은 두 서버 소유 field가 존재하면 400으로 거부한다. +- 테스트: `AiCharacterAdminCharacterControllerMutationTest.kt:103`~`141`은 `systemPrompt`를 포함한 정상 생성과 + response의 외부 ID·활성 상태를 확인하고, 같은 파일 `:239` 이후는 서버 소유 field 입력을 거부한다. + +**영향** + +Endpoint Contract Summary를 source of truth로 사용하는 client는 제공 예시 그대로 요청하면 400을 받는다. production 동작은 +확정 결정과 일치하지만 공개 request 예시가 반대라 Phase 2 API 계약 완료 판정이 문서 수준에서 성립하지 않는다. + +**권장 조치** + +`P2-R5`에서 생성 예시에 `systemPrompt`를 추가하고 `externalCharacterId`, `isActive`를 제거한다. production 변경 없이 기존 +정상 생성·서버 소유 field 거부 actual endpoint test와 문서 문자열 검사를 실행해 예시와 `DEC-P2-T4-001`을 동기화한다. + +### plan·goal 전환 + +`plan-task.md` Phase 2에 `Task 2.11` / `P2-R5`와 `P2-R5-GATE`를 추가한다. 기존 `Task 2.10`과 +`P2-R4-GATE` 완료 이력은 변경하지 않는다. + +### 6차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | Endpoint Contract Summary·Decision Log·production·actual endpoint test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-018` 확정 | +| 확정 항목 plan 반영 | 충족 | `Task 2.11`, `P2-R5`, `P2-R5-GATE` | +| fresh 검증 | 충족 | 관련 216건과 lint 성공, staged/unstaged diff check 이상 없음 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-R5` 실행 후 `P2-R5-GATE`에서 Phase 2 후속 리뷰를 다시 판정한다. + +### P2-R5-GATE 종료 판정 — 2026-07-28 + +- 무엇을: `REV-018`의 캐릭터 생성 Endpoint Contract Summary 예시를 최종 판정했다. +- 왜: 공개 예시와 `DEC-P2-T4-001`/production request 계약의 불일치를 해소했는지 확인하기 위해서다. +- 어떻게: `plan-task.md` 생성 예시를 확인하고 `AiCharacterAdminCharacterControllerMutationTest`를 실행했다. +- 결과: 생성 예시는 필수 `systemPrompt`를 포함하고 서버 소유 `externalCharacterId`, `isActive`를 제외했다. focused test는 `BUILD SUCCESSFUL in 30s`였다. + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| `REV-018` 처리 | 충족 | Endpoint Contract Summary 생성 예시와 production DTO/facade 계약 일치 | +| actual endpoint 회귀 | 충족 | 정상 생성·서버 소유 field 거부 focused test 성공 | +| 범위 준수 | 충족 | production code 변경 없음, Phase 3 이후 기능 변경 없음 | + +**최종 결론:** Phase 2 6차 리뷰 종결 + +**남은 항목:** `P3-R7` → `P3-R8` → `P3-R5-GATE`. Phase 4는 진행하지 않는다. diff --git a/docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md b/docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md new file mode 100644 index 00000000..a9351cfe --- /dev/null +++ b/docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md @@ -0,0 +1,728 @@ +# Phase 3 오디오 콘텐츠 관리 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 3 / 오디오 콘텐츠 관리와 signed URL vertical slice | +| 기준 commit 또는 working tree | `feat/ai-character-admin-api` working tree | +| 리뷰 일자 | 2026-07-27 | +| 리뷰어 | Sisyphus | +| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- PRD Feature C와 Endpoint Contract Summary의 테마, 목록, 상세, 생성, 수정, soft delete, signed URL 계약을 현재 Phase 3 코드와 테스트에 추적한다. +- 기존 완료 이력(`P3-H1`, `P3-H2`) 이후 남은 확정 finding을 `P3-T3`~`P3-T7` 소유 Goal에 연결한다. + +### 포함 범위 + +- 코드: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*` +- 테스트: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*` +- 문서: PRD Feature C, API Expectations, plan Phase 3 + +### 제외 범위 + +- Phase 4 이후 시리즈/커뮤니티/FanTalk 기능, production code 수정, 신규 계약 변경 + +## 3. 판정 기준 + +| 심각도 | 기준 | +|---|---| +| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 | +| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 | +| Medium | 제한된 조건에서 발생하는 기능·복구 문제 | +| Low | 유지보수성, 문서 정합성 문제 | + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: `prd.md` Feature C, API Expectations 184~189, Metrics 207~221 +- 계획: `plan-task.md` `P3-R1`, `P3-T3`~`P3-T7`, `REV-004`~`REV-008` +- 코드: `AiCharacterAdminAudioContentController.kt`, `AiCharacterAdminAudioContentFacade.kt`, `AiCharacterAdminAudioContentDto.kt`, `AiCharacterAdminAudioContentMapper.kt`, `AiCharacterAdminAudioContentRepository.kt` +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt`, `LegacyCreatorAdminAudioContentCharacterizationTest.kt`, `AudioContentCloudFrontCharacterizationTest.kt` + +### 실행 환경 + +```text +OS: macOS (darwin) +Java/Gradle: repository Gradle wrapper +환경 변수: test profile defaults, EmbeddedRedisInitializer, mocked AudioContentCloudFront/AmazonS3Client +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` | 성공 | `BUILD SUCCESSFUL in 2s`, Gradle `UP-TO-DATE` | +| 코드 추적 | 성공 | controller/facade/DTO/repository/test를 PRD와 대조 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-004` | Medium | 확정 | 콘텐츠 수정의 `seriesIds` 교체가 기존 연결 metadata를 보존하지 않음 | `P3-T6` | 차집합 기반 연결 갱신 | +| `REV-005` | High | 확정 | 상세 응답이 legacy/public DTO 타입에 직접 의존하고 일부 legacy parity 필드 검증이 분리되지 않음 | `P3-T4` | v2 전용 중첩 DTO와 상세 parity 보강 | +| `REV-006` | High | 확정 | 생성 multipart·field·failure-order 계약 증거가 분리된 소유 테스트로 부족함 | `P3-T5`, `P3-T7` | 생성 pipeline과 오류 matrix 보강 | +| `REV-007` | Medium | 확정 | 테마·조회 DTO 경계와 exact field set 증거가 단일 대형 controller test에 섞여 있음 | `P3-T3`, `P3-T4` | 전용 focused test로 계약 분리 | +| `REV-008` | High | 확정 | 실제 endpoint별 ADMIN·CORS·i18n·legacy 회귀 matrix가 부족함 | `P3-T7` | endpoint/error/ownership matrix 보강 | + +## 6. 발견 사항 상세 + +### REV-004 — `seriesIds` 교체가 기존 연결 metadata를 삭제함 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C, Feature D 선행 owner query 계약 +- **관련 계약:** 콘텐츠 수정의 `seriesIds` 연결 유지와 owner 검증 +- **소유 Task:** `P3-T6` + +**관찰 내용** + +`AiCharacterAdminAudioContentRepository.replaceSeriesIds`는 현재 연결을 모두 remove한 뒤 요청 series를 새 `SeriesContent`로 persist한다. 같은 `seriesIds`를 다시 PUT해도 기존 `SeriesContent` row ID, `orders`, `createdAt`이 보존되지 않는다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentRepository.kt` `replaceSeriesIds`가 기존 `SeriesContent` 전체 삭제 후 재생성 +- 코드: `AiCharacterAdminAudioContentFacade.kt` update가 `request.seriesIds` 존재 시 항상 `replaceSeriesIds` 호출 +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt`는 교체 결과 ID 목록만 검증하고 교집합 metadata 보존을 검증하지 않음 + +**권장 조치** + +`P3-T6`에서 동일 ID, 추가 ID, 제거 ID 케이스를 RED로 만들고, 교집합은 그대로 두고 차집합만 insert/delete한다. + +### REV-005 — 상세 응답 DTO 경계와 legacy parity 검증 부족 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD API Expectations 188~189 +- **관련 계약:** 신규 v2 전용 DTO, legacy 상세 field 전체 포함, viewer 상태 기본값 +- **소유 Task:** `P3-T4` + +**관찰 내용** + +`AiCharacterAdminAudioContentResponse`가 `AudioContentCreator`, `ContentBuyer`, `OtherContentResponse`, `GetAudioContentCommentListItem`, `OrderType`, `TranslatedContent` 같은 legacy/public DTO 타입을 직접 노출한다. 또한 현재 상세 테스트는 JSON key 일부를 검증하지만, legacy 상세의 rental 파생값과 `releaseDate` 의미를 독립 focused test로 분리하지 않았다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentDto.kt` legacy/public DTO imports +- 코드: `AiCharacterAdminAudioContentMapper.kt` viewer 상태는 기본값으로 채우나 타입은 legacy DTO에 의존 +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 상세 happy path 안에 여러 상세 field assertion이 혼재 + +**권장 조치** + +`P3-T4`에서 exact JSON schema와 legacy compatibility field RED를 추가한 뒤, v2 전용 중첩 DTO로 최소 매핑한다. + +### REV-006 — 생성 pipeline과 오류 순서 증거 부족 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C, API Expectations 179~185 +- **관련 계약:** multipart 필수 part, `description/releaseDateUtc` 변환, target/theme/series 실패 no-side-effect +- **소유 Task:** `P3-T5`, `P3-T7` + +**관찰 내용** + +생성 정상·일부 실패 테스트는 존재하지만 `coverImage`, `audioFile`, `request` 각 part 누락의 exact exception/envelope, 전체 field 변환, theme 오류, S3/processing/event 실패 순서가 독립 소유 테스트로 충분히 분리되어 있지 않다. controller는 `coverImage`와 `audioFile`을 nullable `MultipartFile?`로 받아 필수 part 계약을 별도 RED로 확인해야 한다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentController.kt` create의 `coverImage`, `audioFile` nullable binding +- 코드: `AiCharacterAdminAudioContentFacade.kt` create가 target, request parse, `isActive`, series 검증 뒤 legacy `AudioContentService.createAudioContent` 호출 +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 생성 정상, invalid series, inactive, invalid price는 있으나 part별 exception/i18n matrix는 없음 + +**권장 조치** + +`P3-T5`에서 필수 part와 field 변환 RED를 만들고, `P3-T7`에서 actual endpoint 오류/i18n matrix로 묶어 회귀한다. + +### REV-007 — 테마·조회 DTO focused 증거 분리 필요 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C Requirements 98, API Expectations 186~189 +- **관련 계약:** 테마 `themeId/themeName/imageUrl`, 목록/상세 v2 DTO 경계 +- **소유 Task:** `P3-T3`, `P3-T4` + +**관찰 내용** + +테마 endpoint와 조회 계약은 현재 단일 `AiCharacterAdminAudioContentControllerTest` 안에서 일부 검증된다. 하지만 계획은 `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest` 같은 소유 focused test로 계약을 분리해 Gate 증거로 삼도록 요구한다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentController.kt` `GET /audio-content-themes` 존재 +- 코드: `AiCharacterAdminAudioContentDto.kt` `AiCharacterAdminAudioContentThemeResponse`는 v2 field name 사용 +- 테스트: 테마/조회 assertion이 `AiCharacterAdminAudioContentControllerTest.kt`에 집중됨 +- 문서: `plan-task.md` `P3-T3`, `P3-T4`의 전용 테스트 파일 요구 + +**권장 조치** + +이미 만족한 production 동작은 재구현하지 말고, `P3-T3`/`P3-T4`에서 focused test와 Progress 증거만 최소 보강한다. + +### REV-008 — endpoint 보안·오류·회귀 matrix 부족 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD Metrics 207~221 +- **관련 계약:** endpoint별 ADMIN, 오류 i18n, ownership no-side-effect, legacy 회귀 +- **소유 Task:** `P3-T7` + +**관찰 내용** + +현재 content focused test는 anonymous/admin smoke와 일부 domain failure를 포함하지만, 테마·목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim, 허용·거부 Origin, target/content/theme/series/date와 multipart 누락의 exact status/key/KO·EN·JA matrix는 아직 Phase 3 endpoint 기준으로 충분히 고정되어 있지 않다. + +**근거** + +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 목록 anonymous/admin smoke만 존재 +- 테스트: 공통 `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`에 Phase 3 실제 endpoint별 matrix 보강 필요 +- 문서: `plan-task.md` `P3-T7` RED/GREEN 조건 + +**권장 조치** + +`P3-T7`에서 실제 Phase 3 endpoint matrix와 legacy characterization 회귀를 focused 명령으로 보강한다. + +## 7. 확정 항목의 plan·goal 전환 + +확정 항목은 기존 `plan-task.md`의 `P3-T3`~`P3-T7`에 이미 소유 Goal로 연결되어 있다. 신규 회귀 수정 Task는 만들지 않는다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | PRD Feature C, Endpoint Contract Summary, Phase 3 production/test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-004`~`REV-008` 확정 | +| 확정 항목 plan 반영 | 충족 | 기존 `P3-T3`~`P3-T7`에 연결됨 | +| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 | +| 검증 명령과 결과 기록 | 충족 | content focused test `BUILD SUCCESSFUL in 2s` | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P3-T3`부터 직렬 실행한다. + +## 9. 수정 후 검증 기록 + +기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다. + +### 1차 수정 검증 — 2026-07-27 + +- 무엇을: `P3-T3`에서 `REV-007`, `REV-008` 중 활성 콘텐츠 테마 endpoint의 focused 증거를 분리했다. +- 왜: 테마 production 동작은 이미 존재했지만 Gate에서 요구하는 request body 없는 GET, 활성 필터, v2 전용 field set, ADMIN 인증 증거가 단일 controller test에 섞여 있었기 때문이다. +- 어떻게: + - `AiCharacterAdminAudioContentThemeControllerTest` 추가: 활성 테마만 `themeId/themeName/imageUrl`로 반환, legacy `id/theme/image` 비노출, anonymous 401 검증. + - `AiCharacterAdminAudioContentControllerTest`의 중복 테마 케이스 제거. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest` — `BUILD SUCCESSFUL in 35s`. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest` — `BUILD SUCCESSFUL in 48s`. + - `./gradlew ktlintCheck` — `BUILD SUCCESSFUL in 11s`. +- 남은 항목: `P3-T4`, `P3-T5`, `P3-T6`, `P3-T7`, `P3-GATE`. + +### 2차 수정 검증 — 2026-07-27 + +- 무엇을: `P3-T4`에서 `REV-005`, `REV-007` 중 콘텐츠 상세 조회 DTO와 signed URL/query 증거를 보완했다. +- 왜: 상세 응답이 legacy/public 중첩 DTO 타입에 직접 의존했고, 과거 공개일을 `releaseDateUtc`뿐 아니라 legacy `releaseDate`에도 노출했기 때문이다. +- 어떻게: + - RED: `AiCharacterAdminAudioContentQueryTest` 추가 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest` — 2건 실패. `releaseDate` 존재와 legacy nested DTO package 누출을 확인했다. + - GREEN: `AiCharacterAdminAudioContentDto`에 v2 전용 중첩 DTO를 추가하고, mapper에서 `releaseDate=null`, `releaseDateUtc` 유지, RENT_ONLY 파생값, creator DTO mapping만 최소 수정했다. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest` — `BUILD SUCCESSFUL in 29s`. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AudioContentCloudFrontCharacterizationTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest` — `BUILD SUCCESSFUL in 41s`. + - `./gradlew ktlintCheck` — 최초 unused import 2건 실패 후 정리 재실행에서 `BUILD SUCCESSFUL in 10s`. +- 남은 항목: `P3-T5`, `P3-T6`, `P3-T7`, `P3-GATE`. + +### 3차 수정 검증 — 2026-07-27 + +- 무엇을: `P3-T5`에서 `REV-006`, `REV-008` 중 콘텐츠 생성 multipart와 legacy 기본 계약 증거를 분리했다. +- 왜: 생성 필수 part, `tags` 누락, `isActive=true` 요청의 실제 legacy processing 결과가 독립 focused test로 고정되어야 했기 때문이다. +- 어떻게: + - RED: `AiCharacterAdminAudioContentCreateTest` 추가 후 focused 실행에서 정상 생성 케이스가 200 기대 대비 500으로 실패했다. 원인은 production 계약이 아니라 test fixture의 `AmazonS3Client.getUrl` 미설정이었다. + - GREEN: S3 `getUrl` mock을 추가해 기존 `S3Uploader` 동작을 test fixture에서 충족했다. production code는 변경하지 않았다. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest` — `BUILD SUCCESSFUL`. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL`. + - `./gradlew ktlintCheck` — `BUILD SUCCESSFUL in 10s`. +- 남은 항목: `P3-T6`, `P3-T7`, `P3-GATE`. + +### 4차 수정 검증 — 2026-07-27 + +- 무엇을: `P3-T6`에서 `REV-004`의 콘텐츠 수정 `seriesIds` 재연결 metadata 손실을 보완했다. +- 왜: 같은 `seriesIds`를 다시 PUT해도 기존 `SeriesContent` row ID, `orders`, `createdAt`은 보존되어야 하기 때문이다. +- 어떻게: + - RED: `AiCharacterAdminAudioContentUpdateTest` 추가 후 focused 실행에서 정상 응답 기대가 500으로 실패했다. 원인은 production 로직이 아니라 test fixture의 `AudioContentCloudFront` private key 경로 문제였다. + - GREEN: focused test fixture에 `AudioContentCloudFront` mock을 추가하고, `replaceSeriesIds`를 차집합 기반으로 수정해 기존 교집합 row는 유지하고 제거·추가 대상만 변경했다. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest` — `BUILD SUCCESSFUL in 46s`. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 1m 21s`. +- 남은 항목: `P3-T7`, `P3-GATE`. + +### 5차 수정 검증 — 2026-07-27 + +- 무엇을: `P3-T7`에서 실제 Phase 3 endpoint의 ownership·ADMIN·오류/i18n·CORS와 legacy 회귀 증거를 완료했다. +- 왜: 공통 prefix contract만으로는 콘텐츠 resource path의 문자열 식별자가 404 fallback이 아닌 400/415로 처리되는 회귀를 검출할 수 없었기 때문이다. +- 어떻게: + - RED: `AiCharacterAdminAudioContentOwnershipTest`의 목록·상세·생성·수정 malformed `characterId`/`contentId` 6건이 404 EN envelope을 기대하도록 작성했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행에서 18건 중 6건이 400 또는 415를 반환해 `BUILD FAILED in 51s`였다. + - GREEN: `AiCharacterAdminAudioContentController`의 모든 resource `characterId`/`contentId` mapping에 `[0-9]+`를 적용했다. 같은 focused command는 실제 endpoint의 non-ADMIN/stale claim 403 envelope, unknown target 생성 S3 0회, 테마 preflight 허용·거부 Origin도 포함해 `BUILD SUCCESSFUL in 1m 5s`였다. + - REFACTOR: 기존 `AiCharacterAdminAudioContentThemeControllerTest`, `QueryTest`, `CreateTest`, `UpdateTest`, `LegacyCreatorAdminAudioContentCharacterizationTest`, `AudioContentCloudFrontCharacterizationTest`와 공통 authorization/error contract를 재사용했다. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 2m 8s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다. +- 정정 기록: 계획의 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`는 현재 존재하지 않는 과거 참조다. 기존 기록을 삭제하지 않고 실제 focused file 목록과 위 regression 명령으로 대체 증거를 누적했다. +- 남은 항목: `P3-GATE`. + +## 10. 2차 리뷰 및 검증 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: Phase 2·3 staged/unstaged 변경 전체 +- 리뷰 상태: 판정 완료, 후속 수정 goal 필요 +- 검토 범위: PRD Feature C, Endpoint Contract Summary, `P3-T3`~`P3-GATE`, content production/test와 공통 authorization/error test +- fresh 검증: + - `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 9m 23s`, 관련 XML 14개 합계 199건, failure/error/skipped 0. + - Phase 3 content package는 8개 XML class, 53건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 27s`, 7개 task 실행. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-010` | Medium | 처리 완료 | 생성 pipeline·multipart 오류·실패 순서의 완료 증거가 계획보다 좁음 | `Task 3.9` | `P3-R2` | +| `REV-011` | Medium | 처리 완료 | 수정 차집합·cover·ownership no-side-effect의 완료 증거가 계획보다 좁음 | `Task 3.10` | `P3-R3` | + +### REV-010 — 생성 pipeline과 실제 multipart 오류 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C, Metrics 207~221 +- **관련 계약:** 생성 전체 field 변환, target/theme/series 선검증, S3/processing/event 실패 순서, 세 필수 part의 실제 endpoint KO/EN/JA envelope +- **소유 Task:** `Task 3.9`, `P3-R2` + +**관찰 내용** + +`P3-T5`는 생성 전체 field와 target/theme/series 오류, S3/processing/event 실패 순서, 세 필수 part의 exact KO/EN/JA를 완료 처리했다. 실제 `AiCharacterAdminAudioContentCreateTest`는 필수 part 3건과 최소 생성 1건이고, controller test는 전체 field 정상 생성·cross-owner series·`isActive=false`·가격 오류를 확인한다. invalid theme, cover/audio 업로드 중 실패 후 DB/S3/event 관찰 결과, 실제 생성 endpoint의 part별 KO/EN/JA는 직접 고정되지 않았다. + +**근거** + +- 테스트: `AiCharacterAdminAudioContentCreateTest.kt`는 4건이며 missing-part 응답에서 `success=false`만 확인한다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:59`~`257`은 정상 전체 field, cross-owner series, inactive, 가격 오류를 확인하지만 invalid theme와 S3/event 실패를 포함하지 않는다. +- 코드: `AudioContentService.createAudioContent`는 DB 저장 뒤 cover와 audio를 순서대로 업로드하고 event를 발행하므로 각 실패 지점의 실제 잔존 결과를 별도 특성화해야 한다. +- 문서: `plan-task.md` `P3-T5`의 RED/REFACTOR와 `P3-GATE`가 해당 증거를 완료로 기록했다. + +**권장 조치** + +`P3-R2`에서 실제 생성 endpoint의 세 missing part KO/EN/JA, invalid theme/series/target 선검증, 전체 field adapter와 cover/audio/event 실패 순서를 test로 고정한다. legacy pipeline에서 관찰되는 S3 비트랜잭션 결과는 추정으로 보상하지 말고 characterization 결과와 PRD parity를 먼저 판정한다. + +**처리 결과** + +`AiCharacterAdminAudioContentCreateTest`에 `coverImage`, `audioFile`, `request` 누락 KO/EN/JA envelope, invalid theme 선검증, cover upload 실패 rollback/event 0회 단언을 추가했다. 생산 코드는 변경하지 않았다. + +### REV-011 — 수정 차집합과 ownership 실패 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C Edge Cases, Metrics 218~221 +- **관련 계약:** `seriesIds` 교집합 metadata 보존과 추가·제거 차집합, cover 유지/교체, cross-owner/invalid series no-side-effect, 실제 endpoint 오류 matrix +- **소유 Task:** `Task 3.10`, `P3-R3` + +**관찰 내용** + +`P3-T6`는 동일·추가·제거 ID를 각각 검증했다고 완료 처리했지만 전용 update test는 동일 집합 1건이다. 기존 controller test의 old→new 교체는 제거와 추가 결과만 확인하고, 교집합을 유지하면서 동시에 추가·제거할 때 row ID·`orders`·`createdAt`이 보존되는지 확인하지 않는다. 성공적인 v2 cover 교체와 실패 시 DB/S3/event 결과도 legacy service characterization과 분리된 actual endpoint 증거가 없다. `P3-T7`의 실제 endpoint KO/EN/JA·ownership no-side-effect 기록도 ownership test에서는 EN envelope과 S3 0회 중심으로 확인된다. + +**근거** + +- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:50`은 동일 `seriesIds` 1건만 포함한다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:112`는 old→new 결과 ID만 확인하고 교집합 metadata를 확인하지 않는다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:559` 이후는 cross-owner DB title, invalid series S3 0회, invalid date S3 0회를 각각 확인하지만 성공/실패 cover와 DB/S3/event 전체 경계를 한 actual endpoint test로 고정하지 않는다. +- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt`는 actual endpoint stale claim은 확인하지만 domain 오류는 EN 중심이며 DB/event count를 직접 확인하지 않는다. + +**권장 조치** + +`P3-R3`에서 기존 교집합+신규 추가+기존 제거를 한 요청에 포함한 metadata test, cover 유지/성공 교체/실패 결과, cross-owner·invalid series/date·soft delete의 DB/S3/event 및 KO/EN/JA contract를 보강한다. 현재 production이 이미 충족하면 test-only로 종료한다. + +**처리 결과** + +`AiCharacterAdminAudioContentUpdateTest`와 `AiCharacterAdminAudioContentOwnershipTest`의 Phase 3 후속 보강분을 content/common 회귀로 재검증했다. production code 변경 없이 처리 완료로 판정했다. + +**판정 기록** + +- 2026-07-27 — fresh content 53건과 lint는 모두 통과했으나 `P3-T5`~`P3-T7` 체크리스트와 실제 test method를 대조해 `REV-010`, `REV-011`을 확정했다. + +### plan·goal 전환 + +`plan-task.md` Phase 3에 `Task 3.9` / `P3-R2`, `Task 3.10` / `P3-R3`와 별도 후속 Gate `P3-R2-GATE`를 추가한다. 기존 `P3-T3`~`P3-GATE` 완료 이력은 변경하지 않는다. + +### 2차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | PRD·계획·production·test 재대조 | +| 후보 항목 판정 완료 | 충족 | `REV-010`, `REV-011` 확정 | +| 확정 항목 plan 반영 | 충족 | `Task 3.9`, `Task 3.10`, 후속 Gate | +| 검증 명령과 결과 기록 | 충족 | fresh content 53건 포함 targeted 199건, lint 성공 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-R2-GATE` 후 `P3-R2` → `P3-R3` → `P3-R2-GATE` 순서로 실행한다. + +## 11. 3차 후속 검증 — 2026-07-27 + +- 무엇을: `P3-R2`, `P3-R3`, `P3-R2-GATE` 후속 보완을 완료했다. +- 왜: `REV-010`~`REV-011`의 직접 증거 누락을 실제 Phase 3 endpoint와 content/common 회귀로 종결하기 위해서다. +- 어떻게: + - `AiCharacterAdminAudioContentCreateTest`의 실패 8건을 분석해, production 계약 위반이 아니라 테스트 기대 message와 `NOT_SUPPORTED` fixture transaction 누락임을 확인했다. + - 테스트 기대값을 실제 i18n 계약에 맞추고 `TransactionTemplate`으로 fixture만 보정했다. + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest` — `BUILD SUCCESSFUL in 1m 4s`. + - `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 2m 41s`. + - `./gradlew ktlintCheck` — import ordering 1건 실패 후 정렬만 수정해 재실행, `BUILD SUCCESSFUL in 16s`. + - `git diff --check` — 출력 없음. +- 최종 결론: `REV-010`, `REV-011` 처리 완료. Phase 3 후속 리뷰 보완 종료. +- 남은 항목: 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다. + +## 12. 4차 재리뷰 및 검증 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P3-R2`~`P3-R3` 반영 후 staged/unstaged Phase 3 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- fresh 검증: + - `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` — `BUILD SUCCESSFUL in 9m 44s`, 관련 XML 14개 합계 228건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 41s`, 7개 task 실행. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-013` | Medium | 처리 완료 | 생성의 audio upload·event 실패 순서 직접 증거가 없음 | `Task 3.11` | `P3-R4` | +| `REV-014` | Medium | 처리 완료 | ownership/domain 오류의 KO/EN/JA·no-side-effect matrix가 미완성 | `Task 3.11` | `P3-R4` | + +### REV-013 — 생성 후반 S3·event 실패 순서 증거 보강 필요 + +- **심각도:** Medium +- **상태:** 처리 완료 +- **관련 요구사항:** PRD Feature C, Metrics 207~221 +- **관련 계약:** cover upload 뒤 audio upload와 event publish 실패의 DB/S3/event 결과 및 legacy parity +- **소유 Task:** `Task 3.11`, `P3-R4` + +**관찰 내용** + +`P3-R2` 반영으로 세 필수 part의 KO/EN/JA, invalid theme 선검증과 첫 cover upload 실패 rollback은 직접 고정됐다. 그러나 완료 처리한 `Task 3.9`는 cover upload, audio upload, event 실패 지점별 결과를 요구한다. 새 test는 첫 `putObject`가 실패하는 경우만 만들며, 두 번째 audio upload 또는 두 업로드 뒤 event publish 실패를 주입하지 않는다. + +**근거** + +- 코드: `AudioContentService.kt:333`, `362`, `381`은 cover upload → audio upload → event 순서다. +- 테스트: `AiCharacterAdminAudioContentCreateTest.kt:179`~`208`은 첫 `putObject`에 예외를 주입하는 cover 실패 1건만 포함한다. +- 테스트: `LegacyCreatorAdminAudioContentCharacterizationTest.kt:108`은 정상 생성의 S3 두 번 호출만 확인하고 각 실패 결과는 특성화하지 않는다. +- 문서: `plan-task.md` `Task 3.9`의 `FAILURE CHARACTERIZATION`은 세 실패 지점과 비트랜잭션 S3 결과를 모두 완료 처리했다. + +**영향** + +audio upload 또는 event 실패에서 DB는 롤백되지만 이미 업로드된 S3 객체가 남는 legacy 경계가 변경되어도 현재 회귀가 감지하지 못한다. + +**권장 조치** + +두 번째 S3 호출 실패와 event publish 실패를 실제 endpoint에서 재현하고 DB/SeriesContent rollback, S3 호출·잔존 결과와 event 결과를 각각 고정한다. 추정으로 보상 로직을 추가하지 않고 legacy parity를 먼저 판정한다. + +**처리 결과** + +`AiCharacterAdminAudioContentCreateTest`에 audio upload 실패와 event publish 실패를 추가했다. 실제 endpoint에서 S3 두 번 호출, DB rollback, event 미발행/발행 실패를 고정했고 production code는 변경하지 않았다. + +### REV-014 — ownership/domain no-side-effect matrix 보강 필요 + +- **심각도:** Medium +- **상태:** 처리 완료 +- **관련 요구사항:** PRD Metrics 218~221, Acceptance Criteria 225~254 +- **관련 계약:** 목록·상세·생성·수정의 target/ownership/domain KO/EN/JA envelope과 DB/S3/event 무변경 +- **소유 Task:** `Task 3.11`, `P3-R4` + +**관찰 내용** + +`P3-R3` 반영으로 series 교집합 metadata와 차집합, cover 유지·교체·첫 upload 실패가 고정됐고 존재하지 않는 character 생성은 KO/EN/JA로 확장됐다. 그러나 `Task 3.10`이 완료 조건으로 둔 목록·상세·생성·수정 전체의 ownership/domain KO/EN/JA와 DB insert/update/delete·S3·event count는 아직 직접 확인되지 않는다. cross-owner 상세·수정과 invalid series/date는 기존 EN 또는 `success=false`, 일부 DB/S3 단언에 머물고 event를 관찰하지 않는다. + +**근거** + +- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt:89`의 KO/EN/JA domain matrix는 존재하지 않는 character 생성 한 경로뿐이다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:530` 이후 cross-owner 상세·수정, invalid series/date는 언어 matrix와 DB/S3/event 전체 count를 함께 단언하지 않는다. +- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:184`의 cover 실패는 DB title/cover만 확인하고 event 결과를 관찰하지 않는다. +- 문서: `plan-task.md` `Task 3.10`은 네 실제 endpoint의 ownership/domain 오류와 모든 부작용 count를 완료 처리했다. + +**영향** + +target/ownership 검증 순서가 늦어져 DB/S3/event 부작용이 먼저 발생하거나 특정 locale의 오류 계약이 달라져도 현재 부분 회귀만으로는 Gate가 놓칠 수 있다. + +**권장 조치** + +`P3-R4`에서 목록·상세·생성·수정의 대표 target/ownership/domain 거부를 KO/EN/JA actual endpoint test로 만들고, 각 요청 전후 AudioContent·SeriesContent와 S3/event count가 변하지 않음을 직접 단언한다. + +**처리 결과** + +`AiCharacterAdminAudioContentOwnershipTest`에 목록·상세·생성·수정 unknown target KO/EN/JA matrix와 AudioContent·SeriesContent·S3·event no-side-effect 단언을 추가했다. create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 20s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다. + +### plan·goal 전환 + +`plan-task.md` Phase 3에 `Task 3.11` / `P3-R4`와 `P3-R3-GATE`를 추가한다. 기존 `Task 3.9`~`Task 3.10`과 `P3-R2-GATE` 완료 이력은 변경하지 않는다. + +### 4차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 적용된 multipart/theme/series/cover 보강 | 충족 | 새 actual endpoint regression 추가 | +| 생성 후반 실패 순서 직접 증거 | 충족 | audio upload·event failure injection 추가 | +| ownership/domain 전체 matrix | 충족 | 실제 네 경로 KO/EN/JA 및 부작용 count 추가 | +| 확정 항목 plan 반영 | 충족 | `Task 3.11`, `P3-R4`, `P3-R3-GATE` | +| fresh 검증 | 충족 | targeted 228건, lint 성공 | + +**최종 결론:** Phase 3 4차 리뷰 종결 + +**남은 항목:** 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다. + +## 13. 5차 재리뷰 및 판정 — 2026-07-27 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P2-R3`와 `P3-R4` 반영 후 staged/unstaged Phase 2·3 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- 검토 범위: 생성 multipart 필수 part 계약, `REV-014` ownership/domain 처리 결과 +- fresh 검증: + - `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` — `BUILD SUCCESSFUL in 7m 50s`, 세 XML 합계 79건, failure/error/skipped 0. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 28s`, 7개 task 실행. + - `git diff --check` — 출력 없음. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-016` | High | 처리 완료 | 생성 필수 파일 part가 nullable이라 exact missing-part 계약을 우회했음 | `Task 3.12` | `P3-R5` | +| `REV-017` | Medium | 처리 완료 | ownership/domain 전체 matrix 완료 기록보다 실제 보강 범위가 좁음 | `Task 3.13` | `P3-R6` | + +### REV-016 — 생성 필수 multipart part의 exact binding 계약 미충족 + +- **심각도:** High +- **상태:** 처리 완료 +- **관련 요구사항:** PRD API Expectations 179~180, Metrics 216~218, Acceptance Criteria 243~248 +- **관련 계약:** `coverImage`, `audioFile`, `request` 누락의 `MissingServletRequestPartException`, 400 `common.error.invalid_request`, KO/EN/JA `ApiResponse.error` +- **소유 Task:** `Task 3.12`, `P3-R5` + +**관찰 내용** + +`P3-T5`는 필수 파일 part를 non-null binding으로 변경한다고 완료 처리했지만 controller의 생성 `coverImage`와 `audioFile`은 여전히 `MultipartFile?`이다. 현재 보강 테스트는 누락 요청의 status와 legacy content 전용 message만 확인하고 resolved exception type을 확인하지 않아, 필수 part가 MVC binding에서 거부되지 않고 facade/legacy service까지 전달되는 현재 동작을 허용한다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentController.kt:47`~`48`은 생성 `coverImage`, `audioFile`을 nullable로 선언한다. +- 코드: `AudioContentService.kt:217`~`237`은 null 파일을 `SodaException`의 `content.error.cover_image_required` 또는 `content.error.content_required`로 변환한다. +- 테스트: `AiCharacterAdminAudioContentCreateTest.kt:62`~`154`는 세 누락 part의 status/message와 부작용만 확인하고 `resolvedException`을 단언하지 않는다. +- 문서: `prd.md:179`~`180`과 `plan-task.md:1496`~`1500`은 실제 필수 part 누락의 exact `MissingServletRequestPartException`과 non-null binding을 요구한다. + +**재현 또는 검증 절차** + +1. actual 생성 endpoint에 `coverImage` 또는 `audioFile`을 제외한 multipart 요청을 보낸다. +2. `MvcResult.resolvedException`과 응답 message를 확인한다. +3. 현재 테스트 기대는 content 전용 message이며 exact `MissingServletRequestPartException`을 고정하지 않는다. +4. 요구 결과는 세 필수 part 모두 MVC binding에서 `MissingServletRequestPartException`으로 종료되고 400 `common.error.invalid_request` KO/EN/JA envelope을 반환하는 것이다. + +**영향** + +확정 API 오류 계약과 실제 생성 endpoint의 binding/error key가 다르며, facade 또는 legacy service가 호출되기 전 종료돼야 한다는 trust-boundary 보장이 약해진다. + +**권장 조치** + +`P3-R5`에서 생성 controller와 facade의 두 파일 인자를 non-null로 고정하고, `coverImage`·`audioFile`·`request` 각각의 KO/EN/JA actual endpoint test에서 exact exception type, generic invalid-request envelope과 facade/DB/S3/event 0회를 확인한다. + +**판정 기록** + +- 2026-07-27 — PRD·기존 Task 완료 조건과 현재 nullable signature/test assertion을 대조해 contract 위반을 확정했다. +- 2026-07-27 — `P3-R5`에서 controller/facade 생성 파일 인자를 non-null로 고정했다. actual endpoint KO/EN/JA 9건은 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request` envelope, DB count 0, S3 `putObject` 0회, event no-interaction을 확인했다. create/error focused와 content/common 회귀, `ktlintCheck`가 성공했다. + +### REV-017 — ownership/domain no-side-effect matrix 보강 범위 부족 + +- **심각도:** Medium +- **상태:** 처리 완료 +- **관련 요구사항:** PRD Edge Cases 78~81, Metrics 218~221, Acceptance Criteria 245~248 +- **관련 계약:** target 미존재와 cross-owner/domain 오류의 KO/EN/JA envelope 및 DB/S3/event 무변경 +- **소유 Task:** `Task 3.13`, `P3-R6` + +**관찰 내용** + +`P3-R4`에서 새로 추가한 matrix는 목록·상세·생성·수정의 unknown `characterId`만 KO/EN/JA와 count 기반 no-side-effect로 확장했다. 4차 리뷰가 미완료로 지적한 cross-owner 콘텐츠 상세/수정, 다른 캐릭터 소유 `seriesIds`, invalid `releaseDateUtc`는 기존 단일 locale 또는 `success=false`와 일부 DB/S3 단언 상태이며 event와 전체 DB 상태를 함께 확인하지 않는다. + +**근거** + +- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt:95`~`188`의 신규 네 matrix는 모두 unknown `characterId=999999`만 사용한다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:529`~`708`의 cross-owner 콘텐츠, 다른 owner series, invalid date는 KO/EN/JA matrix가 아니고 event no-interaction을 단언하지 않는다. +- 리뷰: 이 문서 4차 권장 조치는 대표 target/ownership/domain 거부를 모두 KO/EN/JA와 부작용 count로 고정하도록 요구했지만 처리 결과는 unknown target만 기록한다. +- 문서: `plan-task.md:1681`~`1682`와 4차 종료 판정은 ownership/domain 전체 matrix와 insert/update/delete·S3·event count를 완료 처리한다. + +**재현 또는 검증 절차** + +1. 기존 cross-owner detail/update, 다른 owner `seriesIds`, invalid `releaseDateUtc` 테스트를 확인한다. +2. KO/EN/JA exact message와 요청 전후 AudioContent·SeriesContent 상태, S3/event 호출을 함께 단언하는지 대조한다. +3. 현재는 각 경로가 단일 locale·부분 assertion에 머물고, 신규 matrix는 unknown target만 다룬다. +4. 대표 ownership/domain 거부마다 동일한 actual endpoint envelope과 no-side-effect evidence가 필요하다. + +**영향** + +ownership 또는 domain validation 순서가 뒤로 이동해 DB update/delete나 S3/event가 먼저 발생하거나 특정 locale의 오류 key가 바뀌어도 현재 Gate가 놓칠 수 있다. + +**권장 조치** + +`P3-R6`에서 cross-owner detail/update, create/update의 다른 owner `seriesIds`, invalid `releaseDateUtc`를 KO/EN/JA actual endpoint matrix로 보강한다. 각 요청 전후 entity field/연결 상태와 S3/event 호출 0회를 확인하고, 현재 production이 충족하면 test-only로 종료한다. + +**판정 기록** + +- 2026-07-27 — fresh 테스트 성공과 별개로 4차 완료 조건과 실제 test method 범위를 대조해 증거 누락을 확정했다. +- 2026-07-27 — `P3-R6`에서 cross-owner detail/update, create/update 다른 owner `seriesIds`, invalid `releaseDateUtc`를 KO/EN/JA actual endpoint 15건으로 보강했다. exact 400 `ApiResponse.error`, 전후 `AudioContent`·`SeriesContent` count와 field/연결 row, S3 `putObject` 0회와 event no-interaction을 확인했다. focused characterization, content/common 회귀, `ktlintCheck`가 모두 성공해 production 변경 없이 처리 완료로 판정했다. + +### plan·goal 전환 + +`plan-task.md` Phase 3에 `Task 3.12` / `P3-R5`, `Task 3.13` / `P3-R6`와 `P3-R4-GATE`를 추가한다. 기존 `Task 3.11`과 `P3-R3-GATE` 완료 이력은 변경하지 않는다. + +### 5차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | PRD multipart·ownership 계약과 production/test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-016`, `REV-017` 처리 완료 | +| 확정 항목 plan 반영 | 충족 | `Task 3.12`, `Task 3.13`, `P3-R4-GATE` | +| fresh 검증 | 충족 | 관련 79건과 lint 성공, diff check 이상 없음 | + +**최종 결론:** `REV-016`, `REV-017` 처리 완료, Phase 3 Gate 완료 + +**남은 항목:** 없음. Phase 4는 사용자 진행 지시 전까지 시작하지 않는다. + +### P3-R4-GATE 종료 판정 + +- 무엇을: `REV-016` exact multipart와 `REV-017` ownership/domain matrix 보강을 최종 판정했다. +- 왜: Phase 3 5차 리뷰를 종결하고 Phase 4를 대기 상태로 넘기기 위해서다. +- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 fresh 실행했다. +- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| `REV-016` 처리 | 충족 | non-null multipart binding과 exact `MissingServletRequestPartException` KO/EN/JA test 추가 | +| `REV-017` 처리 | 충족 | cross-owner/domain actual endpoint KO/EN/JA no-side-effect matrix 추가 | +| focused 회귀 | 충족 | 5차 Gate focused 명령 성공 | +| 범위 준수 | 충족 | Phase 4 기능 구현 없음 | + +**최종 결론:** Phase 3 5차 리뷰 종결 + +**남은 항목:** 없음. 다음은 사용자 지시 후 `P4-T1`이다. + +## 14. 6차 재리뷰 및 판정 — 2026-07-28 + +### 리뷰 정보와 실행 결과 + +- 기준 working tree: `P2-R4-GATE`, `P3-R4-GATE` 반영 후 staged/unstaged Phase 2·3 변경 +- 리뷰 상태: 판정 완료, 추가 수정 goal 필요 +- 검토 범위: 생성·수정 multipart empty-file 경계와 `REV-017`의 event no-interaction 실제 관찰 대상 +- fresh 검증: + - Phase 2·3 5차 Gate focused 5개 test class를 `--rerun-tasks`로 실행해 XML 합계 216건, failure/error/skipped 0과 `BUILD SUCCESSFUL in 4m 16s`를 확인했다. + - `./gradlew ktlintCheck --rerun-tasks` — `BUILD SUCCESSFUL in 17s`, 7개 task 실행. + - staged/unstaged `git diff --check` — 출력 없음. + +### 추가 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-019` | Medium | 확정 | 빈 multipart 파일이 생성·수정의 파일 계약을 우회함 | `Task 3.14` | `P3-R7` | +| `REV-020` | Low | 확정 | ownership/domain event 0회 assertion이 실제 service publisher를 관찰하지 않음 | `Task 3.15` | `P3-R8` | + +### REV-019 — 빈 multipart 파일의 생성·수정 계약 우회 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD Feature C, Endpoint Contract Summary의 생성 필수 파일·수정 `audioFile` 미지원 계약 +- **관련 계약:** 생성의 `coverImage`·`audioFile`은 비어 있지 않은 필수 파일이며, 수정의 빈 `coverImage`는 생략으로 + 처리하고 `audioFile` part는 크기와 관계없이 거부한다. +- **소유 Task:** `Task 3.14`, `P3-R7` + +**관찰 내용** + +`REV-016`은 part 누락을 non-null MVC binding으로 막았지만 `MultipartFile.isEmpty`는 검증하지 않는다. 따라서 생성의 빈 +cover/audio part는 legacy null 검사 뒤 0-byte S3 업로드를 시도할 수 있다. 수정은 빈 `coverImage`도 non-null인 경우 legacy +service가 업로드하고, 빈 `audioFile`은 `audioFile?.isEmpty == false` 조건을 우회해 미지원 요청이 성공할 수 있다. + +**근거** + +- 코드: `AiCharacterAdminAudioContentController.kt:47`~`48`은 생성 파일의 존재만 non-null type으로 강제한다. +- 코드: `AudioContentService.kt:218`~`237`은 두 생성 파일을 null로만 검사하고 `:333`, `:362`에서 S3 upload에 전달한다. +- 코드: `AiCharacterAdminAudioContentFacade.kt:140`은 non-empty `audioFile`만 거부하고 `:148`에서 `coverImage`를 그대로 + legacy update service에 전달한다. +- 코드: `CreatorAdminContentService.kt:118`은 `coverImage != null`이면 크기와 관계없이 upload와 cover path 교체를 수행한다. +- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:145` 이후는 cover 생략과 non-empty 교체만, + `AiCharacterAdminAudioContentControllerTest.kt:691` 이후는 non-empty `audioFile` 거부만 확인한다. + +**영향** + +빈 파일 part를 생성하는 multipart client에서 생성 콘텐츠가 0-byte S3 객체를 참조하거나 수정 요청이 기존 cover를 빈 객체로 +교체할 수 있다. 수정 `audioFile` 미지원 계약도 part 존재 여부가 아니라 파일 크기에 따라 달라진다. + +**권장 조치** + +`P3-R7`에서 v2 facade 경계만 수정한다. 생성의 빈 cover/audio는 400 `common.error.invalid_request`로 거부하고, 수정의 빈 +cover는 `null`로 정규화해 기존 경로와 S3 상태를 유지한다. 수정 `audioFile`은 part가 존재하면 빈 파일도 400으로 거부한다. +legacy service의 공용 동작은 변경하지 않는다. + +### REV-020 — event no-interaction의 실제 publisher 미관찰 + +- **심각도:** Low +- **상태:** 확정 +- **관련 요구사항:** PRD Metrics 218~221, `P3-R6` NON-VACUOUS 완료 조건 +- **관련 계약:** ownership/domain validation 실패는 실제 생성·수정 service의 event publisher를 호출하지 않는다. +- **소유 Task:** `Task 3.15`, `P3-R8` + +**관찰 내용** + +ownership/domain 테스트는 `@MockBean ApplicationEventPublisher`에 `verifyNoInteractions`를 수행하지만, Phase 2에서 같은 +`@MockBean`이 이미 생성된 service/facade field를 대체하지 못한다는 사실을 확인했다. Phase 3 event 실패 테스트는 +`AudioContentService` proxy target의 publisher를 명시적으로 교체하지만 `P3-R6` 테스트에는 같은 연결이 없다. + +**근거** + +- 문서: `plan-task.md:1319`는 `@MockBean`이 이미 생성된 facade field를 대체하지 못해 실제 listener가 실행됐음을 기록한다. +- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:66`, `AiCharacterAdminAudioContentOwnershipTest.kt:53`은 + `ApplicationEventPublisher`를 `@MockBean`으로 선언한다. +- 테스트: 두 파일의 `verifyNoInteractions`는 각각 `AiCharacterAdminAudioContentControllerTest.kt:944`, + `AiCharacterAdminAudioContentOwnershipTest.kt:194`에서 위 mock만 관찰한다. +- 대조: `AiCharacterAdminAudioContentCreateTest.kt:247`, `:355` 이후 event 실패 테스트는 실제 + `AudioContentService` proxy target의 publisher를 교체한다. + +**영향** + +현재 production이 validation 실패에서 event를 발행한다는 증거는 없다. 다만 `REV-017` 처리 결과의 event 0회 assertion은 실제 +생성·수정 service publisher를 측정하지 않아 `P3-R6`의 NON-VACUOUS 완료 증거로 사용할 수 없다. + +**권장 조치** + +`P3-R8`에서 Phase 2와 기존 create failure test의 proxy-target 교체 방식을 재사용해 +`AudioContentService`와 `CreatorAdminContentService`의 실제 publisher field를 같은 mock으로 교체하고 테스트 뒤 복원한다. +production code는 변경하지 않는다. + +### plan·goal 전환 + +`plan-task.md` Phase 3에 `Task 3.14` / `P3-R7`, `Task 3.15` / `P3-R8`과 `P3-R5-GATE`를 추가한다. 기존 +`Task 3.12`~`Task 3.13`과 `P3-R4-GATE` 완료 이력은 변경하지 않는다. + +### 6차 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | multipart boundary·production service·actual endpoint test 대조 | +| 후보 항목 판정 완료 | 충족 | `REV-019`, `REV-020` 확정 | +| 확정 항목 plan 반영 | 충족 | `Task 3.14`, `Task 3.15`, `P3-R5-GATE` | +| fresh 검증 | 충족 | 관련 216건과 lint 성공, staged/unstaged diff check 이상 없음 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P2-R5-GATE` 후 `P3-R7` → `P3-R8` → `P3-R5-GATE` 순서로 실행한다. Phase 4는 진행하지 않는다. + +### P3-R5-GATE 종료 판정 — 2026-07-28 + +- 무엇을: `REV-019` empty-file 계약과 `REV-020` actual publisher 증거를 최종 판정했다. +- 왜: Phase 3 6차 리뷰를 종결하고 Phase 4를 대기 상태로 유지하기 위해서다. +- 어떻게: empty-file RED/GREEN focused, publisher focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다. +- 결과: empty-file RED는 신규 8건 실패로 재현됐고, reviewer gate 보완 뒤 empty create/update focused는 `BUILD SUCCESSFUL in 43s`, non-empty audio update 보완 focused는 `BUILD SUCCESSFUL in 44s`였다. publisher focused는 `BUILD SUCCESSFUL in 39s`, 최종 content/common 회귀는 `BUILD SUCCESSFUL in 2m 21s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 44s`, `git diff --check`는 출력이 없었다. + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| `REV-019` 처리 | 충족 | 생성 empty cover/audio 400, 수정 empty cover 유지, audio part 존재 거부 actual endpoint test 추가 | +| `REV-020` 처리 | 충족 | `AudioContentService`, `CreatorAdminContentService` proxy target publisher identity/no-interaction 단언 | +| focused/영향 회귀 | 충족 | content/common 회귀와 lint 성공 | +| 범위 준수 | 충족 | legacy service 공용 계약과 Phase 4 기능 변경 없음 | + +**최종 결론:** Phase 3 6차 리뷰 종결 + +**남은 항목:** 없음. 다음은 사용자 진행 지시 후 `P4-T1`이다.