docs(ai-character): 관리자 API 검증 기록을 보강한다
This commit is contained in:
@@ -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`이다.
|
||||
Reference in New Issue
Block a user