docs(ai-character): 관리자 API 계약과 검증 기록을 갱신한다

This commit is contained in:
2026-07-30 00:35:34 +09:00
parent d86a9fb059
commit 5a90fbf98e
11 changed files with 9179 additions and 287 deletions

View File

@@ -9,7 +9,7 @@
| 리뷰 일자 | 2026-07-27 |
| 리뷰어 | Sisyphus |
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
| 리뷰 상태 | 판정 완료 |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
@@ -726,3 +726,622 @@ production code는 변경하지 않는다.
**최종 결론:** Phase 3 6차 리뷰 종결
**남은 항목:** 없음. 다음은 사용자 진행 지시 후 `P4-T1`이다.
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
- 기준 문서: PRD Feature C, `plan-task.md`, `api-contract.openapi.json`
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
- 검증 방식: production/test 전체 호출 검색과 repository/DTO 정적 추적을 수행했다. 사용자 요청에 따라 Gradle,
컴파일, 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-022` | Low | 처리 완료 | 관리자 content repository에 호출되지 않는 확장 코드 잔존 | `Task 3.19` | `P3-R9` |
### REV-022 — 호출되지 않는 repository 확장과 전용 enum
- **심각도:** Low
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C
- **관련 계약:** 현재 5개 AudioContent operation의 실제 조회/생성/수정 경계
- **소유 Task:** `Task 3.19`, `P3-R9`
**관찰 내용**
현재 facade가 사용하는 관리자 전용 repository method는 owner-scoped 상세 조회
`findByIdAndCreatorMemberId` 하나다. 동일 repository의 page 조회, series ID 조회/교체, 활성 series 검사와 private helper는
production/test 호출자가 없고, `AiCharacterAdminAudioContentStatus`도 이 미사용 코드에서만 참조된다.
**근거**
- 코드: `AiCharacterAdminAudioContentRepository.kt:50`~`56`의 owner-scoped 상세 조회는 실제 facade 호출 대상이다.
- 코드: 같은 파일 `:25`~`:48`, `:58`~`:142`의 나머지 public/private method는 전체 호출 검색 결과 외부 참조가 없다.
- 코드: `AiCharacterAdminAudioContentDto.kt:28`~`31`의 status enum은 위 미사용 page query에서만 참조된다.
**권장 조치**
`P3-R9`에서 실제 사용 중인 상세 조회만 보존하고 호출 0건 method, helper, enum 및 그로 인해 unused가 된 import만 제거한다.
legacy repository나 콘텐츠/series 동작은 변경하지 않고 상세·ownership 회귀로 동작 불변을 확인한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.19` / `P3-R9``P3-R9-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
### 7차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 문서·구현 대조 | 충족 | AudioContent 5개 operation과 facade/repository 호출 정적 추적 |
| 후보 판정 | 충족 | `REV-022` 확정 |
| plan 반영 | 충족 | `Task 3.19`, `P3-R9`, `P3-R9-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R6-GATE``P3-R9`를 실행하고 `P3-R9-GATE`에서 Phase 3을 재판정한다.
### P3-R9-GATE 종료 판정 — 2026-07-28
- 무엇을: `REV-022`의 관리자 오디오 repository 미사용 확장 제거를 최종 판정했다.
- 왜: 실제 facade 호출 대상은 owner-scoped 상세 조회 하나뿐이고, 나머지 page/series helper와 전용 status enum은 현재 5개 AudioContent operation에 쓰이지 않기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentRepository`에서 상세 조회 외 method와 helper를 제거하고 `AiCharacterAdminAudioContentStatus`를 삭제했다. package-scoped 호출 검색, 상세·ownership focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
- 결과: 대상 package 호출 검색은 출력이 없었다. 상세·ownership focused는 `BUILD SUCCESSFUL in 3m 39s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck``BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력 없음이었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-022` 처리 | 충족 | 미사용 repository method/helper/status enum 제거 |
| static 호출 검색 | 충족 | 대상 admin content package에서 제거 대상 호출 0건 |
| owner-scoped 상세 회귀 | 충족 | detail/ownership focused test 성공 |
| 영향 범위 회귀 | 충족 | content/common 회귀, lint, diff check 성공 |
| 범위 준수 | 충족 | legacy repository/service, 콘텐츠·시리즈 동작 변경 없음 |
**최종 결론:** Phase 3 7차 리뷰 종결
**남은 항목:** `P4-R1` 실행 후 `P4-R1-GATE`에서 Phase 4를 재판정한다.
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C·API Expectations, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 생성 request strict parse와 legacy `AudioContentService` 호출, Java time 변환, prefix 예외 handler,
생성 actual endpoint 테스트
- 검증 방식: 요청값 → facade → legacy service → 예외 handler와 side-effect 순서를 정적으로 역추적했다.
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-030` | Medium | 처리 완료 | 잘못된 생성 날짜·시간대가 client 오류가 아닌 500으로 반환됨 | `Task 3.20` | `P3-R10` |
### REV-030 — 생성 날짜·시간대 의미 오류가 500으로 분류됨
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C, API Expectations의 request/domain 오류 400
- **관련 계약:** `AudioContentCreateRequest.releaseDate``yyyy-MM-dd HH:mm`, `timezone`은 시간대 ID
- **소유 Task:** `Task 3.20`, `P3-R10`
**관찰 내용**
facade의 strict reader는 JSON 문법·타입·미지 필드만 검증하고 파싱한 request를 버린다. 이후 legacy service가
`releaseDate``LocalDateTime`으로 변환하고 `ZoneId.of(timezone)`을 호출한다. 잘못된 값은
`DateTimeParseException`/`ZoneRulesException``DateTimeException`으로 빠지며, prefix handler는 이를 client 오류로
분류하지 않아 500 `common.error.unknown`을 반환한다.
**근거**
- 코드: `AiCharacterAdminAudioContentFacade.kt:65`~`:80`은 strict parse 뒤 같은 raw JSON을 legacy service에 전달한다.
- 코드: `AudioContentService.kt:226`~`:230`에서 날짜 형식과 `ZoneId`를 변환한다.
- 코드: `AiCharacterAdminExceptionHandler.kt:55`~`:71``DateTimeException`을 400 분기에 포함하지 않는다.
- 계약: OpenAPI `AudioContentCreateRequest``releaseDate` 설명과 기본 `timezone`.
- 계획: 기존 `Task 3.13`은 invalid date 400 증거를 완료 조건으로 적었지만 실제 테스트는 현재 계약 밖
`releaseDateUtc` 미지 필드만 검증한다.
- 테스트: 현재 생성 테스트에는 유효한 날짜·시간대와 제거된 `releaseDateUtc`만 있고 실제 `releaseDate` 형식·`timezone`
의미 오류가 없다.
**정적 재현 절차**
1. 유효한 `coverImage`, `contentFile`, theme과 필수 JSON field를 준비한다.
2. `releaseDate="not-a-date"` 또는 `timezone="Invalid/Zone"`으로 생성 요청을 보낸다.
3. strict JSON parse는 통과하고 legacy Java time 변환이 예외를 던진다.
4. 현재 handler 분류는 500이며 요구 결과는 side effect 없는 400 `common.error.invalid_request`다.
**영향**
형식상 JSON은 맞지만 의미가 잘못된 client 입력이 서버 장애로 기록·응답된다. 실제 업로드 이전에 실패하므로 현재 경로의
DB/S3/event 변경 가능성은 낮지만, 오류 계약과 운영 장애 지표가 왜곡된다.
**권장 조치**
공통 handler를 넓히지 말고 facade가 strict parse한 생성 DTO를 재사용해 날짜 형식과 `ZoneId`만 legacy 호출 전에 검증한다.
두 입력의 KO/EN/JA 400과 DB/S3/event 0건을 actual endpoint로 고정한다.
**판정 기록**
- 2026-07-28 — 코드·OpenAPI·handler·테스트 정적 추적으로 확정. 테스트는 사용자 요청에 따라 미실행.
- 2026-07-28 — `P3-R10`에서 `AiCharacterAdminAudioContentCreateTest``releaseDate="not-a-date"`
`timezone="Invalid/Zone"`의 KO/EN/JA actual endpoint matrix를 추가했다. RED는 신규 6건이 400 기대 assertion에서 실패했고,
facade가 strict parse 결과의 `releaseDate`/`timezone`을 legacy service 호출 전에 Java time API로 검증하도록 수정한 뒤
create focused, create+controller focused, content/common 영향 범위 회귀와 `ktlintCheck`가 모두 성공했다. production 변경은
v2 audio content facade 경계에 한정했고 OpenAPI schema와 legacy `AudioContentService`는 변경하지 않았다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.20` / `P3-R10``P3-R10-GATE`를 추가했다. 기존 완료 Task/Gate는 유지한다.
### 8차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema 대조 | 충족 | AudioContent 5개 mapping과 OpenAPI schema 유지 |
| 후보 판정 | 충족 | `REV-030` 원인·오류 분류·테스트 공백 확인 및 처리 완료 |
| plan 반영 | 충족 | `Task 3.20`, `P3-R10`, `P3-R10-GATE` |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** `REV-030` 처리 완료, Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2` 실행 후 Phase 4를 재판정한다.
### P3-R10-GATE 종료 판정 — 2026-07-28
- 무엇을: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 400 `common.error.invalid_request`로 복구했다.
- 왜: 형식상 JSON은 유효하지만 의미가 잘못된 client 입력이 legacy Java time 변환까지 내려가 500으로 반환되는 계약 위반을 막기 위해서다.
- 어떻게: v2 facade에서 strict parse 결과를 재사용해 `yyyy-MM-dd HH:mm``ZoneId`를 legacy service 호출 전에 검증했다.
- 결과: RED는 신규 6건 실패로 재현됐고, GREEN 후 create focused는 `BUILD SUCCESSFUL in 1m 4s`, create+controller focused는
`BUILD SUCCESSFUL in 1m 16s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`
`BUILD SUCCESSFUL in 38s`였다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-030` 처리 | 충족 | invalid `releaseDate`/`timezone` actual endpoint KO/EN/JA 400 추가 |
| side-effect 차단 | 충족 | DB count, S3 putObject 0회, event no-interaction 단언 |
| 영향 범위 회귀 | 충족 | content/common 회귀와 lint 성공 |
| 범위 준수 | 충족 | OpenAPI schema, legacy service, upload/processing pipeline 변경 없음 |
**최종 결론:** Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2`다.
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 상세 facade/mapper, 레거시 상세 response 파생 규칙, 예약일·locale·signed URL 테스트
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항
#### `REV-036` — High — 미래 예약 콘텐츠 상세의 releaseDate 소실
- OpenAPI 상세 response는 nullable `releaseDate`를 레거시 `GetAudioContentDetailResponse` 필드로 유지한다.
- 레거시 `AudioContentService`는 미래 예약일을 UTC에서 Asia/Seoul로 변환하고
`content.release_date.format`의 KO/EN/JA 형식 문자열을 반환하며, 공개 시각이 지나면 null을 반환한다.
- v2 `AiCharacterAdminAudioContentMapper.toResponse`는 콘텐츠 상태와 locale에 관계없이 `releaseDate = null`
고정한다.
- 현재 상세 테스트의 예약일은 점검일보다 과거라 null 분기만 검증해 미래 분기 누락을 발견하지 못한다.
**영향:** 예약 공개 전 관리자 상세에서 공개 예정 시각이 숨겨지고 레거시 response 의미와 OpenAPI 이관 원칙을 위반한다.
**권장 조치:** 기존 `SodaMessageSource``LangContext`를 사용해 미래 여부, UTC→Asia/Seoul 변환, locale별 포맷을
mapper에 최소 이관하고 미래·과거 KO/EN/JA actual endpoint를 고정한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.21` / `P3-R11``P3-R11-GATE`를 추가했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | route 유지 | AudioContent 5개 mapping과 response 필드는 존재 |
| 미래 예약일 | 수정 필요 | mapper가 `releaseDate`를 무조건 null로 설정 |
| 과거 예약일 | 충족 | null 반환은 레거시 의미와 일치 |
| plan 반영 | 충족 | `Task 3.21`, `P3-R11`, `P3-R11-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** Phase 3 후속 수정 필요
**남은 항목:** `P2-R7-GATE``P3-R11``P3-R11-GATE`.
## 18. 9차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-036`을 처리했다.
- 왜: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null이라 레거시 locale별 공개 예정 시각을 숨겼기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentQueryTest`에 미래 예약일 KO/EN/JA와 과거 null actual endpoint 테스트를 추가했다. focused 실행에서 미래 3개 locale이 null 반환으로 실패했다.
- GREEN: `AiCharacterAdminAudioContentMapper``SodaMessageSource`, `LangContext`를 사용해 레거시 `content.release_date.format`과 UTC→Asia/Seoul 변환을 적용하도록 최소 수정했다.
- 검증: focused query test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
- 결과: `REV-036` 처리 완료. 미래 예약일은 KO/EN/JA 형식 문자열로 반환하고 과거 예약일은 null을 유지한다.
**최종 결론:** Phase 3 9차 리뷰 종결
**남은 항목:** 없음.
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 테마·목록·상세·생성·수정 facade/mapper/repository, signed URL과 최신 예약일 보완
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
AudioContent runtime의 신규 확정 finding은 없다. owner-scoped 상세, 레거시 목록/생성/수정 DTO,
빈 파일 경계, signed URL과 미래·과거 KO/EN/JA 예약일 의미가 유지된다.
`Task 3.21` 헤더가 `[ ]`로 남은 문제는 Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | AudioContent 5개 route와 OpenAPI field·multipart 경계 일치 |
| ownership | 충족 | target owner content만 상세·수정 가능 |
| signed URL/private path | 충족 | 공통 CloudFront 정책 재사용과 private path 비노출 |
| 예약 공개일 | 충족 | 미래 locale별 표시, 과거 null 유지 |
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
**최종 결론:** Phase 3 기능 추가 수정 없음
**남은 항목:** 없음.
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
### 확인된 문제
#### `REV-041` — 오디오 생성 primitive의 required·null 계약 미강제
- **심각도:** High
- **상태:** 처리 완료
- **계약:** OpenAPI `AudioContentCreateRequest``price`를 required non-null integer로 정의하고,
`themeId`, 각 boolean primitive도 nullable로 선언하지 않는다.
- **구현:** `CreateAudioContentRequest`의 해당 값은 Kotlin primitive이며, v2 content facade의 strict reader는
`FAIL_ON_UNKNOWN_PROPERTIES`, `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 활성화한 뒤 원본 JSON을 legacy service로 전달한다.
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 primitive 누락·null은 `0`/`false`로 보정될 수 있다.
- **영향:** 필수 `price` 누락·null이 400 없이 생성으로 이어질 수 있고, `isFullDetailVisible` 같은 필드의 explicit null은
문서·DTO의 생략 기본값과 다른 값으로 처리될 수 있다.
### 보완 계획
| 항목 | 판정 |
|---|---|
| 신규 Task | `Task 3.22` / `P3-R12` |
| 시작 조건 | `P2-R8-GATE` |
| Gate | `P3-R12-GATE` |
| RED | required `price` 누락·null, non-null primitive null과 S3·DB·event 무변경 |
| GREEN | v2 생성 경계의 primitive null/누락 400 변환과 optional 생략 기본값 유지 완료 |
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
### 처리 결과
- `AiCharacterAdminAudioContentCreateTest``price` 누락·null, primitive field explicit null actual POST와 파일 업로드·DB·event no-side-effect 회귀를 추가했다.
- `AiCharacterAdminAudioContentFacade.readRequest()``FAIL_ON_NULL_FOR_PRIMITIVES``FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가해 전역 mapper·레거시 service·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
- RED: 신규 8개 invocation은 보완 전 `status().isBadRequest` 기대에서 실패했다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다.
- GREEN/GATE: 보완 후 focused, content/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
**최종 결론:** Phase 3는 `REV-041` 처리 완료
**다음 Goal:** `P4-R4`.
## 21. 오디오 콘텐츠 댓글 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-045` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글
조회/작성/수정/삭제 5개 operation이 없다.
- 조회는 필수 `timezone``page`, `size`, 레거시 `totalCount/items`를 유지한다.
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 콘텐츠의 row를
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
- 답글 `parentId`는 같은 콘텐츠의 활성 원댓글이어야 한다.
### plan 전환
- 신규 Task: `Task 3.23` / `P3-R13`
- Gate: `P3-R13-GATE`
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 CRUD 구현 필요
**다음 Goal:** `P3-R13`.
## 22. 오디오 콘텐츠 댓글 구현 및 Gate — 2026-07-29
- 무엇을: `REV-045`를 처리했다.
- 왜: 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 5개 operation이 없었기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentCommentTest`에 root/reply 조회, target AI 작성, target 작성 row 수정, owner 범위 row soft delete, 잘못된 parent/timezone/page/size/unknown field/cross-resource 계약 7건을 추가했고 미구현 route의 404/405로 실패했다.
- GREEN: `AiCharacterAdminAudioContentController`에 5개 route를 추가하고, facade에서 target active owner content, 같은 콘텐츠의 활성 root parent, target 작성 수정 권한을 선검증한 뒤 기존 `AudioContentCommentService`를 재사용했다.
- Gate: focused 댓글 테스트, content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 결과: `REV-045` 처리 완료. 삭제는 작성자와 관계없이 target 소유 콘텐츠의 해당 row만 soft delete하고 cascade하지 않으며, 이미 비활성인 row는 200 no-op을 유지한다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 후속 기능 종결
**다음 Goal:** `P4-R5`.
## 23. UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI 2.2.0 AudioContent 10개 operation, `DEC-UTC-DATE-001`
- 검토 범위: 오디오 생성 request, 상세 GET, 댓글·답글 GET의 controller/facade/DTO/mapper/repository
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-050` — High — 오디오 4개 operation의 timezone/UTC 계약 불일치
- 생성은 현재 레거시 `CreateAudioContentRequest`를 typealias로 사용해 `timezone` body와
`yyyy-MM-dd HH:mm` 로컬 `releaseDate`를 받는다.
- 상세 GET은 필수 `timezone` query를 받지만 facade에서 사용하지 않는다. 상세 `releaseDate`는 미래 예약일에
UTC를 Asia/Seoul로 바꾼 locale 문자열이며 현재/과거는 null이다.
- 댓글·답글 GET도 필수 `timezone` query를 받고 레거시 목록 service/repository의 표시 문자열을 반환한다.
- 승인된 최신 계약은 생성 body와 세 GET에서 `timezone`을 제거하고, 생성의 nullable `releaseDate`, 상세의
기존 nullable `releaseDate`, 댓글의 기존 `date`를 ISO-8601 UTC(`Z`)로 사용한다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| route 수 | 유지 | AudioContent 10개 operation 자체는 변경 없음 |
| 생성 request | 처리 완료 | v2 전용 DTO가 `timezone`을 거부하고 UTC `releaseDate`만 내부 경계에 전달 |
| 상세 response | 처리 완료 | query를 제거하고 미래 예약일만 UTC `Z`로 반환 |
| 댓글·답글 response | 처리 완료 | `page`/`size`만 받고 기존 `date`를 UTC `Z`로 mapping |
| legacy/public 격리 | 충족 | 기존 controller/service의 timezone 계약을 유지 |
| OpenAPI 상태 | 처리 완료 | 영향 4개 operation을 `implemented`로 동기화 |
### plan·goal 전환
- 신규 Task: `Task 3.24` / `P3-R14`
- Gate: `P3-R14-GATE`
- 완료 조건: 생성·상세·댓글·답글 actual endpoint UTC exact JSON, 상세 기존 null/노출 조건과 댓글
pagination/ownership 보존, legacy/public 회귀
- 범위 밖: 오디오 목록 날짜, 로컬 시각+timezone 병행 지원, 신규 dependency·DDL
### `P3-R14` / `P3-R14-GATE` 처리 결과
- RED: create/query/comment focused actual endpoint 테스트는 48개 중 9개가 기존 `timezone` 필수와 legacy 날짜 포맷으로
실패했다.
- GREEN: v2 생성 DTO가 `timezone`을 미지 필드로 거부하고 UTC instant를 `LocalDateTime`으로 한 번 변환해 내부 생성
overload로 전달한다. 상세와 root/reply 목록은 기존 null·pagination·ownership·filter 의미를 유지하면서 `releaseDate`
`date``toUtcIso()`로 반환한다.
- Gate: focused는 `BUILD SUCCESSFUL in 2m 13s`, parent 재실행은 `BUILD SUCCESSFUL in 52s`, content/common·legacy 영향
범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck``BUILD SUCCESSFUL in 23s`였다. 당시 OpenAPI는 34개
`implemented`, 2개 `alignment-required`였고, 후속 `P5-R6` 뒤 36개 모두 `implemented`로 통합됐다. `git diff --check`
출력이 없었다.
**최종 결론:** `REV-050` 처리 완료, Phase 3 UTC 계약 정합화 완료
**다음 Goal:** `P5-R6`.
## 24. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation과 공통 `Page`/`Size`
- 검토 범위: audio controller/facade의 query binding·검증, 관련 댓글 actual endpoint 테스트
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### `REV-052` — High — 오디오 댓글·답글 목록의 optional pagination 계약 위반
- OpenAPI의 `Page`, `Size``required: false`이고 각각 기본값 `0`, `20`이다.
- 댓글 목록과 답글 목록 controller의 `page`, `size`에는 `defaultValue`가 없어 두 query를 생략하면 MVC binding
단계에서 400이 된다.
- facade의 `validateCommentQuery()`도 실제 query 이름 집합이 정확히 `page`, `size` 두 개일 때만 허용하므로,
controller 기본값만 추가해도 전체 또는 부분 생략 요청을 거부한다.
- 현재 댓글 테스트는 query 전체 생략을 400으로 기대해 계약 불일치를 회귀로 고정하고 있다.
- 영향은 두 GET의 정상 요청 가용성에 직접 미치므로 High로 판정한다.
### 검증 근거
| 근거 | 확인 내용 |
|---|---|
| OpenAPI | 두 GET이 공통 optional `Page`/`Size`를 참조 |
| controller | 댓글·답글 모두 기본값 없는 `@RequestParam page`, `size` |
| facade | query 이름의 부분집합이 아니라 정확한 집합 일치 요구 |
| test | query 전체 생략 요청을 400으로 기대 |
### plan 전환
- 신규 Task: `Task 3.25` / `P3-R15`
- Gate: `P3-R15-GATE`
- 최소 수정: 두 controller query 기본값과 facade의 미지 query 거부 조건만 정합화
- 완료 조건: 전체·부분 생략 200/default, 범위 오류·미지 query 400, 기존 UTC/ownership/pagination 회귀
- 범위 밖: OpenAPI·legacy/public API·FanTalk query policy 변경
### `P3-R15` / `P3-R15-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCommentTest` 9건 중 댓글·답글 전체 생략 테스트 2건이 `isOk` 기대에서 실패해 `BUILD FAILED in 45s`였다.
- GREEN: 두 controller 목록의 `page`, `size`에 각각 `0`, `20` 기본값을 적용하고, facade는 `page`, `size`의 부분집합만 허용해 미지 query·음수 page·1 미만 size의 기존 400 `ApiResponse.error` 경계를 유지했다.
- Gate: focused는 `BUILD SUCCESSFUL in 40s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck``BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 controller/facade와 actual endpoint 변경을 포함하므로 실행하지 않았다.
**현재 결론:** `REV-052` 처리 완료. Phase 3 완료 판정에는 별도 `REV-056` / `P3-R16` 보완이 남아 있다.
### `REV-056` — High — AudioContent multipart request part의 JSON media type 미강제
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
- 실제 `AiCharacterAdminAudioContentControllerTest`와 update 테스트는 유효 JSON을 `text/plain` request part로
보내 200을 기대하므로 계약 불일치가 실행 테스트에도 고정돼 있다.
- 기존 정상 strict parsing·file/series/UTC 의미를 유지하면서 part-level media type만 415로 차단해야 한다.
### 추가 plan 전환
- 신규 Task: `Task 3.26` / `P3-R16`
- Gate: `P3-R16-GATE`
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
S3/DB/processing/event no-side-effect
**최종 결론:** Phase 3은 `REV-052`, `REV-056` 수정 전 완료 판정 불가
**다음 Goal:** `P3-R15` (`P2-R10-GATE` 완료 후).
### `P3-R16` / `P3-R16-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentControllerTest`의 83건 중 text/plain·Content-Type 누락 415 기대 13건이 기존 200으로 실패해 `BUILD FAILED in 1m 15s`였다.
- GREEN: Character `P2-R10``MultipartHttpServletRequest` header 검사 패턴을 AudioContent POST·PUT controller에만 적용했다. 기존 JSON String strict reader와 facade는 변경하지 않았다.
- Gate: POST·PUT actual endpoint는 KO/EN/JA의 text/plain·Content-Type 누락에 localized `ApiResponse.error` 415와 `Accept: application/json`, DB/S3/event 무변경을 확인했다. JSON 정상 경로, 생성 필수 part 400, UTC/file/series 회귀도 유지했다. focused는 `BUILD SUCCESSFUL in 56s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck``BUILD SUCCESSFUL in 16s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 변경 slice를 포함하므로 실행하지 않았다.
**처리 결과:** `REV-056` 처리 완료.
## 25. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI `AudioContentCreateMultipart`·`AudioContentUpdateMultipart`
- 검토 범위: AudioContent POST·PUT controller의 multipart binding, 파일 교체 거부와 관련 mutation 테스트
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### `REV-061` — Medium — operation별 허용 목록 밖 multipart part를 일반적으로 거부하지 않음
- OpenAPI는 생성에 `contentFile`, `coverImage`, `request`, 수정에 `coverImage`, `request`만 정의하고 두 schema 모두
`additionalProperties: false`다.
- controller는 전체 part 이름을 검사하지 않는다. 수정의 `audioFile`, `contentFile`만 별도 nullable 인자로 받아 facade에서
거부하므로, `unexpected` 같은 다른 이름의 part는 무시된다.
- 생성·수정 모두 정의되지 않은 part를 포함한 요청이 정상 mutation으로 진행될 수 있어 계약 위반을 확정했다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.27` / `P3-R17` |
| Gate | `P3-R17-GATE` |
| RED | 생성·수정 미정의 part와 S3·DB·processing/event 결과 |
| GREEN | 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` exact allow-list |
| 회귀 | 기존 `audioFile`·`contentFile` 수정 거부, 필수/빈 파일, request part 415 |
**처리 결과 (2026-07-29 / P3-R17):**
- AudioContent POST는 multipart part 이름을 `{contentFile, coverImage, request}`로 제한하고, PUT은 `{coverImage, request}`로 제한하도록 controller 경계에 allow-list를 추가했다.
- PUT controller/facade의 `audioFile`, `contentFile` nullable 인자는 제거했고, 기존 파일 교체 거부는 동일한 미정의 part 검증 경계로 통합했다.
- RED에서 생성·수정 `unexpected` part KO/EN/JA 테스트 6개가 기존 정상 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다.
**Gate 결과 (2026-07-29 / P3-R17-GATE):**
- Focused multipart 회귀, Phase 3 content 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 생성·수정 미정의 part 400/no-side-effect, 수정 `audioFile`·`contentFile` 교체 미지원, 정상/필수/빈 파일/request 415 경계가 유지됨을 확인했다.
**최종 결론:** `REV-061` resolved. Phase 3 완료.
**다음 Goal:** `P4-R8`.
## 26. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: OpenAPI AudioContent create/update multipart schema의 operation별 허용 part와
`additionalProperties: false`
- 검토 범위: AudioContent POST·PUT controller의 allow-list와 미정의 part·파일 교체 회귀 테스트
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-066` — Medium — 일반 form-field multipart part가 allow-list 우회
- `AiCharacterAdminAudioContentController.kt:81-88`은 operation별 허용 집합을 받지만 실제 검사는
`fileMap.keys`에 한정한다.
- 기존 create/update 회귀는 각각 `AiCharacterAdminAudioContentCreateTest.kt:251-266`,
`AiCharacterAdminAudioContentUpdateTest.kt:145-159`의 filename이 있는 `MockMultipartFile`만 사용한다.
- filename 없는 일반 form-field `unexpected`는 생성 `{contentFile, coverImage, request}`, 수정
`{coverImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.28` / `P3-R18` |
| Gate | `P3-R18-GATE` |
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·processing·event no-side-effect |
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
| 범위 제한 | 파일 교체 의미·OpenAPI·전역 resolver·legacy/public 변경 없음 |
**최종 결론:** Phase 3 보완 필요 — `REV-066` 확정
**다음 Goal:** `P3-R18` (`P2-R12-GATE` 완료 후).
## 25. 10차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-066`의 AudioContent POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
- 왜: 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 거부되지 않았기 때문이다.
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-066` 처리 완료. Phase 3 후속 Gate 완료.
**남은 항목:** 없음.
## 26. 15차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature D의 기존 creator 동작 재사용, OpenAPI 오디오 생성 preview 필드,
plan의 기존 preview 오류 key 유지 조건
- 검토 범위: `AiCharacterAdminAudioContentFacade.create`, `AudioContentService.createAudioContent` 두 overload,
v2 오디오 생성 actual endpoint 테스트
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 교차 대조했다. 사용자 지시에 따라
컴파일과 테스트는 실행하지 않았다.
### `REV-072` — High — v2 오디오 생성이 기존 preview 시간 검증을 우회
- v2 facade는 strict JSON parse와 UTC `releaseDate` 변환 후 `CreateAudioContentRequest`와 파싱된 날짜를 받는
`AudioContentService.createAudioContent` overload를 호출한다.
- 기존 `previewStartTime`·`previewEndTime`의 쌍, `HH:mm:ss` 형식, 최소 15초 검증 호출은 문자열 request를 받는
legacy overload에만 있다. v2가 호출하는 공유 대상 overload에는 검증 호출이 없다.
- 해당 overload는 검증 없이 DB를 저장하고 cover/audio를 S3에 업로드한 뒤 두 값이 모두 있으면 그대로 metadata에
넣고 event를 발행한다. 따라서 한쪽만 있는 값은 조용히 무시되고, 형식 오류·15초 미만 값은 metadata로 전달될 수 있다.
- v2 테스트에는 정상 `00:00:05`~`00:00:25` 입력만 있고 세 거부 규칙의 actual endpoint 회귀가 없다.
- 기존 creator 검증/parity와 부작용 선검증을 깨뜨리므로 High로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.29` / `P3-R19` |
| Gate | `P3-R19-GATE` |
| RED | 시작만 입력, 형식 오류, 15초 미만의 400 및 DB/S3/event no-side-effect |
| GREEN | 기존 검증 호출을 두 경로가 공유하는 parsed request overload로 이동 |
| 회귀 | KO/EN/JA 기존 오류 key, 정상 preview metadata, legacy/public 생성 |
### `REV-072` 처리 결과 — 2026-07-30
- `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime` 호출을 이동해 legacy
string request 생성과 v2 parsed request 생성이 같은 preview 검증을 정확히 한 번 공유한다.
- v2 actual endpoint에 한쪽만 입력, 형식 오류, 15초 미만 preview의 KO/EN/JA 400 응답과 DB/S3/event no-side-effect를
추가했고, 정상 15초 이상 preview는 audio upload metadata의 `preview_start_time`·`preview_end_time` 보존을 확인했다.
- 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest`
`BUILD SUCCESSFUL in 39s`; Phase 3 content와 legacy AudioContent 영향 범위 회귀 → `BUILD SUCCESSFUL in 1m 22s`;
`./gradlew ktlintCheck``BUILD SUCCESSFUL in 32s`; `git diff --check` → 출력 없음.
**최종 결론:** `REV-072` 처리 완료. Phase 3 완료.
**다음 Goal:** `P7-R11` 통합 재판정 완료.
## 27. 16차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation
- 검토 범위: 테마·목록·상세·생성·수정, signed URL·UTC 날짜, 댓글 CRUD, owner/actor/parent와 multipart 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- AudioContent 10개 operation과 controller mapping, owner-scoped 조회·수정 및 active 콘텐츠 댓글 경계가 일치한다.
- 생성의 UTC `releaseDate`, 필수 파일, strict request, preview 쌍·형식·최소 15초 검증이 공유 service 경계에 유지된다.
- 댓글 작성의 동일 콘텐츠 활성 원댓글, target AI 수정 권한, row-only soft delete와 UTC 응답 계약이 유지된다.
- 신규 확정 finding이 없어 Phase 3 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 3 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.