# AI 캐릭터 관리자 API Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** `ADMIN`이 AI 캐릭터용 Member로 로그인하지 않고 `characterId` 기준으로 캐릭터, 콘텐츠·댓글, 시리즈, 커뮤니티·댓글, FanTalk 목록·답변 작성·수정·팬 원글 삭제와 등록용 원작·장르 조회를 안전하게 대리 관리하는 신규 v2 관리자 API를 구현한다. **Architecture:** 신규 외부 경계는 `/api/v2/admin/ai-characters` 하위 controller/facade/application에 둔다. 공통 target resolver가 `characterId -> ChatCharacter.creatorMember`를 해석하고 `CREATOR + AI_CHARACTER` 불변식과 ownership을 먼저 검증한 뒤, 각 domain vertical slice가 기존 entity/repository/S3/CloudFront/event 컴포넌트를 테스트로 고정해 선택적으로 재사용한다. **Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Security, JPA/Hibernate, QueryDSL, MySQL, Gradle Wrapper, JUnit5. | 문서 항목 | 내용 | |---|---| | 상태 | 구현 완료 | | 작성일 | 2026-07-24 | | 요구사항 기준 | `docs/20260724_AI캐릭터_관리자_API/prd.md` | | API 기준 | `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` | | 현재 Phase | 완료 | | 현재 활성 Goal | 완료 | ## 현재 상태 | Phase | 상태 | 완료/전체 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| | 1 | 완료 | 7/7 | 완료 | 10차 정적 리뷰 신규 finding 없음 | | 2 | 완료 | 18/18 | 완료 | 16차 정적 리뷰 신규 finding 없음 | | 3 | 완료 | 29/29 | 완료 | 16차 정적 리뷰 신규 finding 없음 | | 4 | 완료 | 16/16 | 완료 | 10차 정적 리뷰 신규 finding 없음 | | 5 | 완료 | 16/16 | 완료 | 10차 정적 리뷰 신규 finding 없음 | | 6 | 완료 | 10/10 | 완료 | 10차 정적 리뷰 신규 finding 없음 | | 7 | 완료 | 13/13 | 완료 | 10차 통합 정적 리뷰 신규 finding 없음 | - Phase는 결과와 의존성을 묶는 문서 단위다. `create_goal`에는 Task 또는 Phase Gate 하나만 등록한다. - 동시에 하나의 미완료 goal만 운용하고, 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다. - 기존 `[x]` Task와 검증 기록은 당시 완료 이력으로 보존한다. 후속 리뷰에서 발견한 문제는 기존 Task를 다시 열지 않고 새 review/fix Goal로 처리한다. - 각 Goal은 체크박스, focused test, 완료 증거와 Progress 기록이 모두 충족된 뒤에만 `complete`로 갱신한다. - 검증은 focused test와 영향받는 slice/legacy 회귀를 우선한다. 전체 `./gradlew test`는 공통 경계·여러 Phase 영향, targeted 결과만으로 영향 범위를 판단할 수 없는 실패 또는 최종 release 판정에 실제로 필요하다고 기록한 경우에만 실행한다. - 전체 회귀를 생략하면 생략 근거와 대신 실행한 focused/영향 범위 회귀 명령을 Progress와 검증 기록에 남긴다. - 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화나 독립 작업도 불가능할 때만 `blocked`로 갱신한다. --- ## Source of Truth - 요구사항 원본: `.omx/specs/deep-interview-ai-character-admin-api.md` - 2026-07-24 후속 확정 정책: 신규 prefix는 JWT `ROLE_ADMIN` + 현재 DB `Member.role == ADMIN` 이중 인가를 적용하고, stale ADMIN claim은 403으로 거부한다. 신규 prefix의 API application/controller/security filter 오류는 정확한 비2xx status + `ApiResponse.error` + `Accept-Language` 기반 KO/EN/JA message를 반환한다. 이 후속 정책이 원본과 충돌하면 후속 정책을 우선한다. - 2026-07-25 후속 확정 정책: 신규 prefix의 CORS는 현재 코드에 정의된 캐릭터 관리자 frontend Origin `http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net`만 허용한다. 기존 범용 관리자 frontend와 creator frontend Origin은 허용하지 않는다. - 2026-07-25 2차 리뷰 후속 확정 정책: 공유 `/admin/member/login`, `/member/logout`는 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합을 path-specific으로 허용한다. CORS 정책 거부 403 body는 API 오류 envelope 계약에서 제외한다. 신규 prefix의 406은 `common.error.invalid_request`, `MissingPathVariableException`은 500 `common.error.unknown`으로 처리하고, 405 `Allow`와 415 `Accept` 표준 header를 유지한다. - 문서 작성 규칙: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md` - 기존 AI 캐릭터 연결 문서: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/{prd.md,plan-task.md}` - 2026-07-28 후속 확정 정책: 신규 endpoint의 JSON request/response는 레거시 필드명·타입·optional/nullable·기본값과 성공 `data` 형태를 그대로 유지한다. path로 이동한 ID만 body에서 제거한다. FanTalk 답변 작성만 계획의 축약 응답을 유지하고, FanTalk 목록은 공개 v2 `CreatorChannelFanTalkTabResponse` 필드 형태의 관리자 전용 endpoint로 제공한다. 따라서 `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한은 계약 차원에서 폐기하고, 레거시처럼 다른 optional field와 동시 입력을 허용하되 비활성화만 반영한다. - 2026-07-29 후속 기능 확정 정책: 오디오 콘텐츠 댓글 CRUD, 커뮤니티 댓글 CRUD, 팬 작성 FanTalk 원글 삭제, 캐릭터 등록용 원작 검색, 시리즈 등록용 장르 목록을 관리자 전용 endpoint로 추가한다. 캐릭터에 직접 달리는 레거시 댓글 삭제 API는 v2 전환 뒤 사용하지 않으므로 구현하지 않는다. 시리즈 상세 `data`는 목록 `items`의 단일 객체와 동일한 필드·타입으로 정합화한다. - 2026-07-29 UTC 날짜 계약 확정 정책: 신규 관리자 오디오 생성 request의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 GET의 `timezone` query를 제거한다. 생성의 nullable `releaseDate`는 클라이언트가 ISO-8601 UTC(`Z`)로 변환해 보내고, 상세 `releaseDate`와 댓글 `date`는 기존 필드명 및 null/노출 조건을 유지한 ISO-8601 UTC(`Z`)로 반환한다. 기존 로컬 시각+timezone 입력은 병행 지원하지 않으며 legacy/public API 계약은 변경하지 않는다. - 2026-07-29 FanTalk 답변 수정 확정 정책: `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`는 레거시 `PUT /explorer/profile/cheers`에서 path로 이동한 `cheersId`만 제거한다. optional/nullable `content`, `isActive`, 빈 객체 no-op, 비활성 reply 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지하되, target AI가 작성하고 target의 활성 root에 직접 연결된 reply로 한정한다. - 기계 검증 가능한 API 계약 원본: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - 계약 근거와 예외 설명: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` ## Endpoint Contract Summary 신규 API prefix는 `/api/v2/admin/ai-characters`로 한다. 기존 `/admin/*`, `/creator-admin/*`, 공개 `/api/v2/creator-channels/*`의 성공·오류 status/body/message 계약은 변경하지 않는다. 모든 성공 응답은 기존 관례처럼 `ApiResponse.ok(...)` wrapper를 사용한다. API application/controller/security filter 오류는 오류 의미에 맞는 HTTP status와 `ApiResponse.error(...)` wrapper를 사용한다. `characterId`는 target resource endpoint의 외부 대상 식별자다. 캐릭터 목록/검색은 선택된 target이 없어 `characterId`를 받지 않고, 캐릭터 생성은 새 `ChatCharacter`를 만드는 endpoint라 path `characterId`를 받지 않는다. 목록/검색 endpoint의 query와 pagination은 `api-contract.openapi.json`에 명시한 각 레거시 API를 따른다. FanTalk 관리자 목록만 공개 v2 query policy의 `page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 적용한다. 정식 endpoint, request/response schema, 타입, required/optional/nullable, 기본값과 multipart encoding은 `api-contract.openapi.json`만 기준으로 사용한다. 사람이 읽는 레거시 근거와 생성 방법은 `api-contract.md`를 참고한다. | Domain | operation | 구현 상태와 소유 Goal | |---|---:|---| | Character | 5 | 5 route 구현 및 multipart request part media type 정합화 완료 (`P2-R10`) | | AudioContent | 10 | 10 route 구현 및 pagination·multipart part 계약 정합화 완료 (`P3-R15`, `P3-R16`) | | Series | 10 | 10 route 구현 및 multipart part media type 정합화 완료 (`P4-R7`) | | Community | 8 | 8 route 구현 및 JSON·multipart part 계약 정합화 완료 (`P5-R7`~`P5-R9`) | | FanTalk | 4 | 4 route 구현 및 JSON media type·답변 수정 계약 정합화 완료 (`P6-R3`, `P6-R4`) | - 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`, `replyId`만 레거시 request body에서 제거한다. - 그 밖의 JSON 필드명·타입·optional/nullable·기본값과 성공 `data` 형태는 레거시 API를 유지한다. - 레거시 mutation의 성공 `data`는 `null`이고 오디오 콘텐츠 생성만 `CreateAudioContentResponse(contentId)`를 반환한다. - FanTalk 답변 작성만 승인된 축약 응답 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. - FanTalk 답변 수정은 레거시 `PutWriteCheersRequest`의 optional/nullable `content`, `isActive`와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지한다. target AI가 작성하고 path의 활성 root에 직접 연결된 reply만 수정하며 비활성 reply 재활성화와 빈 객체 no-op을 허용한다. - 캐릭터 수정의 `isActive=false`는 다른 optional JSON field와 함께 받을 수 있으며 레거시 의미대로 비활성화만 반영한다. - 커뮤니티 목록은 2026-07-29 사용자 확정에 따라 레거시 직접 배열의 예외로 둔다. `timezone` query 없이 `totalCount`, `page`, `size`, `hasNext`, `items` pagination wrapper를 반환한다. - 오디오 생성 request는 `timezone` 없이 nullable `releaseDate`를 ISO-8601 UTC(`Z`)로 받는다. 오디오 상세는 `timezone` query 없이 기존 nullable `releaseDate`를 UTC로 반환한다. - 오디오 콘텐츠와 커뮤니티 댓글의 조회는 `timezone` 없이 `page`, `size`와 레거시 `totalCount`, `items`를 유지하고 각 `date`를 ISO-8601 UTC(`Z`)로 반환한다. 작성은 target AI 캐릭터 명의로 수행하고 수정은 target AI가 작성한 댓글/답글만 허용한다. 삭제는 target 소유 리소스의 댓글/답글이면 작성자와 무관하게 해당 row만 soft delete한다. - 댓글 생성의 optional `parentId`가 없으면 원댓글, 있으면 같은 리소스의 활성 원댓글에 대한 답글이다. 삭제는 cascade하지 않으며 이미 비활성인 row 삭제는 성공 no-op이다. - 팬 작성 FanTalk 삭제는 target 채널의 활성 root만 soft delete하고 creator reply row는 유지한다. - 캐릭터에 직접 달리는 레거시 댓글 삭제는 v2 미사용 API라 구현 범위에서 제외한다. - 원작 검색은 필수 `searchTerm`과 `OriginalWorkResponse` 직접 배열, 장르 목록은 활성 장르의 `id`, `genre`, `isAdult` 직접 배열을 사용한다. - 시리즈 상세 `data`는 배열 wrapper 없이 목록 `items`의 단일 객체와 동일한 11개 필드를 반환하고 기존 상세 전용 `genre`, `keywords`는 제거한다. - multipart의 `request` part는 `application/json`이고 각 파일 part의 이름과 required 여부는 OpenAPI encoding을 따른다. - 공통 오류는 400/401/403/404/405/406/415/500과 `ApiResponse.error`를 사용한다. 405의 `Allow`, 415의 `Accept`, 미지원 `Accept-Language`의 KO fallback과 Spring CORS 정책 거부 403 예외를 유지한다. - 기존 23개 operation route와 구현된 후속 14개 operation route를 유지한다. OpenAPI 37개 모두 `x-implementation-status`는 `implemented`다. 2026-07-29 6차·7차 정적 리뷰에서 확인한 optional pagination, JSON-only, multipart media type과 operation별 multipart part 이름 보완은 각 소유 Goal에서 완료했으며, `P7-R9`에서 전체 문서 상태를 통합 재판정한다. --- ## 과거 구현 계약 이력 (비규범) 아래 축약 예시는 2026-07-28 레거시 계약 확정 전 Phase 2·3 구현과 계획 변경 이력을 보존하기 위한 자료다. 클라이언트 개발, 신규 구현, 테스트의 계약으로 사용하지 않으며 위 Endpoint Contract Summary와 `api-contract.openapi.json`이 항상 우선한다. ```json { "success": true, "message": null, "data": {} } ``` 공통 API 오류 응답은 `success=false`, 현지화된 `message`, `data=null`, `errorProperty=null`을 포함하고 2xx로 normalize하지 않는다. `Accept-Language: ko|en|ja`에 따라 KO/EN/JA를 반환하며, 없거나 지원하지 않는 언어는 KO로 fallback한다. security filter 단계도 MVC interceptor에 의존하지 않고 header를 직접 해석한다. | 오류 | HTTP status | message key | |---|---:|---| | JWT 없음·잘못됨·만료·폐기 | 401 | `common.error.bad_credentials` | | JWT role 비ADMIN | 403 | `common.error.access_denied` | | JWT ADMIN + 현재 DB role 비ADMIN stale claim | 403 | `common.error.access_denied` | | request binding·target 미존재·creatorMember 누락·role/memberKind 불변식 위반 | 400 | `common.error.invalid_request` | | 신규 prefix 미매핑 경로 | 404 | `common.error.invalid_request` | | 지원하지 않는 HTTP method | 405 | `common.error.invalid_request` | | 지원하지 않는 응답 media type | 406 | `common.error.invalid_request` | | 지원하지 않는 요청 media type | 415 | `common.error.invalid_request` | | `MissingPathVariableException`·예상하지 못한 서버 오류 | 500 | `common.error.unknown` | 405 응답은 표준 `Allow` header를, 415 응답은 표준 `Accept` header를 유지한다. ```json { "success": false, "message": "Invalid request.", "data": null, "errorProperty": null } ``` 신규 prefix는 캐릭터 관리자 Origin `http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net`만 허용한다. 공유 `/admin/member/login`, `/member/logout`는 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합만 path-specific으로 허용하며, 다른 legacy/public 경로의 허용 범위는 변경하지 않는다. 허용되지 않은 Origin, method 또는 header가 Spring CORS 계층에서 403으로 정책 거부되면 handler 진입 전 종료되는 브라우저 보안 경계이므로 그 응답의 body, content type, 현지화 및 `ApiResponse.error` envelope는 외부 계약으로 고정하지 않는다. Phase 2~6에서 추가되는 domain/client/server 오류는 구현 전에 각 Task에서 정확한 비2xx status와 KO/EN/JA message key를 고정하고 같은 envelope를 적용한다. 신규 prefix 전용 오류 처리는 legacy/public endpoint에 적용하지 않는다. #### 캐릭터 목록/검색 `GET /api/v2/admin/ai-characters?search=루나&page=0&size=20` Query parameters: ```json { "search": "루나", "page": 0, "size": 20 } ``` Response `data`: ```json { "totalCount": 1, "page": 0, "size": 20, "hasNext": false, "items": [ { "characterId": 101, "name": "루나", "description": "달빛을 좋아하는 AI 캐릭터", "imageUrl": "https://cdn.example.com/characters/luna.png", "creatorMemberId": 9001, "creatorNickname": "루나", "originalWorkId": 31, "externalCharacterId": "ext-luna-001", "isActive": true, "createdAtUtc": "2026-07-24T00:00:00Z" } ] } ``` #### 캐릭터 상세 `GET /api/v2/admin/ai-characters/{characterId}` Response `data`: ```json { "characterId": 101, "name": "루나", "description": "달빛을 좋아하는 AI 캐릭터", "imageUrl": "https://cdn.example.com/characters/luna.png", "creatorMemberId": 9001, "creatorNickname": "루나", "creatorProfileImageUrl": "https://cdn.example.com/characters/luna.png", "creatorIntroduce": "달빛을 좋아하는 AI 캐릭터", "originalWorkId": 31, "externalCharacterId": "ext-luna-001", "isActive": true, "createdAtUtc": "2026-07-24T00:00:00Z", "updatedAtUtc": "2026-07-24T00:00:00Z" } ``` #### 캐릭터 생성 `POST /api/v2/admin/ai-characters` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "request": { "name": "루나", "systemPrompt": "루나는 달빛을 좋아하는 AI 캐릭터입니다.", "description": "달빛을 좋아하는 AI 캐릭터", "originalWorkId": 31 } } ``` Response `data`: 캐릭터 상세와 동일하다. #### 캐릭터 수정/비활성화 `PUT /api/v2/admin/ai-characters/{characterId}` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "request": { "name": "루나", "description": "수정된 소개", "originalWorkId": 31 } } ``` `externalCharacterId`는 response 전용이며 request에 포함하면 400이다. 일반 수정은 `image`와 함께 보낼 수 있으며 image가 없으면 기존 이미지를 유지한다. soft delete는 `request: {"isActive": false}`만 허용하고 일반 수정 field 또는 image와 혼합하면 400이다. Response `data`: 캐릭터 상세와 동일하다. `isActive=false`는 soft delete 의미다. #### 오디오 콘텐츠 목록/검색 `GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20` Query parameters: ```json { "search": "밤", "status": "OPEN | SCHEDULED", "page": 0, "size": 20 } ``` Response `data`: ```json { "totalCount": 1, "page": 0, "size": 20, "hasNext": false, "items": [ { "contentId": 501, "title": "밤 산책", "coverImageUrl": "https://cdn.example.com/audio/501-cover.png", "audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...", "price": 1000, "isAdult": false, "isActive": true, "releaseDateUtc": "2026-07-25T00:00:00Z", "status": "OPEN" } ] } ``` #### 오디오 콘텐츠 상세 `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` Response `data`: ```json { "contentId": 501, "title": "밤 산책", "description": "조용한 밤 산책 오디오", "coverImageUrl": "https://cdn.example.com/audio/501-cover.png", "audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...", "price": 1000, "isAdult": false, "isActive": true, "releaseDateUtc": "2026-07-25T00:00:00Z", "status": "OPEN", "seriesIds": [701], "createdAtUtc": "2026-07-24T00:00:00Z", "updatedAtUtc": "2026-07-24T00:00:00Z" } ``` #### 오디오 콘텐츠 생성 `POST /api/v2/admin/ai-characters/{characterId}/audio-contents` Content-Type: `multipart/form-data` Form fields: ```json { "coverImage": "File", "audioFile": "File", "request": { "title": "밤 산책", "description": "조용한 밤 산책 오디오", "price": 1000, "isAdult": false, "isActive": true, "themeId": 11, "releaseDateUtc": "2026-07-25T00:00:00Z", "seriesIds": [701] } } ``` Response `data`: 오디오 콘텐츠 상세와 동일하다. #### 오디오 콘텐츠 수정/soft delete `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` Content-Type: `multipart/form-data` Form fields: ```json { "coverImage": "File | optional", "audioFile": "미지원", "request": { "title": "밤 산책 수정", "description": "수정된 설명", "price": 1200, "isAdult": false, "isActive": false, "releaseDateUtc": null, "seriesIds": [701] } } ``` Response `data`: 오디오 콘텐츠 상세와 동일하다. #### 시리즈 목록 `GET /api/v2/admin/ai-characters/{characterId}/series?page=0&size=20` Query parameters: ```json { "page": 0, "size": 20 } ``` Response `data`: ```json { "totalCount": 1, "page": 0, "size": 20, "hasNext": false, "items": [ { "seriesId": 701, "title": "루나의 밤", "introduction": "밤을 주제로 한 시리즈", "coverImageUrl": "https://cdn.example.com/series/701.png", "genreId": 3, "isAdult": false, "state": "OPEN", "isActive": true, "orders": 1 } ] } ``` #### 시리즈 상세 `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}` Response `data`: ```json { "seriesId": 701, "title": "루나의 밤", "introduction": "밤을 주제로 한 시리즈", "coverImageUrl": "https://cdn.example.com/series/701.png", "publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"], "genreId": 3, "keywords": ["밤", "산책"], "isAdult": false, "state": "OPEN", "isActive": true, "writer": "루나", "studio": "소다라이브", "orders": 1 } ``` #### 시리즈 생성 `POST /api/v2/admin/ai-characters/{characterId}/series` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "request": { "title": "루나의 밤", "introduction": "밤을 주제로 한 시리즈", "publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"], "genreId": 3, "keywords": ["밤", "산책"], "isAdult": false, "state": "OPEN", "writer": "루나", "studio": "소다라이브" } } ``` Response `data`: 시리즈 상세와 동일하다. #### 시리즈 수정/soft delete `PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "request": { "title": "루나의 밤 수정", "introduction": "수정된 소개", "publishedDaysOfWeek": ["FRIDAY"], "genreId": 3, "keywords": ["밤"], "isAdult": false, "state": "OPEN", "isActive": false, "writer": "루나", "studio": "소다라이브" } } ``` Response `data`: 시리즈 상세와 동일하다. #### 시리즈 콘텐츠 조회 `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents?search=밤&page=0&size=20` Query parameters: ```json { "search": "밤", "page": 0, "size": 20 } ``` #### 시리즈 콘텐츠 연결 해제 `DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}` Request body: 없음. `RemoveContentToTheSeriesRequest`의 `seriesId`, `contentId`는 path variable로 이동한다. Response `data`: ```json { "totalCount": 1, "page": 0, "size": 20, "hasNext": false, "items": [ { "contentId": 501, "title": "밤 산책", "coverImageUrl": "https://cdn.example.com/audio/501-cover.png", "isAdult": false, "orders": 1 } ] } ``` #### 시리즈 콘텐츠 연결 `POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` Request body: ```json { "contentIdList": [501, 502] } ``` Response `data`: `null` #### 시리즈 콘텐츠 연결 해제 `DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` Request body: ```json { "contentId": 501 } ``` Response `data`: `null` #### 시리즈 순서 변경 `PUT /api/v2/admin/ai-characters/{characterId}/series/orders` Request body: ```json { "seriesIds": [701, 702, 703] } ``` Response `data`: 시리즈 목록과 동일하다. #### 커뮤니티 게시글 목록 `GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20` Query parameters: ```json { "page": 0, "size": 20 } ``` Response `data`: ```json { "totalCount": 1, "page": 0, "size": 20, "hasNext": false, "items": [ { "postId": 801, "content": "오늘의 소식입니다.", "imageUrl": "https://cdn.example.com/community/801.png", "audioSignedUrl": null, "price": 0, "isAdult": false, "isFixed": true, "fixedAtUtc": "2026-07-24T00:00:00Z", "isActive": true, "createdAtUtc": "2026-07-24T00:00:00Z" } ] } ``` #### 커뮤니티 게시글 등록 `POST /api/v2/admin/ai-characters/{characterId}/community-posts` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "audioFile": "File | optional", "request": { "content": "오늘의 소식입니다.", "price": 0, "isAdult": false, "isFixed": false, "isActive": true } } ``` Response `data`: ```json { "postId": 801, "content": "오늘의 소식입니다.", "imageUrl": "https://cdn.example.com/community/801.png", "audioSignedUrl": null, "price": 0, "isAdult": false, "isFixed": false, "fixedAtUtc": null, "isActive": true, "createdAtUtc": "2026-07-24T00:00:00Z", "updatedAtUtc": "2026-07-24T00:00:00Z" } ``` #### 커뮤니티 게시글 수정/고정/soft delete `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` Content-Type: `multipart/form-data` Form fields: ```json { "image": "File | optional", "audioFile": "File | optional", "request": { "content": "수정된 소식입니다.", "price": 0, "isAdult": false, "isFixed": false, "isActive": false } } ``` Response `data`: 커뮤니티 게시글 등록 응답과 동일하다. `isActive=false`이면 `isFixed=false`, `fixedAtUtc=null`이어야 한다. #### FanTalk 답변 작성 `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` Request body: ```json { "content": "응원해줘서 고마워요!" } ``` Response `data`: ```json { "fanTalkId": 901, "replyId": 902, "creatorMemberId": 9001, "content": "응원해줘서 고마워요!", "createdAtUtc": "2026-07-24T00:00:00Z" } ``` --- ## 구현 Phase ### Phase 1: 공통 ADMIN 인증과 AI 캐릭터 target resolver 기반 #### 공통 Task 실행 규칙 - 각 구현 Task는 `RED: 실패 테스트 작성/실패 확인`, `GREEN: 최소 구현/통과 확인`, `REFACTOR: 정리/회귀 확인`을 포함한다. - 테스트 작성이 현실적으로 불가능한 검증 전용 Task는 `TDD 예외 사유`와 `대체 검증 방법`을 Task에 명시한다. - 기존 business method 재사용 전 특성화/회귀 테스트는 신규 v2 use-case RED 테스트와 분리한다. 특성화 테스트는 기존 legacy/creator-admin 구현을 대상으로 먼저 통과해 baseline을 고정하고, 그 결과를 신규 v2 RED 기대값으로 옮긴다. - Phase 2~6의 모든 신규 오류 RED는 정확한 비2xx status, `ApiResponse.error` shape, `Accept-Language` KO/EN/JA message를 함께 검증한다. 새 오류 분기는 status, message key, 3개 언어 message, 테스트가 모두 정해지기 전 완료 처리하지 않는다. #### 목표 모든 신규 API가 공유할 JWT ADMIN + 현재 DB ADMIN 이중 인가, prefix 전용 오류 envelope/i18n, `characterId` 기반 target 해석, `CREATOR + AI_CHARACTER` 불변식, ownership no-side-effect 검증 기반을 만든다. #### 범위와 비범위 - 포함: 신규 v2 admin package 골격, JWT role + 현재 DB role 이중 인가, prefix 전용 security/application 오류 처리, 공통 target resolver/use-case, no-side-effect 테스트 fixture. - 제외: 캐릭터/콘텐츠/시리즈/커뮤니티/FanTalk 실제 domain 기능 구현. #### 선행 Phase 및 의존성 - 선행 Phase 없음. - 기존 `ChatCharacter.creatorMember`, `MemberRole.CREATOR`, `MemberKind.AI_CHARACTER`가 존재해야 한다. #### API endpoint와 request/response contract - 모든 후속 endpoint에 공통 적용한다. - target resource request는 JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`을 모두 만족하는 principal과 path/query/body의 `characterId`를 받는다. 캐릭터 목록/검색과 생성은 Endpoint Contract Summary의 예외를 따른다. - resolver output은 내부 전용 `AiCharacterAdminTarget(characterId, chatCharacter, creatorMember)`로 계획한다. - Phase 1 API 실패 응답은 Endpoint Contract Summary의 400/401/403/404/405/406/415/500과 `ApiResponse.error`/KO·EN·JA 계약을 따르며 domain side effect가 없어야 한다. Spring CORS 정책 거부 403 body는 해당 envelope 계약의 예외다. #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: `ChatCharacterRepository`에 `characterId`로 `creatorMember`를 `LEFT JOIN FETCH`하는 query를 추가해 반환 직후 연관 객체가 초기화되도록 한다. - Service/Application: `AiCharacterAdminTargetResolver` 생성. - Controller/Facade: `kr.co.vividnext.sodalive.v2.api.admin.aicharacter` 하위 신규 controller/facade 패키지 생성. #### DB migration - 없음. 신규 DDL, 신규 migration 파일을 만들지 않는다. #### transaction과 concurrency 고려사항 - target resolver는 read-only transaction으로 동작한다. - 후속 write use-case는 target 검증을 write transaction 시작부에서 먼저 수행한다. - target 검증 실패는 transaction rollback-only가 필요 없는 사전 실패로 끝나야 한다. #### 보안 및 개인정보 위험 - JWT `ROLE_ADMIN`만 신뢰하지 않고 `TokenProvider`가 이미 조회한 `MemberAdapter.member.role`을 신규 prefix 인가에서 함께 확인한다. `TokenProvider`의 전역 authority 계산은 변경하지 않는다. - JWT ADMIN + 현재 DB 비ADMIN stale claim과 `MemberAdapter`가 아닌 principal은 target resolver 실행 전에 403으로 거부한다. - `creatorMember`를 인증 principal로 교체하지 않는다. - `creatorMemberId`를 관리자 입력값으로 신뢰하지 않는다. - Phase 1 resolver의 잘못된 target 요청은 Hibernate 통계로 DB insert/update/delete 0건을 검증한다. resolver는 S3, 외부 API, 이벤트 발행 의존성을 갖지 않으며 Phase 2~6 write slice에서 각 외부 부작용 0건을 별도 검증한다. #### acceptance criteria - JWT ADMIN + 현재 DB ADMIN 요청만 유효한 AI character target을 resolver로 해석할 수 있다. - JWT 없음·잘못됨·만료·폐기는 401, JWT 비ADMIN 또는 현재 DB 비ADMIN은 403, character 미존재·creatorMember 미존재·target role/memberKind 불일치는 400이다. - 위 API 오류는 모두 `ApiResponse.error`와 `Accept-Language`에 따른 KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403 body는 해당 envelope 계약의 예외다. - 405는 `Allow`, 415는 `Accept` header를 유지하고, 지원하지 않는 응답 media type은 406 `common.error.invalid_request`, `MissingPathVariableException`은 500 `common.error.unknown`으로 반환한다. - Phase 1 resolver 실패는 DB insert/update/delete가 0건이다. S3, 외부 API, 이벤트 부작용은 해당 의존성이 처음 도입되는 Phase 2~6 write slice에서 검증한다. #### targeted test - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt` - Integration Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt` - RED: resolver 미구현 상태에서 `shouldResolveAiCharacterCreatorMemberForAdminTarget`, `shouldRejectMissingCharacterWithoutSideEffect`, `shouldRejectHumanCreatorMemberWithoutSideEffect` 테스트를 작성해 실패를 확인한다. - GREEN: resolver와 최소 repository query를 구현해 resolver 테스트를 통과시킨다. - Controller 권한 테스트: `AiCharacterAdminAuthorizationTest`에서 JWT role × 현재 DB role 매트릭스와 stale claim을 고정하고, Phase 2~6 controller test에서 신규 endpoint 전체가 같은 이중 인가를 공유하는지 검증한다. - 오류 계약 테스트: `AiCharacterAdminErrorContractTest`에서 400/401/403/404/405/406/415/500, 405 `Allow`, 415 `Accept`, JWT filter 예외, KO/EN/JA body와 legacy fallback을 검증한다. CORS는 미매핑 fallback뿐 아니라 실제 mapped endpoint와 `/admin/member/login`, `/member/logout`의 허용·거부 Origin/preflight를 검증하고, 정책 거부 403 body에는 envelope를 요구하지 않는다. - Run: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` #### 전체 회귀 테스트 영향 - 기존 endpoint를 건드리지 않아야 한다. - 신규 package 추가로 component scan과 security 설정 충돌이 없어야 한다. #### rollback 전략 - 신규 v2 admin controller/facade/resolver/error/security package와 관련 테스트를 제거한다. - `SecurityConfig.kt`, `WebConfig.kt`, `ExceptionHandlerFilter.kt`, `TokenProvider.kt`, `ChatCharacterRepository.kt`의 Phase 1 변경을 함께 되돌린다. - DB rollback은 없다. #### 권장 commit 경계 - `feat: add ai character admin target resolver` - [x] **Task 1.1: resolver RED 테스트 작성** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt` - RED: 유효 target, missing character, wrong role, wrong memberKind, missing creatorMember, cross-owner fixture를 먼저 작성하고 실패를 확인한다. - GREEN: 구현 전 Task라 production code를 변경하지 않는다. - REFACTOR: fixture 중복만 정리하고 테스트 의미는 약화하지 않는다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` - 검증 기록: 무엇: resolver RED 테스트. 왜: resolver와 `findByIdWithCreatorMember` 미구현을 실제 실패로 고정하기 위해. 어떻게: 위 명령을 실행했다. 결과: `compileTestKotlin`이 두 미구현 항목으로 실패했다. - [x] **Task 1.2: resolver 최소 구현** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt` - RED: Task 1.1 실패 테스트가 같은 실패 이유로 남아 있음을 확인한다. - GREEN: `characterId`로 `ChatCharacter`와 `creatorMember`를 조회하고 strict validation을 적용한다. - REFACTOR: resolver/repository naming과 예외 메시지를 인접 v2 관례에 맞추고 회귀 테스트를 재실행한다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest` - 검증 기록: 무엇: resolver 최소 구현과 repository/ownership 통합 검증. 왜: mock 기반 테스트만으로 실제 조회 동작을 확인할 수 없었기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest`와 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest`를 실행했다. 결과: 두 실행 모두 `BUILD SUCCESSFUL`이었다. - [x] **Task 1.3: ADMIN 권한 controller smoke 테스트** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - RED: 최소 sample controller 또는 Phase 2 첫 controller 기준 anonymous/non-admin 접근 실패와 admin 접근 성공 테스트를 먼저 작성해 실패를 확인한다. - GREEN: 공통 security 설정 또는 controller annotation을 최소 구현해 테스트를 통과시킨다. - REFACTOR: Phase 2~6의 모든 신규 endpoint controller test가 같은 권한 매트릭스를 따르도록 test helper를 정리한다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest` - 검증 기록: 무엇: sample route ADMIN 권한 smoke. 왜: 신규 prefix의 ADMIN rule 적용 전후를 확인하기 위해. 어떻게: 위 명령으로 권한 테스트를 실행했다. 결과: 적용 전 권한 실패를 확인했고, `/api/v2/admin/ai-characters/**` ADMIN rule 적용 후 `BUILD SUCCESSFUL`이었다. - [x] **Task 1.4: JWT claim + 현재 DB ADMIN 이중 인가** - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt` - RED: `UsernamePasswordAuthenticationToken(MemberAdapter(currentMember), token, jwtAuthorities)` fixture로 JWT ADMIN + DB ADMIN 200, JWT 비ADMIN + DB ADMIN 403, JWT ADMIN + DB 비ADMIN stale claim 403, ADMIN authority + 비`MemberAdapter` principal 403을 작성한다. stale claim은 현재 구현에서 200이므로 이 실패를 확인한다. - GREEN: 신규 prefix의 matcher 하나에서 아래 세 조건을 AND로 묶고 `MemberAdapter`, legacy matcher는 변경하지 않는다. `TokenProvider`는 JWT subject 누락/비숫자 값이 500으로 누수되지 않도록 `common.error.bad_credentials`로만 보정하며, 전역 authority 계산과 token 저장소 검증 의미는 변경하지 않는다. ```kotlin .antMatchers("/api/v2/admin/ai-characters/**") .access( "hasRole('ADMIN') and " + "principal instanceof T(kr.co.vividnext.sodalive.member.MemberAdapter) and " + "principal.member.role == T(kr.co.vividnext.sodalive.member.MemberRole).ADMIN" ) ``` - REFACTOR: production `SecurityConfig` matcher의 비확산은 full-context `AiCharacterAdminAuthorizationTest`의 `/phase1-legacy-sample`로 확인한다. 기존 `AdminAgentReadControllerSecurityTest`, `AdminContentControllerSecurityTest`는 각자의 자체 security chain을 사용하는 controller 회귀 증거로 구분하며 production matcher 비확산의 증거로 해석하지 않는다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest` - 검증 기록: 무엇: JWT claim과 현재 DB ADMIN 이중 인가. 왜: stale ADMIN claim과 비`MemberAdapter` principal이 허용되면 안 되기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`를 실행했다. 결과: 6개 중 해당 2개가 403 기대 대비 200으로 실패한 RED를 확인했고, 세 조건을 AND로 적용한 뒤 같은 명령이 `BUILD SUCCESSFUL`이었다. - [x] **Task 1.5: 신규 prefix 오류 envelope/status/i18n 계약** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminApiException.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminErrorResponseWriter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/i18n/Lang.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt` - RED: legacy anonymous/revoked-token/지원하지 않는 method 오류의 현재 status/body를 먼저 통과하는 특성화 baseline으로 고정한다. 신규 prefix의 anonymous·잘못된 JWT·폐기 JWT 401, JWT/DB role 불충족 403, invalid request/target 400, 미매핑 경로 404, 지원하지 않는 method 405, 지원하지 않는 media type 415, 예상하지 못한 controller/JWT filter 오류 500을 KO/EN/JA로 parameterized 검증한다. 확정된 캐릭터 관리자 Origin의 404/405/415와 실제 JWT header를 요청하는 미매핑 경로 preflight에 CORS 응답 header가 적용되는지 확인한다. 허용된 Origin의 API 오류는 `success=false`, localized `message`, JSON content type을 확인하고, 기존 범용 관리자와 creator frontend Origin의 CORS 정책 거부는 403과 CORS 허용 header 부재만 확인한다. 현재 `sendError`, hardcoded KO, 기본 Spring error body 때문에 실패하는 것을 확인한다. - GREEN: `AiCharacterAdminErrorResponseWriter`가 `Lang.fromAcceptLanguage`와 `SodaMessageSource`로 `ApiResponse.error`를 만들고 JSON을 기록하게 한다. `AiCharacterAdminSecurityErrorHandler`는 401 `common.error.bad_credentials`와 403 `common.error.access_denied`를 위 writer에 위임한다. `SecurityConfig`는 신규 prefix matcher에만 이 handler를 선택하고 기존 `JwtAuthenticationEntryPoint`/`JwtAccessDeniedHandler`를 fallback으로 유지한다. `ExceptionHandlerFilter`도 신규 prefix에서 잡은 JWT 예외만 신규 401 handler로 위임하고 legacy branch는 그대로 둔다. - GREEN: target resolver는 400 + `common.error.invalid_request`를 가진 `AiCharacterAdminApiException`을 던진다. URI matcher로 신규 prefix에만 적용되는 `AiCharacterAdminExceptionHandler`는 controller 선택 전 오류까지 처리해 명시적 API 예외, request binding 400, method 405, media type 415, controller `AccessDeniedException` 403, 예상하지 못한 오류 500을 각각 정확한 status와 localized `ApiResponse.error`로 반환한다. 낮은 우선순위의 prefix fallback handler는 미매핑 경로를 404로 반환하고 캐릭터 관리자 Origin 전용 CORS 설정을 적용한다. 단순 `SodaException` 교체나 전역 `SodaExceptionHandler` 변경은 하지 않는다. - GREEN: `ExceptionHandlerFilter`가 잡은 폐기 JWT 등 알려진 인증 실패만 401로 보내고, JWT 처리 중 예상하지 못한 예외는 위 prefix exception handler에 위임해 500 `common.error.unknown`으로 반환한다. - REFACTOR: raw message key 노출, MVC `LangInterceptor` 의존, legacy 오류 응답 변경이 없는지 확인하고 신규/legacy contract 테스트를 함께 재실행한다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` - 검증 기록: 무엇: 신규 prefix 오류 envelope/status/i18n 계약. 왜: 신규 401/403/400/500 응답이 기존 body·message와 달랐기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`를 실행했다. 결과: 최초 15개 중 13개의 status/content type/message 불일치 RED를 확인했고, prefix 기반 handler와 writer 적용 후 44개 invocation이 모두 통과해 `BUILD SUCCESSFUL`이었다. - [x] **Task 1.6: Phase 1 코드 리뷰 후속 보완** - Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md`, `src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt` - RED: 서명과 subject는 유효하지만 `auth` claim이 누락, 공백 또는 빈 분할 항목을 포함한 JWT가 `common.error.bad_credentials`로 거부되지 않는 테스트를 작성하고 실패를 확인한다. - GREEN: `TokenProvider`가 `auth` claim을 authority로 변환하기 전에 문자열 타입, 공백 여부와 각 분할 항목을 검증하고 잘못된 claim은 `common.error.bad_credentials`로 거부하는 최소 구현을 적용한다. - REFACTOR: Phase 1 테스트 전용 controller를 각 테스트 클래스 내부 nested class로 이동하고 `@TestComponent`로 component scan에서 제외한 뒤 해당 테스트 context에만 명시적으로 import한다. 테스트 fixture 범위만 바꾸는 구조 정리이므로 별도 동작 RED 대신 targeted/full-context 회귀와 application context 시작 성공으로 검증한다. - CORS: 현재 코드의 캐릭터 관리자 Origin `http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net`만 허용하고 기존 범용 관리자/creator Origin은 거부하는 정책으로 PRD/plan과 CORS 계약 테스트를 동기화한다. - 2차 리뷰 RED: 신규 prefix의 실제 mapped endpoint와 공유 `/admin/member/login`, `/member/logout`에서 캐릭터 관리자 Origin 요청/preflight가 허용되지 않는 실패를 확인한다. 응답 media type 협상 실패 406, 405 `Allow` header, 415 `Accept` header, `MissingPathVariableException` 500 계약 테스트를 추가해 현재 동작과의 불일치를 확인한다. - 2차 리뷰 GREEN: 신규 prefix는 캐릭터 관리자 Origin만 허용하는 기존 정책을 유지하고, 두 공유 인증 경로에만 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합을 적용한다. `AiCharacterAdminExceptionHandler`는 406을 `common.error.invalid_request`, `MissingPathVariableException`을 500 `common.error.unknown`으로 분류하고 405/415 표준 header를 보존한다. Spring CORS 정책 거부 403 body는 localized `ApiResponse.error` envelope 계약에서 제외한다. - 2차 리뷰 REFACTOR: 실제 mapped endpoint, 두 공유 인증 경로와 미매핑 fallback의 허용·거부 Origin/preflight를 함께 회귀하고, path-specific CORS 확장이 다른 legacy/public 경로로 확산되지 않았는지 확인한다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` - 검증 기록: 무엇: malformed `auth` claim, CORS, HTTP 오류, 테스트 fixture 격리 후속 보완. 왜: claim 누수와 실제 mapped/shared path CORS·405/406/415/500 계약 누락을 해소하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest`를 실행했다. 결과: malformed claim 7개 invocation RED 후 9개가 통과했고, fallback 보완 후 targeted+legacy 136개가 모두 통과해 `BUILD SUCCESSFUL`이었다. 추가 로그인/로그아웃 CORS 보완 뒤 `AiCharacterAdminLoginJwtIntegrationTest`도 `BUILD SUCCESSFUL`이었다. - [x] **Task 1.7: Phase 1 후속 리뷰 전체 반영** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt` - RED: full-context `AiCharacterAdminLoginJwtIntegrationTest`에 허용 Origin의 `PROPFIND` 400/i18n/CORS, raw double slash 400/CORS, 미허용 Origin 403/`Access-Control-Allow-Origin` 부재와 legacy `RequestRejectedException` 유지 계약을 추가하고, 신규 prefix 세 요청이 `RequestRejectedException`으로 실패하며 legacy fallback은 통과하는 production-before RED를 확인한다. - GREEN: `SecurityConfig`가 기존 `AiCharacterAdminSecurityErrorHandler`를 global `RequestRejectedHandler`로 등록하고, handler는 신규 prefix에만 400/CORS 계약을 적용한다. `setUnsafeAllowAnyHttpMethod(true)` 없이 허용된 캐릭터 관리자 Origin에는 CORS header를 포함한 400 `common.error.invalid_request`와 현지화된 `ApiResponse.error`를, 미허용 Origin에는 기존 Spring CORS 정책과 같은 body 계약 없는 403을 반환한다. legacy/public은 `DefaultRequestRejectedHandler`에 위임해 기존 `RequestRejectedException` 동작을 유지한다. Spring 5.3의 비표준 method enum 한계는 CORS 검사 request에만 `GET` wrapper를 사용해 우회하고 실제 firewall method 허용 범위는 확장하지 않는다. - REFACTOR: `AiCharacterAdminErrorContractTest`, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminAccessDeniedErrorContractTest`를 production `@SpringBootTest` + MockMvc + EmbeddedRedis full context로 전환한다. 기존 ErrorContract의 표준 `POST` -> GET-only mapping 405, `Allow: GET`, CORS 계약도 full context에서 회귀한다. `AiCharacterAdminLoginJwtIntegrationTest`는 `MemberTokenRepository.deleteAll()`을 `@AfterEach`에 실행해 Redis token fixture를 cleanup한다. `AiCharacterAdminTargetResolverIntegrationTest`는 repository 조회 직후 `Hibernate.isInitialized(found.creatorMember)`를 단언해 production `LEFT JOIN FETCH`가 실제 회귀 방지에 필요함을 고정한다. Phase 1 production에는 Bean Validation provider를 추가하지 않고, `MethodArgumentNotValidException`은 test-only endpoint에서 의존성 없이 직접 던져 handler 분기를 검증한다. 이 Task의 실행 명령과 결과는 먼저 이 Task 아래에 기록하고, phase/전체 aggregate만 문서 하단 검증 기록에 누적한다. - Verify: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` - 검증 기록(RED): 무엇: 신규 prefix firewall와 legacy fallback 계약. 왜: production firewall 거부가 신규 API 오류 계약 밖으로 탈출하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest`를 실행했다. 결과: 17개 중 신규 prefix 3개가 `RequestRejectedException`으로 실패했고 legacy fallback 테스트는 통과했다. - 검증 기록(GREEN): 무엇: prefix-aware global `RequestRejectedHandler`. 왜: 신규 prefix만 400/i18n/CORS로 변환하고 legacy/public 동작을 보존하기 위해. 어떻게: RED와 동일한 명령을 실행했다. 결과: 17/17, `BUILD SUCCESSFUL`을 확인했다. - 검증 기록(REFACTOR): 무엇: 네 core controller security/error 클래스의 production full-context 계약과 Redis fixture 격리. 왜: slice 설정이 아닌 실제 security/CORS/filter 구성을 검증하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest`를 실행했다. 결과: 115/115, failure/error 0, `BUILD SUCCESSFUL`을 확인했다. - 검증 기록(FETCH JOIN): 무엇: repository 조회 직후 `creatorMember` 초기화의 non-vacuous 회귀 계약. 왜: resolver transaction 내부 접근만으로 fetch join 누락이 가려지는 것을 막기 위해. 어떻게: production query의 `LEFT JOIN FETCH`를 임시로 `LEFT JOIN`으로 바꾸고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest`를 실행했다. 결과: 4개 중 fetch 테스트 1개가 line 66에서 실패해 `BUILD FAILED`(38초)을 확인했다. 즉시 `LEFT JOIN FETCH`를 복원했고, 복원 상태는 하단 최신 canonical 154/154에 포함되어 통과했다. - 검증 기록(직접 400 분기): 무엇: malformed JSON의 `HttpMessageNotReadableException`, test-only endpoint에서 의존성 없이 직접 던진 `MethodArgumentNotValidException`, 실제 multipart 필수 part 누락의 `MissingServletRequestPartException` 각 KO/EN/JA 총 9 invocation. 왜: exact `resolvedException` 타입과 localized 400 envelope를 각 handler 분기에서 직접 고정하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest' --rerun-tasks`를 실행했다. 결과: 98/98, failure/error/skipped 0, `BUILD SUCCESSFUL`을 확인했고 production/build dependency 변경은 없었다. --- ### Phase 2: AI 캐릭터 관리 vertical slice #### 목표 AI 캐릭터 목록/검색/상세/생성/수정/비활성화와 등록용 원작 검색을 신규 ADMIN v2 API로 제공하고 레거시 관리자 동작 parity를 고정한다. #### 범위와 비범위 - 포함: character CRUD API, 등록용 원작 검색, 외부 캐릭터 API 연동, 이미지 저장, 원작 연결, 언어 감지/번역 이벤트, creatorMember 생성/표시 정보 동기화 parity. - 제외: hard delete, cascade delete, 기존 legacy admin endpoint 변경. #### 선행 Phase 및 의존성 - Phase 1 resolver와 ADMIN 권한 기반이 선행되어야 한다. - 기존 `ChatCharacterService`, `ChatCharacterCreatorMemberService`, image/S3/event 관련 컴포넌트 동작을 특성화해야 한다. #### API endpoint와 request/response contract - 정식 schema는 `api-contract.openapi.json`의 Character operation을 따른다. - `GET /api/v2/admin/ai-characters?searchTerm=&page=&size=` -> `ChatCharacterListPageResponse(totalCount, content)` 또는 동일 필드의 검색 response. - `GET /api/v2/admin/ai-characters/{characterId}` -> nested 필드 전체를 포함한 `ChatCharacterDetailResponse`. - `POST /api/v2/admin/ai-characters` multipart 필수 `image`, 필수 `request: ChatCharacterRegisterRequest` -> `data: null`. - `PUT /api/v2/admin/ai-characters/{characterId}` multipart optional `image`, 필수 `request: ChatCharacterUpdateRequest`에서 `id` 제외 -> `data: null`. - `GET /api/v2/admin/ai-characters/original-works/search?searchTerm=` -> soft delete를 제외한 `List`. - update request의 `isActive=false`는 레거시 soft delete 의미다. #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: character 목록/검색용 query adapter 추가 가능. - Service: 신규 `AiCharacterAdminCharacterFacade/ApplicationService`에서 기존 하위 service를 선택적으로 재사용한다. - DTO: 신규 admin v2 전용 request/response DTO 생성. #### DB migration - 없음. #### transaction과 concurrency 고려사항 - 생성/수정은 단일 transaction에서 character, relation, creatorMember 표시 정보 동기화를 완료한다. - 외부 API/S3/event 순서는 기존 레거시 동작 특성화 결과를 따른다. - 중복 이름 검증은 기존 정책을 유지하며 동시 생성 시 DB/서비스 레벨 실패가 부분 저장을 남기지 않아야 한다. #### 보안 및 개인정보 위험 - 목록/상세 응답에 AI creatorMember 로그인 credential, token, private storage path를 노출하지 않는다. - ADMIN 외 접근을 허용하지 않는다. #### acceptance criteria - 목록/검색/상세는 AI 캐릭터 관리자 화면에 필요한 필드를 반환한다. - 생성/수정은 레거시 관리자와 동일한 business side effect를 만든다. - 비활성화는 `isActive=false`이며 row와 연결 Member/콘텐츠를 삭제하지 않는다. #### targeted test - Characterization: `LegacyChatCharacterAdminCharacterizationTest`에서 기존 character admin create/update/soft delete 결과와 외부 API·S3·event failure order, transaction/compensation 계약을 통과 상태로 고정한다. - V2 RED/GREEN: `AiCharacterAdminCharacterControllerTest`, `AiCharacterAdminCharacterServiceTest`. - Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'` #### 전체 회귀 테스트 영향 - 기존 `AdminChatCharacterController` 및 public character 조회 응답이 변하지 않아야 한다. - creatorMember 동기화 기존 테스트가 계속 통과해야 한다. #### rollback 전략 - 신규 character admin v2 route/facade만 제거한다. - 이미 생성/수정된 정상 데이터는 기존 관리자와 같은 domain 데이터라 별도 schema rollback이 없다. #### 권장 commit 경계 - `feat: add ai character admin character slice` - [x] **Task 2.1: 기존 character parity 특성화 baseline 고정** - **Goal 이력 `P2-H1`:** 기존 character admin 동작을 신규 v2 구현의 비교 기준으로 고정했다. - **완료 증거:** 아래 특성화 테스트 RED/GREEN/REFACTOR 기록과 실제 명령 결과. - **범위 밖:** 신규 v2 endpoint 구현. - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt` - CHARACTERIZE: 기존 character admin 구현을 대상으로 중복 이름, 외부 API·S3·event 호출/실패 순서와 transaction/compensation, original work 연결, 언어 감지/번역 이벤트, creatorMember 표시 정보 동기화, `isActive=false` 및 연결 Member/콘텐츠 미삭제 결과를 고정한다. - BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다. - REFACTOR: fixture와 assertion naming만 정리하고 parity baseline은 변경하지 않는다. - Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest` - 검증 기록(RED): 무엇: 기존 character admin baseline 특성화 테스트. 왜: 신규 v2 production code 전에 기존 생성/수정/비활성화/원작/언어 이벤트/creatorMember·콘텐츠 보존 계약을 고정하기 위해. 어떻게: 위 테스트를 추가하고 동일 명령을 실행했다. 결과: 이벤트 baseline 추가 직후 Mockito/S3 stub과 request JSON 누락으로 2회 실패해 RED를 확인했다. - 검증 기록(GREEN): 무엇: Task 2.1 baseline 통과. 왜: 기존 구현이 현재 특성화 계약을 만족하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다. - 검증 기록(REFACTOR): 무엇: Kotlin style과 리뷰 gate. 왜: 테스트-only baseline의 품질과 계획 준수 여부를 확인하기 위해. 어떻게: `./gradlew ktlintCheck`와 spec/code-quality read-only review를 실행했다. 결과: `BUILD SUCCESSFUL`, 두 리뷰 모두 `APPROVED`였다. - [x] **Task 2.2: character controller/facade/DTO 구현** - **Goal 이력 `P2-H2`:** 캐릭터 목록·상세·생성·수정의 최초 v2 API 구현을 제공했다. - **완료 증거:** 아래 controller test와 `ktlintCheck` 기록. 이 이력만으로 Phase 2 Gate 통과를 의미하지 않는다. - **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완. - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/*` - RED: Task 2.1 baseline에서 옮긴 `AiCharacterAdminCharacterServiceTest`와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다. - RED: 현재 신규 dependency 금지 제약에 따라 기존 Spring binding 또는 수동 validation 전략을 우선하고, 실제 DTO의 invalid 요청 통합 계약을 추가한다. Bean Validation provider가 반드시 필요하면 구현 전에 PRD/계획과 dependency 허용 범위를 명시적으로 변경하고 승인을 받는다. - GREEN: endpoint contract summary의 character endpoint를 구현하고 목록/검색 `page/size` 기본값·최소·최대 보정을 적용한다. - REFACTOR: 신규 DTO가 legacy/public DTO를 외부 계약으로 재노출하지 않는지 확인하고 회귀 테스트를 재실행한다. - Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'` - 검증 기록(RED): 무엇: v2 character controller 계약 테스트 7개. 왜: 신규 route가 목록/검색/상세/생성/수정과 ADMIN 경계를 제공하지 않음을 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: 모든 기대 endpoint가 미구현 route의 404를 반환해 7개가 실패했다. - 검증 기록(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`을 확인했다. - [x] **Task 2.3: Phase 2 요구사항·계약·코드 리뷰** **Goal 실행 `P2-R1`:** PRD Feature B와 Endpoint Contract Summary를 Phase 2 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다. - **추적 review ID:** `REV-001`, `REV-002`, `REV-003`, `REV-007`, `REV-008`. - **시작 조건:** `P2-H1`, `P2-H2` 산출물과 검증 기록 존재. - **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint/side-effect 추적표, 모든 후보의 확정·오탐·보류 판정, 후속 Goal 연결과 Progress 기록. - **범위 밖:** 리뷰 도중 production code 수정, Phase 3 이후 기능 검토. - **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다. - **대체 검증 방법:** PRD·계약·production·test를 대조하고 현재 focused test를 실행해 관찰 결과를 리뷰 문서에 기록한다. **Files:** - Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - 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` - Confirm: `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` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - [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으로 판정했다. - [x] **Task 2.4: 캐릭터 목록·검색·상세 보완** **Goal 실행 `P2-T3`:** 캐릭터 조회 API의 검색·pagination·응답·target 계약을 독립적으로 검증하고 확정된 누락을 최소 수정한다. - **추적 review ID:** `REV-007` 중 목록 응답 field set과 DTO 경계. - **시작 조건:** `P2-R1` 완료와 관련 review ID 확정. 확정 finding이 없으면 Decision Log에 `해당 없음` 근거를 남긴다. - **완료 증거:** 조회 전용 RED/GREEN, focused test, DTO 비노출 점검과 Progress 기록. - **범위 밖:** 생성, 수정, 외부 API·S3 mutation. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - 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` - [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` - [x] **Task 2.5: 캐릭터 생성 흐름 보완** **Goal 실행 `P2-T4`:** 캐릭터 생성의 중복 검증, 외부 API, 이미지, 원작, creatorMember와 이벤트 흐름을 parity 기준으로 완결한다. - **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 생성 request·`characterType` 계약. - **시작 조건:** `P2-T3` 완료와 생성 관련 확정 review ID. - **완료 증거:** 정상 생성 및 실패 지점별 RED/GREEN, DB/S3/외부 API/event 결과, focused/legacy test와 Progress 기록. - **범위 밖:** 기존 external character API 계약 변경, 캐릭터 수정·비활성화. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - 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` - [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` - [x] **Task 2.6: 캐릭터 수정·비활성화 흐름 보완** **Goal 실행 `P2-T5`:** 캐릭터 수정과 `isActive=false`가 표시 정보를 동기화하고 연결 Member·콘텐츠를 보존하도록 완결한다. - **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 수정 request·응답·soft-delete parity. - **시작 조건:** `P2-T4` 완료와 수정·비활성화 관련 확정 review ID. - **완료 증거:** 수정·이미지 유지/교체·soft delete RED/GREEN, 보존/no-partial-update 검증과 Progress 기록. - **범위 밖:** hard delete, cascade delete, 복원 API. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - 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` - [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` - [x] **Task 2.7: Phase 2 보안·오류·회귀 보완** **Goal 실행 `P2-T6`:** Phase 2의 모든 endpoint가 공통 ADMIN·오류·CORS 계약을 공유하고 legacy/public 계약을 회귀시키지 않음을 고정한다. - **추적 review ID:** `REV-001`, `REV-007`, `REV-008`. - **시작 조건:** `P2-T5` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정. - **완료 증거:** endpoint 권한 매트릭스, 정확한 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록. - **범위 밖:** Phase 1 공통 security/error 구조 재설계, Phase 3 기능. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.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` - 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` - [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 회귀를 최종 판정한다. - [x] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P2-R1`, `P2-T3`~`P2-T6` 완료 또는 근거 있는 `해당 없음` 판정. - **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록. - **범위 밖:** Gate 통과를 위한 test 삭제·완화, Phase 3 기능 수정. - [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.*' ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck ``` **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 2 7차 리뷰 보완 - [x] **Task 2.12: 캐릭터 mutation의 미사용 응답 매핑 제거** **Goal 실행 `P2-R6`:** `REV-021`의 POST/PUT 성공 응답이 `data: null`인 계약을 유지하면서 controller가 사용하지 않는 facade 응답 생성과 전체 DTO 매핑을 제거한다. - **추적 review ID:** `REV-021`. - **시작 조건:** `P23-CONTRACT-GATE` 완료 이력과 `phase2-character-review.md` 7차 리뷰 판정 존재. - **완료 증거:** create/update facade 반환형을 `Unit`으로 축소하고 불필요한 mapper 호출을 제거한 diff, mutation exact `data: null` 회귀, character/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 캐릭터 생성·수정 business pipeline, 외부 API/S3/event 순서, 공개 API schema 변경, 인접 mapper 정리. - **TDD 예외 사유:** OpenAPI와 기존 actual endpoint test가 이미 `data: null`을 고정한 상태에서 사용되지 않는 내부 계산만 제거하는 동작 불변 리팩터링이다. - **대체 검증 방법:** controller가 facade 반환값을 소비하지 않는지 정적 확인하고 기존 mutation exact JSON 테스트를 focused 회귀한다. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - [x] **REFACTOR:** create/update facade 반환형을 `Unit`으로 바꾸고 mutation 마지막의 미사용 `characterMapper.toResponse(...)`를 제거한다. - [x] **CONTRACT TEST:** POST/PUT actual endpoint가 계속 200과 exact `data: null`을 반환하는지 확인한다. - [x] **회귀 확인:** character package와 공통 authorization/error 회귀 및 `ktlintCheck`를 실행한다. - 검증 기록: 무엇: `REV-021`의 캐릭터 mutation 미사용 response mapping을 제거했다. 왜: POST/PUT 성공 응답은 `data: null`인데 facade가 controller가 버리는 전체 상세 DTO를 생성하고 있었기 때문이다. 어떻게: `create`/`update` 반환형을 `Unit`으로 축소하고 마지막 `characterMapper.toResponse(...)` 호출만 제거했다. 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 54s`, character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 57s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest ./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 git diff --check ``` #### Phase 2 7차 리뷰 Gate **Goal 실행 `P2-R6-GATE`:** `REV-021`의 불필요한 mutation 응답 매핑 제거와 계약 불변 증거를 재검토한다. - [x] **`P2-R6-GATE` 완료:** `P2-R6` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P2-R6` 완료. - **완료 증거:** `REV-021` 처리 완료, POST/PUT `data: null` 계약 유지, focused/영향 범위 회귀와 lint·diff 성공. - **범위 밖:** Gate에서 production code 수정, 기존 Phase 2 완료 이력 변경. - 검증 기록: 무엇: `P2-R6-GATE`에서 `REV-021` 처리 완료와 Phase 2 7차 리뷰 종결을 확인했다. 왜: Phase 3 7차 보완의 시작 조건이 `P2-R6-GATE` 완료이기 때문이다. 어떻게: `phase2-character-review.md` 7차 리뷰 후속 판정을 갱신하고 위 focused/영향 범위 회귀, lint, diff check 결과를 대조했다. 결과: `REV-021`은 처리 완료로 판정했고 Phase 2는 12/12 완료 상태로 동기화했다. #### Phase 2 9차 리뷰 보완 - [x] **Task 2.13: 캐릭터 필수 image와 `isActive=true` 레거시 계약 복구** **Goal 실행 `P2-R7`:** 캐릭터 생성의 빈 필수 `image`를 부작용 전에 거부하고, 수정의 `isActive=true` 단독 요청을 레거시와 같은 유효한 no-op mutation으로 처리한다. - **추적 review ID:** `REV-034`, `REV-035`. - **시작 조건:** `phase2-character-review.md` 9차 정적 리뷰 판정 존재. - **완료 증거:** 빈 생성 image의 400/no-side-effect와 `isActive=true` 단독 수정의 200 `data: null` actual endpoint RED/GREEN, character/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** OpenAPI schema 변경, 레거시 controller/service 변경, 캐릭터 mutation pipeline 리팩터링, `isActive=false`의 기존 비활성화 의미 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 필수 `image`를 빈 part로 보낸 POST가 현재 200으로 처리되고 외부 생성·DB 저장이 발생하는지 actual endpoint로 고정한다. - [x] **RED:** `{"isActive":true}`만 보낸 PUT이 현재 400을 반환하지만 레거시 endpoint는 유효한 변경 요청으로 받아 200 `data: null`을 반환하는 차이를 고정한다. - [x] **GREEN:** create facade 진입 직후 빈 image를 400 `common.error.invalid_request`로 거부해 외부 API, DB, S3, event를 호출하지 않는다. - [x] **GREEN:** `isActive`가 null이 아니면 변경 요청으로 인정하되, `false` 비활성화 분기와 `true` no-op의 레거시 service 호출·응답 의미를 그대로 유지한다. - [x] **REFACTOR:** character package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해 Progress와 리뷰 문서에 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest ./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 git diff --check ``` #### Phase 2 9차 리뷰 Gate **Goal 실행 `P2-R7-GATE`:** `REV-034`~`REV-035`의 multipart 필수 파일과 레거시 update parity를 재검토한다. - [x] **`P2-R7-GATE` 완료:** `P2-R7` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P2-R7` 완료. - **완료 증거:** 두 review ID 처리 완료, 빈 생성 image 400/no-side-effect, `isActive=true` 단독 PUT 200 `data: null`, 기존 `isActive=false`와 일반 mutation 회귀 성공. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록(RED): 무엇: `REV-034` 빈 생성 image와 `REV-035` `isActive=true` 단독 수정. 왜: empty multipart와 optional boolean parity를 실제 endpoint에서 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 신규 2건이 각각 400 기대 대비 200, 200 기대 대비 400으로 실패했다. - 검증 기록(GREEN/GATE): 무엇: character empty image 거부와 `isActive=true` no-op parity. 왜: 외부 API·DB·S3·event 전 400과 레거시 200 `data:null` 의미를 복구하기 위해. 어떻게: 같은 focused 명령 재실행 후 `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping 23개, `git diff --check`를 실행했다. 결과: 모두 성공했고 `git diff --check`는 출력이 없었다. #### Phase 2 11차 리뷰 보완 - [x] **Task 2.14: 캐릭터 관계 필수 정수의 null·누락 거부** **Goal 실행 `P2-R8`:** 캐릭터 생성 관계의 필수 `importance`가 누락되거나 null이면 JVM 기본값 `0`으로 보정하지 않고 외부 API·DB·S3·event 전에 400으로 거부한다. - **추적 review ID:** `REV-040`. - **시작 조건:** `phase2-character-review.md` 11차 정적 리뷰 판정 존재. - **완료 증거:** `importance` 누락·null의 actual endpoint RED/GREEN과 no-side-effect, 정상 정수 생성 회귀, character/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/controller 변경, OpenAPI schema 변경, 관계 중요도 범위 정책 추가. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 관계 객체에서 `importance`를 누락하거나 null로 보낸 POST가 현재 `0`으로 역직렬화되어 mutation을 진행하는지 actual endpoint로 고정하고 외부 API·DB·S3·event 결과를 단언한다. - [x] **GREEN:** v2 캐릭터 생성 경계에서 필수 non-null primitive의 존재와 null 여부를 strict parse 결과로 검증해 `common.error.invalid_request` 400으로 변환한다. - [x] **CONTRACT TEST:** 정상 `importance` 정수와 관계가 없는 생성은 기존 결과를 유지하고, 미지 필드 거부도 회귀하지 않는지 확인한다. - [x] **REFACTOR:** 검증을 v2 경계의 최소 범위에 두고 character/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. - 검증 기록(RED): 무엇: 관계 `importance` 누락·null actual POST. 왜: Jackson primitive 기본값 `0` 보정으로 외부 API·DB·S3·event 부작용이 발생할 수 있는지 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests "*shouldRejectMissingRelationshipImportanceBeforeSideEffects" --tests "*shouldRejectNullRelationshipImportanceBeforeSideEffects"`를 실행했다. 결과: 신규 2건이 `status().isBadRequest` 기대에서 실패해 `BUILD FAILED`였다. - 검증 기록(GREEN): 무엇: v2 캐릭터 request reader의 primitive null/누락 거부. 왜: 전역 mapper·레거시 DTO 변경 없이 v2 생성 경계에서 OpenAPI required non-null 정수 계약을 강제하기 위해. 어떻게: 같은 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 48s`였다. - 검증 기록(GATE): 무엇: 정상 정수 관계 생성, 미지 필드 거부, character/common 영향 범위와 lint/diff. 왜: `REV-040` 보완이 기존 캐릭터 생성·공통 오류/인가 계약을 회귀시키지 않는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`, `./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`, `git diff --check`를 실행했다. 결과: 각 Gradle 명령은 `BUILD SUCCESSFUL`이었고 `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest ./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 git diff --check ``` #### Phase 2 11차 리뷰 Gate **Goal 실행 `P2-R8-GATE`:** `REV-040`의 캐릭터 관계 필수 정수 nullability와 부작용 경계를 재검토한다. - [x] **`P2-R8-GATE` 완료:** `P2-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P2-R8` 완료. - **완료 증거:** review ID 처리 완료, `importance` 누락·null 400/no-side-effect, 정상 생성 회귀 성공. - **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경. #### Phase 2 후속 기능 보완 - [x] **Task 2.15: 캐릭터 등록용 원작 검색** **Goal 실행 `P2-R9`:** AI 캐릭터 등록 화면에서 soft delete되지 않은 원작을 필수 `searchTerm`으로 검색하고 레거시 `OriginalWorkResponse` 전체 필드의 직접 배열로 반환한다. - **추적 review ID:** `REV-044`. - **시작 조건:** `P2-R8-GATE` 완료와 PRD·OpenAPI의 승인된 원작 검색 계약 존재. - **완료 증거:** 제목·콘텐츠 타입·카테고리 부분 검색, soft delete 제외, 빈 결과, 필수 query 오류와 exact response, Character/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 원작 등록·수정·삭제, pagination·정렬 정책 추가, 원작 schema 축약, 레거시 `/admin/chat/original` 변경. **Interfaces:** - `GET /api/v2/admin/ai-characters/original-works/search?searchTerm={searchTerm}` - Produces: `ApiResponse>`. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminOriginalWorkSearchTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/service/AdminOriginalWorkService.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/dto/OriginalWorkDtos.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** actual GET으로 세 검색 필드, 삭제 원작 제외, 빈 결과, `searchTerm` 누락 400과 `OriginalWorkResponse`의 13개 필드를 고정한다. - [x] **GREEN:** `AdminOriginalWorkService.searchOriginalWorksAll`과 `OriginalWorkResponse.from`을 재사용해 별도 pagination이나 축약 DTO 없이 응답한다. - [x] **CONTRACT TEST:** 신규 route가 `/{characterId}`와 충돌하지 않고 ADMIN 이중 인가, 오류 envelope와 `Accept-Language` fallback을 유지하는지 확인한다. - [x] **REFACTOR:** 조회 전용 facade method 외 추상화를 추가하지 않고 Character/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. - 검증 기록(RED): 무엇: 캐릭터 등록용 원작 검색 신규 v2 endpoint actual GET 계약. 왜: route 미구현 상태에서 검색 필드·soft delete 제외·빈 결과·필수 query 오류가 실패하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminOriginalWorkSearchTest`를 실행했다. 결과: 4개 중 3개가 신규 route 부재로 실패했다. - 검증 기록(GREEN): 무엇: 원작 검색 endpoint 최소 구현. 왜: 레거시 `searchOriginalWorksAll`과 `OriginalWorkResponse.from` 재사용이 계약을 충족하는지 확인하기 위해. 어떻게: 같은 focused test를 재실행했다. 결과: `BUILD SUCCESSFUL`이었다. - 검증 기록(회귀): 무엇: Character/common 영향 범위, lint, diff whitespace. 왜: 신규 target 없는 route가 기존 character route와 공통 ADMIN/error 경계를 깨지 않는지 확인하기 위해. 어떻게: 아래 영향 범위 test, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: 영향 범위 test와 ktlint는 `BUILD SUCCESSFUL`, `git diff --check`는 출력 없음이었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminOriginalWorkSearchTest ./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 git diff --check ``` #### Phase 2 후속 기능 Gate **Goal 실행 `P2-R9-GATE`:** `REV-044`의 원작 검색 범위와 레거시 response parity를 재검토한다. - [x] **`P2-R9-GATE` 완료:** `P2-R9` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 2 리뷰와 Progress를 갱신한다. - **시작 조건:** `P2-R9` 완료. - **완료 증거:** 검색 필드·soft delete 제외·직접 배열·필수 query·공통 경계 회귀 성공, OpenAPI operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 2 multipart request part 계약 후속 보완 - [x] **Task 2.16: 캐릭터 생성·수정 request part의 application/json 강제** **Goal 실행 `P2-R10`:** 캐릭터 생성·수정 multipart의 `request` part가 OpenAPI encoding대로 `application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다. - **추적 review ID:** `REV-055`. - **시작 조건:** `P2-R9-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재. - **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·mutation 의미를 유지하고, `text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header, 외부 API·S3·DB·event no-side-effect를 반환한다. - **범위 밖:** JSON schema·strict reader·image 계약, external/S3/DB 순서, legacy/public endpoint, OpenAPI·신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual endpoint와 no-side-effect로 고정한다. - [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String strict reader에 동일 payload를 전달한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON, 필수 part 누락 400 계약을 확인한다. - [x] **REFACTOR:** 공통 helper가 필요하면 8개 multipart mapping의 media type 확인에만 한정하고 Character/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. - **RED 결과 (2026-07-29):** `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`에서 새 POST·PUT, `text/plain`·content type 누락, KO/EN/JA 12개 415 기대 케이스가 실패했다. - **GREEN/GATE 결과 (2026-07-29):** 같은 focused 명령은 `BUILD SUCCESSFUL in 31s`였고, Character/common 영향 범위 명령은 `BUILD SUCCESSFUL in 1m 2s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 18s`, `git diff --check`는 출력 없이 성공했다. - **전체 회귀 생략:** controller part 경계와 해당 actual endpoint 테스트만 변경했으므로 focused와 Character/common 오류 계약 회귀로 검증했다. 전체 `./gradlew test`는 실행하지 않았다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 2 multipart request part 계약 후속 Gate **Goal 실행 `P2-R10-GATE`:** `REV-055` 수정 뒤 Character POST·PUT의 part-level JSON-only·415 경계를 재검토한다. - [x] **`P2-R10-GATE` 완료:** `P2-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 2 리뷰와 Progress를 갱신한다. - **시작 조건:** `P2-R10` 완료. - **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 2 multipart part 이름 계약 후속 보완 - [x] **Task 2.17: 캐릭터 생성·수정의 미정의 multipart part 거부** **Goal 실행 `P2-R11`:** Character 생성·수정 multipart에서 OpenAPI가 정의한 `image`, `request` 외 part를 handler의 business mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-060`. - **시작 조건:** `phase2-character-review.md` 13차 정적 리뷰 판정 존재. - **완료 증거:** POST·PUT actual endpoint에서 `unexpected` 파일/문자열 part의 400 KO/EN/JA envelope와 외부 API·S3·DB·event no-side-effect, 정상 허용 part·기존 415 경계 회귀. - **범위 밖:** OpenAPI schema 변경, 전역 multipart resolver 변경, legacy/public endpoint, 허용 파일의 내용 검증, 공통 추상화 추가. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성·수정에 정상 `request`와 `unexpected` part를 함께 보내 현재 mutation이 성공하는 경로를 actual endpoint로 고정하고 외부 API·S3·DB·event 결과를 단언한다. - [x] **GREEN:** 기존 multipart 검사에서 실제 part 이름 집합이 POST·PUT 허용 집합 `{image, request}`의 부분집합인지 확인하고, 초과 이름이 있으면 `AiCharacterAdminApiException(HttpStatus.BAD_REQUEST, "common.error.invalid_request")`를 던진다. - [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수 part 누락 400, request part media type 415와 no-side-effect를 함께 확인한다. - [x] **REFACTOR:** Character controller/test만 최소 변경하고 Character/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. - 검증 기록(RED): 무엇: Character POST·PUT의 미정의 multipart part 거부. 왜: OpenAPI `additionalProperties:false`와 달리 `unexpected` part가 무시된 채 mutation이 진행될 수 있었기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects'`를 실행했다. 결과: 신규 6개 invocation이 400 기대 대비 200으로 실패해 RED를 확인했다. - 검증 기록(GREEN/GATE): 무엇: Character multipart 허용 part 이름 `{image, request}` 적용과 기존 request part media type·누락 회귀. 왜: 미정의 part를 business facade 진입 전에 400으로 차단하고 기존 정상/415/400 경계를 유지하기 위해서다. 어떻게: controller에서 `MultipartHttpServletRequest.fileMap.keys`를 검사하고 아래 focused/영향 범위 회귀와 `ktlintCheck`, `git diff --check`를 실행했다. 결과: focused undefined/non-json/missing request 명령은 `BUILD SUCCESSFUL in 1m 41s`, Character/common 영향 범위는 `BUILD SUCCESSFUL in 1m 36s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 30s`, `git diff --check`는 출력 없음이었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests '*shouldRejectNonJsonRequestPartBeforeSideEffects' --tests '*shouldKeepMissingRequestPartAsBadRequest' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 2 multipart part 이름 계약 후속 Gate **Goal 실행 `P2-R11-GATE`:** `REV-060` 수정 뒤 Character POST·PUT의 허용 part 이름과 기존 media type 경계를 재검토한다. - [x] **`P2-R11-GATE` 완료:** `P2-R11` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 2 리뷰와 Progress를 갱신한다. - **시작 조건:** `P2-R11` 완료. - **완료 증거:** 미정의 part 400/no-side-effect, 정상 허용 part, 필수 part·415 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. - 검증 기록: 무엇: `P2-R11-GATE`에서 `REV-060` 처리 완료와 Phase 2 13차 리뷰 종결을 확인했다. 왜: Phase 3 `P3-R17` 시작 조건인 `P2-R11-GATE` 완료를 판정하기 위해서다. 어떻게: focused/영향 범위 회귀, lint, diff check와 `phase2-character-review.md` 최신 결론을 대조했다. 결과: Character 미정의 multipart part는 400/no-side-effect로 처리되고 기존 정상 허용 part·필수 part 누락·request part 415 회귀가 유지됐다. #### Phase 2 multipart 일반 form-field part 후속 보완 - [x] **Task 2.18: 캐릭터 생성·수정의 전체 multipart part 이름 검증** **Goal 실행 `P2-R12`:** Character POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한 모든 multipart part 이름을 검사해 `{image, request}` 외 이름을 mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-065`. - **시작 조건:** `phase2-character-review.md` 8차 정적 리뷰 판정 존재. - **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 external API·S3·DB·event no-side-effect, 기존 파일형 미정의 part·정상 허용 part·필수 part·request part 415 회귀 성공. - **범위 밖:** OpenAPI schema, 전역 multipart resolver, legacy/public endpoint, 공통 추상화, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `MockPart` 등 filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재 `fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다. - [x] **GREEN:** servlet request의 전체 part 이름 집합을 `{image, request}`와 비교해 초과 이름을 facade 진입 전에 공통 400으로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 파일형 미정의 part, 정상 생성·수정, 필수 part 누락, request part 415와 no-side-effect를 확인한다. - [x] **REFACTOR:** Character controller/test만 최소 변경하고 Character/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 2 multipart 전체 part 이름 후속 Gate **Goal 실행 `P2-R12-GATE`:** `REV-065` 수정 뒤 Character POST·PUT의 파일·일반 form-field를 포함한 전체 part 이름과 기존 media type 경계를 재검토한다. - [x] **`P2-R12-GATE` 완료:** `P2-R12` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 2 리뷰와 Progress를 갱신한다. - **시작 조건:** `P2-R12` 완료. - **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` - 검증 기록(RED): 무엇: Character POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다. - 검증 기록(GREEN): 무엇: Character multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 `{image, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다. - 검증 기록(GATE): Character/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다. --- ### Phase 3: 오디오 콘텐츠·댓글 관리와 signed URL vertical slice #### 목표 선택한 AI 캐릭터 소유 오디오 콘텐츠 목록/검색/상세/생성/수정/soft delete, 댓글 CRUD와 관리자 재생용 signed URL을 제공한다. #### 범위와 비범위 - 포함: 콘텐츠 owner 검증, 기존 파일 처리/가격/공개/예약/번역/알림 parity, 댓글 root/reply 조회·작성·수정·soft delete, `AudioContentCloudFront` 재사용, private path 비노출. - 제외: 콘텐츠 구매/좋아요, 캐릭터 직접 댓글, 댓글 hard delete·cascade, content upload/processing pipeline 변경, community audio 30분 정책 통합. #### 선행 Phase 및 의존성 - Phase 1 target resolver. - 콘텐츠 생성/수정/delete 기존 동작 특성화 테스트. #### API endpoint와 request/response contract - `GET /api/v2/admin/ai-characters/audio-content-themes` - Request: query/body 없음. - Response: `List`. - `GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search_word=&page=&size=` -> `GetCreatorAdminContentListResponse` 전체 필드. - `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}?timezone=Asia/Seoul` -> `GetAudioContentDetailResponse` 전체 nested 필드. - `POST /api/v2/admin/ai-characters/{characterId}/audio-contents` - multipart 필수 `contentFile`, `coverImage`, `request: CreateAudioContentRequest`. - Response: `CreateAudioContentResponse(contentId)`. - `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` - multipart optional `coverImage`, 필수 `request: UpdateCreatorAdminContentRequest`에서 `id` 제외. - Response: `data: null`. - 수정 `audioFile` 교체는 레거시 creator admin 수정 pipeline에 없어 제공하지 않는다. - 댓글은 `GET|POST .../{contentId}/comments`, `PUT|DELETE .../{contentId}/comments/{commentId}`, `GET .../{commentId}/replies`의 5개 operation을 사용한다. - 정식 전체 schema와 optional/nullable은 `api-contract.openapi.json`의 AudioContent operation을 따른다. 현재 구현의 `description`, `releaseDateUtc`, `audioSignedUrl`, `status`, `seriesIds`, `themeName`, `imageUrl` 별칭은 `P23-CONTRACT-3`에서 레거시 계약으로 정합화한다. #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: `AudioContent` owner-scoped query adapter 추가 가능. - Service: 신규 content admin application service에서 기존 creator/admin content service를 테스트로 고정 후 선택 위임 또는 최소 추출한다. - CloudFront: 기존 `AudioContentCloudFront` 그대로 주입해 사용한다. #### DB migration - 없음. #### transaction과 concurrency 고려사항 - write transaction 시작 직후 target과 `content.member.id == creatorMember.id`를 검증한다. - S3 업로드, 이벤트 발행 순서는 기존 동작 parity를 따른다. - soft delete는 기존 콘텐츠 삭제 동작처럼 `isActive=false`, 필요한 경우 `releaseDate=null`을 유지한다. #### 보안 및 개인정보 위험 - private S3 object path, signed key material을 응답하지 않는다. - 다른 캐릭터 content ID 접근은 4xx와 no side effect다. #### acceptance criteria - target 캐릭터 소유 콘텐츠만 조회/변경된다. - signed URL 만료 계산이 기존 creator admin policy와 동일하다는 테스트가 있다. - signed URL 만료 계산·path 처리에서 실제 기존 구현에서 관찰되는 edge case가 특성화 테스트로 고정된다. - invalid target/ownership 실패 시 DB/S3/event side effect가 없다. #### targeted test - Characterization: `LegacyCreatorAdminAudioContentCharacterizationTest`, `AudioContentCloudFrontCharacterizationTest`. - V2 RED/GREEN: `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`. - Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` #### 전체 회귀 테스트 영향 - 기존 `CreatorAdminContentController`, `AdminContentController`, public content 조회 테스트가 통과해야 한다. #### rollback 전략 - 신규 content v2 admin route/facade를 제거한다. - 신규 DDL이 없으므로 schema rollback은 없다. #### 권장 commit 경계 - `feat: add ai character admin content slice` - [x] **Task 3.1: 기존 콘텐츠와 signed URL 특성화 baseline 고정** - **Goal 이력 `P3-H1`:** 기존 creator/admin 콘텐츠와 signed URL 동작을 신규 v2 비교 기준으로 고정했다. - **완료 증거:** 아래 특성화 테스트와 `ktlintCheck` 실행 기록. - **범위 밖:** 신규 v2 content endpoint 구현. - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt` - CHARACTERIZE: 기존 creator/admin content 구현을 대상으로 검증, 파일 처리, 가격, 공개/예약, 번역/알림, soft delete, upload/processing pipeline 결과를 고정한다. - CHARACTERIZE: 기존 creator admin signed URL 만료 계산식과 만료 계산·path 처리에서 실제로 관찰되는 edge case, private path 비노출 계약을 고정한다. - BASELINE: 신규 v2 production code 변경 전에 두 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다. - REFACTOR: 테스트 fixture만 정리하고 content/signed URL parity baseline은 변경하지 않는다. - Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*CharacterizationTest'` - 검증 기록 (2026-07-26): 위 특성화 테스트 6건과 `./gradlew ktlintCheck`가 모두 통과했다. - [x] **Task 3.2: content controller/facade/DTO 구현** - **Goal 이력 `P3-H2`:** 콘텐츠 조회·생성·수정의 최초 v2 API 구현을 제공했다. - **완료 증거:** 아래 RED/GREEN 및 후속 보완 기록. 이 이력만으로 Phase 3 Gate 통과를 의미하지 않는다. - **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완. - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*` - RED: Task 3.1 baseline에서 옮긴 content parity, cross-character content 접근, signed URL/private path 계약 테스트와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다. - GREEN: content endpoint와 owner-scoped query/write를 구현하고 목록/검색 `page/size` 기본값·최소·최대 보정을 적용한다. - REFACTOR: signed URL/private path mapping과 기존 pipeline 재사용 경계를 정리하고 회귀 테스트를 재실행한다. - Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` - 구현 기록(2026-07-26): `GET` 목록/검색·상세와 `PUT` 수정/soft delete를 target resolver와 owner-scoped query로 구현했다. 응답은 v2 DTO만 사용하며 private content path 필드를 반환하지 않는다. - 구현 보완(2026-07-27): `POST` 생성을 기존 `AudioContentService.createAudioContent` 재사용 방식으로 제공한다. v2 adapter에서 `themeId`, ISO-8601 `releaseDateUtc`, owner-scoped `seriesIds` 연결을 처리하고, 업로드 완료 전 `isActive` 직접 활성화는 기존 processing pipeline parity를 위해 수행하지 않는다. - 검증 기록(RED, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest'`를 실행했고, 신규 route 부재로 목록은 404, 상세와 PUT은 404/405여서 의도대로 실패함을 확인했다. - 검증 기록(GREEN, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`와 `./gradlew ktlintCheck`를 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. - 검토 보완(2026-07-26): 상세 응답에 target owner 범위의 활성 series ID 목록을 추가하고, 다른 캐릭터 콘텐츠 PUT이 DB를 변경하지 않는 통합 테스트를 추가했다. - 재검증(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에서 미지원으로 명시했다. - [x] **Task 3.3: Phase 3 요구사항·계약·코드 리뷰** **Goal 실행 `P3-R1`:** PRD Feature C와 Endpoint Contract Summary를 콘텐츠 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다. - **추적 review ID:** `REV-004`, `REV-005`, `REV-006`, `REV-007`, `REV-008`. - **시작 조건:** `P2-GATE`, `P3-H1`, `P3-H2` 완료 증거 존재. - **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint·pipeline·side-effect 추적표, 모든 후보 판정과 Progress 기록. - **범위 밖:** 리뷰 도중 production code 수정, series/community 기능 검토. - **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다. - **대체 검증 방법:** PRD·계약·production·test를 대조하고 focused test를 실행해 실제 결과를 리뷰 문서에 기록한다. **Files:** - Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - 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` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt` - 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` - [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는 수정하지 않았다. - [x] **Task 3.4: 활성 콘텐츠 테마 API 보완** **Goal 실행 `P3-T3`:** 활성 콘텐츠 테마를 전용 `themeId/themeName/imageUrl` DTO로 반환하는 관리자 API를 완결한다. - **추적 review ID:** `REV-007`, `REV-008` 중 v2 DTO 경계와 endpoint 인가. - **시작 조건:** `P3-R1` 완료와 테마 endpoint review 판정. - **완료 증거:** request body 없는 GET, 활성 필터, DTO field contract RED/GREEN과 Progress 기록. - **범위 밖:** 테마 CRUD, legacy DTO 외부 노출. **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/AiCharacterAdminAudioContentDto.kt` - 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` - [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` - [x] **Task 3.5: 콘텐츠 목록·상세·signed URL 보완** **Goal 실행 `P3-T4`:** owner-scoped 콘텐츠 조회와 signed URL·상세 DTO 계약을 독립적으로 완결한다. - **추적 review ID:** `REV-005`, `REV-007` 중 상세 parity와 v2 전용 DTO 경계. - **시작 조건:** `P3-T3` 완료와 조회/signed URL 관련 review 판정. - **완료 증거:** 검색·status·pagination·상세·TTL/path/private 정보 RED/GREEN과 Progress 기록. - **범위 밖:** 콘텐츠 생성·수정, community audio 30분 정책. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt` - Modify: `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/AiCharacterAdminAudioContentQueryTest.kt` - 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` - [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` - [x] **Task 3.6: 콘텐츠 생성 pipeline 보완** **Goal 실행 `P3-T5`:** 콘텐츠 생성의 multipart 입력, legacy field 변환, 파일·processing·series 연결과 side effect parity를 완결한다. - **추적 review ID:** `REV-006`, `REV-007`, `REV-008` 중 생성 binding·field·pipeline 특성화. - **시작 조건:** `P3-T4` 완료와 생성 pipeline 관련 review 판정. - **완료 증거:** 전체 생성 field·파일·날짜·series RED/GREEN, 실패 순서와 focused/legacy test 기록. - **범위 밖:** 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/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - 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` - [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` - [x] **Task 3.7: 콘텐츠 수정·soft delete 보완** **Goal 실행 `P3-T6`:** 콘텐츠 수정, cover 유지/교체, series 재연결과 soft delete를 owner-safe하게 완결한다. - **추적 review ID:** `REV-004`. - **시작 조건:** `P3-T5` 완료와 수정·삭제 관련 review 판정. - **완료 증거:** 수정·soft delete·audioFile 미지원·cross-owner RED/GREEN과 Progress 기록. - **범위 밖:** audio file 교체, hard delete. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt` - 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` - [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` - [x] **Task 3.8: 콘텐츠 ownership·오류·legacy 회귀 보완** **Goal 실행 `P3-T7`:** Phase 3 모든 endpoint의 ownership·ADMIN·오류 계약과 legacy/public 회귀를 고정한다. - **추적 review ID:** `REV-006`, `REV-007`, `REV-008`. - **시작 조건:** `P3-T6` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정. - **완료 증거:** 권한 매트릭스, 정확한 domain/client 오류 status/key/KO·EN·JA, no-side-effect와 legacy 회귀 기록. - **범위 밖:** Phase 1 공통 handler 재설계, Phase 4 series 기능. **Files:** - 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` - 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] **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 회귀를 최종 판정한다. - [x] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P3-R1`, `P3-T3`~`P3-T7` 완료 또는 근거 있는 `해당 없음` 판정. - **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록. - **범위 밖:** Gate 실패와 무관한 Phase 4 기능 구현. - [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.*' ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck ``` **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 3 7차 리뷰 보완 - [x] **Task 3.19: 관리자 오디오 repository의 미사용 확장 제거** **Goal 실행 `P3-R9`:** `REV-022`의 실제 호출되지 않는 조회·시리즈 교체 helper와 그 전용 status enum을 제거해 repository를 현재 owner-scoped 상세 조회 책임으로 축소한다. - **추적 review ID:** `REV-022`. - **시작 조건:** `P2-R6-GATE` 완료와 `phase3-audio-content-review.md` 7차 리뷰 판정 존재. - **완료 증거:** 호출 검색 결과와 일치하는 미사용 method/enum/import 제거 diff, owner-scoped 상세 조회 회귀, content/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 실제 사용 중인 `findByIdAndCreatorMemberId`, legacy repository/service, 콘텐츠·시리즈 동작 변경, 인접 repository 리팩터링. - **TDD 예외 사유:** 호출자가 없는 내부 코드 제거이며 외부 동작이나 계약을 추가하지 않는 동작 불변 리팩터링이다. - **대체 검증 방법:** production/test 전체 호출 검색으로 제거 대상을 확정하고 상세·ownership 테스트를 focused 회귀한다. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt` - Test: `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` - [x] **REFACTOR:** `findPage`, `findSeriesIds`, `replaceSeriesIds`, 그 private helper와 `AiCharacterAdminAudioContentStatus`를 제거하고 발생한 unused import만 정리한다. - [x] **STATIC 확인:** `findByIdAndCreatorMemberId` 외 제거 대상의 호출이 0건인지 production/test 전체에서 확인한다. - [x] **회귀 확인:** 콘텐츠 상세·ownership focused test, content/common 영향 범위 회귀와 `ktlintCheck`를 실행한다. - 검증 기록: 무엇: `REV-022`의 관리자 오디오 repository 미사용 확장과 전용 status enum을 제거했다. 왜: 현재 facade가 사용하는 repository 경계는 owner-scoped 상세 조회 `findByIdAndCreatorMemberId` 하나뿐이기 때문이다. 어떻게: `findPage`, `findSeriesIds`, `replaceSeriesIds`, `hasActiveSeriesIds`, 관련 private helper와 `AiCharacterAdminAudioContentStatus`를 제거하고 package-scoped 호출 검색을 실행했다. 결과: 대상 package 호출 검색은 출력이 없었고, 상세·ownership focused 명령은 `BUILD SUCCESSFUL in 3m 39s`, content/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력이 없었다. ```bash rg -n 'findPage|findSeriesIds|replaceSeriesIds|hasActiveSeriesIds|AiCharacterAdminAudioContentStatus' src/main src/test ./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 git diff --check ``` #### Phase 3 7차 리뷰 Gate **Goal 실행 `P3-R9-GATE`:** `REV-022`의 미사용 코드 제거와 콘텐츠 동작 불변 증거를 재검토한다. - [x] **`P3-R9-GATE` 완료:** `P3-R9` 완료 후 위 static/focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P3-R9` 완료. - **완료 증거:** `REV-022` 처리 완료, owner-scoped 상세 동작 유지, focused/영향 범위 회귀와 lint·diff 성공. - **범위 밖:** Gate에서 production code 수정, legacy 콘텐츠·시리즈 동작 변경. - 검증 기록: 무엇: `P3-R9-GATE`에서 `REV-022` 처리 완료와 Phase 3 7차 리뷰 종결을 확인했다. 왜: Phase 4 7차 보완의 시작 조건이 `P3-R9-GATE` 완료이기 때문이다. 어떻게: `phase3-audio-content-review.md` 7차 리뷰 후속 판정을 갱신하고 위 static/focused/영향 범위 회귀, lint, diff check 결과를 대조했다. 결과: `REV-022`는 처리 완료로 판정했고 Phase 3은 19/19 완료 상태로 동기화했다. #### Phase 3 8차 리뷰 보완 - [x] **Task 3.20: 오디오 생성 날짜·시간대 의미 검증 복구** **Goal 실행 `P3-R10`:** `REV-030`의 잘못된 `releaseDate` 형식과 `timezone` 값을 legacy service 호출 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-030`. - **시작 조건:** `P3-R9-GATE` 완료와 `phase3-audio-content-review.md` 8차 리뷰 판정 존재. - **완료 증거:** 잘못된 날짜 형식·시간대의 actual endpoint RED, KO/EN/JA 400 envelope과 DB/S3/event 0건, 정상 생성 및 content/common 영향 범위 회귀, lint·diff와 Progress 기록. - **범위 밖:** OpenAPI field/schema 변경, legacy `AudioContentService` 전역 동작 변경, 공통 예외 handler에 `DateTimeException`을 일괄 client 오류로 추가, upload/processing pipeline 변경. **Files:** - 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/AiCharacterAdminAudioContentCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 그 밖의 필드는 유효한 생성 request에서 `releaseDate="not-a-date"`와 `timezone="Invalid/Zone"`을 각각 보내 현재 500 `common.error.unknown`이 반환되는지 확인한다. - [x] **RED:** 두 입력을 KO/EN/JA actual endpoint matrix로 고정하고 AudioContent·S3·event가 요청 전후 변하지 않음을 단언한다. - [x] **GREEN:** strict JSON parse 결과를 재사용해 `releaseDate`의 `yyyy-MM-dd HH:mm` 형식과 `timezone`의 `ZoneId`를 legacy service 호출 전에 검증하고 `DateTimeException`을 해당 입력 경계에서만 400으로 변환한다. - [x] **REFACTOR:** 정상 예약/즉시 생성과 기존 preview/theme 오류 key를 유지하고 content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다. - 검증 기록(RED): 무엇: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 KO/EN/JA actual endpoint로 고정했다. 왜: strict JSON parse는 통과하지만 legacy `AudioContentService`의 Java time 변환 예외가 500으로 분류됐기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 6개 matrix를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`를 실행했다. 결과: 신규 6건이 400 기대 assertion에서 실패해 `BUILD FAILED in 1m 4s`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: v2 facade 생성 경계에서 `releaseDate`를 `yyyy-MM-dd HH:mm`으로, `timezone`을 `ZoneId`로 legacy 호출 전에 검증했다. 왜: 전역 handler나 legacy service 영향 없이 신규 관리자 생성 API의 client 오류만 400으로 분류하기 위해. 어떻게: strict parse 결과를 재사용해 `DateTimeException`을 `common.error.invalid_request`로 변환하고 focused/영향 범위 회귀를 실행했다. 결과: 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`였다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest ./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 git diff --check ``` #### Phase 3 8차 리뷰 Gate **Goal 실행 `P3-R10-GATE`:** `REV-030`의 의미 검증과 오류·no-side-effect 계약을 재검토한다. - [x] **`P3-R10-GATE` 완료:** `P3-R10` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P3-R10` 완료. - **완료 증거:** `REV-030` 처리 완료, 잘못된 날짜·시간대 400/no-side-effect, 정상 생성 회귀 성공. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록: 무엇: `P3-R10-GATE`에서 `REV-030` 처리 완료와 Phase 3 8차 리뷰 종결을 확인했다. 왜: Phase 4 2차 보완으로 넘어가기 전에 오디오 생성 의미 검증과 영향 범위 회귀가 완료됐는지 판정하기 위해. 어떻게: `phase3-audio-content-review.md`에 처리 결과를 누적하고 위 focused/영향 범위 회귀와 lint 결과를 대조했다. 결과: 잘못된 날짜·시간대는 400/no-side-effect로 고정됐고 Phase 3은 20/20 완료 상태로 동기화했다. #### Phase 3 9차 리뷰 보완 - [x] **Task 3.21: 오디오 상세의 예약 공개일 레거시 표시 복구** **Goal 실행 `P3-R11`:** 미래 예약 콘텐츠 상세의 `releaseDate`를 KO/EN/JA 레거시 형식으로 반환하고, 공개 시각이 지난 콘텐츠는 기존처럼 null을 반환한다. - **추적 review ID:** `REV-036`. - **시작 조건:** `P2-R7-GATE` 완료와 `phase3-audio-content-review.md` 9차 정적 리뷰 판정 존재. - **완료 증거:** 미래·과거 예약일과 KO/EN/JA locale actual endpoint RED/GREEN, signed URL·전체 상세 DTO 불변, content/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** OpenAPI schema 변경, 레거시 `AudioContentService` 변경, 예약 공개·signed URL 정책 재설계, 목록 response mapping 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Confirm: `src/main/resources/messages*.properties` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 미래 UTC `releaseDate`를 가진 owner 콘텐츠 상세가 현재 모든 locale에서 null을 반환하는 계약 차이를 actual endpoint로 고정한다. - [x] **GREEN:** 기존 `SodaMessageSource`와 `LangContext`를 사용해 legacy의 미래 여부, UTC→Asia/Seoul 변환, `content.release_date.format` 포맷을 동일하게 적용한다. - [x] **CONTRACT TEST:** 미래 예약일은 KO/EN/JA 형식 문자열, 현재 또는 과거 예약일은 null이며 나머지 상세 필드와 signed URL 결과가 변하지 않는지 확인한다. - [x] **REFACTOR:** 단일 mapper 안에서 legacy 규칙만 최소 이관하고 content package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest ./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 git diff --check ``` #### Phase 3 9차 리뷰 Gate **Goal 실행 `P3-R11-GATE`:** `REV-036`의 미래·과거 예약일과 locale별 상세 응답 parity를 재검토한다. - [x] **`P3-R11-GATE` 완료:** `P3-R11` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P3-R11` 완료. - **완료 증거:** review ID 처리 완료, 미래 KO/EN/JA `releaseDate`, 과거 null, signed URL·상세 DTO 회귀 성공. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록(RED): 무엇: 미래 예약 오디오 상세 `releaseDate` KO/EN/JA. 왜: mapper가 모든 상세 `releaseDate`를 null로 고정하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest`를 실행했다. 결과: 신규 미래 locale 3건이 기대 문자열 대비 null로 실패했다. - 검증 기록(GREEN/GATE): 무엇: 미래 예약일의 레거시 locale 표시와 과거 null. 왜: 기존 `content.release_date.format`과 UTC→Asia/Seoul 변환을 신규 상세 endpoint에 맞추기 위해. 어떻게: 같은 focused 명령 재실행 후 targeted/전체/lint/OpenAPI/mapping/diff 검증을 실행했다. 결과: focused query test와 전체 검증이 모두 성공했다. #### Phase 3 11차 리뷰 보완 - [x] **Task 3.22: 오디오 생성 primitive 필드의 null·누락 계약 강제** **Goal 실행 `P3-R12`:** 오디오 생성의 필수 `price` 누락·null과 non-null primitive의 명시적 null을 JVM 기본값으로 보정하지 않고 파일 업로드·DB·event 전에 400으로 거부하며, optional 필드 생략 시 기존 기본값은 유지한다. - **추적 review ID:** `REV-041`. - **시작 조건:** `P2-R8-GATE` 완료와 `phase3-audio-content-review.md` 11차 정적 리뷰 판정 존재. - **완료 증거:** 필수 `price` 누락·null과 optional primitive null의 actual endpoint RED/GREEN/no-side-effect, optional 생략·정상 생성 회귀, content/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, 별도 입력 범위 정책 추가. **Files:** - 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/AiCharacterAdminAudioContentCreateTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `price` 누락·null과 `themeId`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`의 명시적 null이 현재 기본값으로 처리되는지 actual endpoint matrix로 고정하고 AudioContent·S3·event 무변경을 단언한다. - [x] **GREEN:** v2 생성 경계에서 OpenAPI required/non-null primitive의 존재와 null 여부를 검증하고 `common.error.invalid_request` 400으로 변환한다. - [x] **CONTRACT TEST:** optional primitive를 생략하면 OpenAPI·레거시 기본값을 유지하고 정상 예약·즉시 생성, 기존 미지 필드·날짜·시간대 검증이 변하지 않는지 확인한다. - [x] **REFACTOR:** 기존 strict parse 결과를 재사용하는 최소 검증으로 제한하고 content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. - 검증 기록(RED): 무엇: 오디오 생성 `price` 누락·null과 primitive field explicit null actual POST. 왜: Jackson primitive 기본값 `0`/`false` 보정으로 파일 업로드·DB·event 부작용이 발생할 수 있는지 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests "*shouldRejectMissingOrNullPriceBeforeUpload" --tests "*shouldRejectNullPrimitiveFieldsBeforeUpload"`를 실행했다. 결과: 8개 invocation이 `status().isBadRequest` 기대에서 실패해 `BUILD FAILED`였다. `themeId:null`은 기존 missing-theme 검증으로 이미 400이었다. - 검증 기록(GREEN): 무엇: v2 오디오 생성 request reader의 primitive null/누락 거부. 왜: 전역 mapper·레거시 DTO/service 변경 없이 v2 생성 경계에서 OpenAPI required/non-null primitive 계약을 강제하기 위해. 어떻게: 같은 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 42s`였다. - 검증 기록(GATE): 무엇: optional 생략 기본값, 정상 생성, 날짜/시간대·미지 필드 검증, content/common 영향 범위와 lint/diff. 왜: `REV-041` 보완이 기존 오디오 생성·공통 오류/인가 계약을 회귀시키지 않는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`, `./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`, `git diff --check`를 실행했다. 결과: 각 Gradle 명령은 `BUILD SUCCESSFUL`이었고 `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest ./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 git diff --check ``` #### Phase 3 11차 리뷰 Gate **Goal 실행 `P3-R12-GATE`:** `REV-041`의 오디오 생성 primitive nullability와 기본값·부작용 경계를 재검토한다. - [x] **`P3-R12-GATE` 완료:** `P3-R12` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P3-R12` 완료. - **완료 증거:** review ID 처리 완료, invalid primitive 400/no-side-effect, 생략 기본값과 정상 생성 회귀 성공. - **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경. #### Phase 3 후속 기능 보완 - [x] **Task 3.23: 오디오 콘텐츠 댓글 CRUD** **Goal 실행 `P3-R13`:** target AI 캐릭터 소유 활성 오디오 콘텐츠의 원댓글·답글을 조회하고 target AI 명의로 작성·수정하며, 해당 콘텐츠에 달린 댓글·답글은 작성자와 관계없이 row 단위로 soft delete한다. - **추적 review ID:** `REV-045`. - **시작 조건:** `P2-R9-GATE` 완료와 PRD·OpenAPI의 승인된 댓글 행위자·소유권 계약 존재. - **완료 증거:** 5개 actual endpoint, root/reply 조회, target AI 작성, 작성자 제한 수정, owner 범위 삭제, cross-resource/parent/character 격리, idempotent delete와 exact response 회귀. - **범위 밖:** 캐릭터 직접 댓글 삭제, 댓글 hard delete·cascade, 새 pagination wrapper, 레거시/public endpoint 변경. **Interfaces:** - `GET|POST /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments` - `PUT|DELETE /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}` - `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}/replies` - 조회 query: 필수 `timezone`, `page`, `size`; response `GetAudioContentCommentListResponse(totalCount, items)`. - 작성 body: 필수 `comment`, optional/nullable `parentId`, optional `isSecret=false`, optional/nullable `languageCode`. - 수정 body: 필수 `comment`; mutation 성공 `data: null`. **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/AiCharacterAdminAudioContentDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Modify: `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/AiCharacterAdminAudioContentCommentTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** root/reply 목록의 `timezone/page/size`, `totalCount/items`, target 소유 활성 콘텐츠 경계를 actual GET으로 고정한다. - [x] **RED:** root와 reply 작성 시 저장된 `member`가 target `creatorMember`이고, `parentId`가 같은 콘텐츠의 활성 root가 아니면 400/no insert/no event인지 고정한다. - [x] **RED:** target AI가 작성한 활성 댓글/답글만 수정되고 팬 작성, 다른 콘텐츠·캐릭터 댓글 수정은 400/no mutation인지 고정한다. - [x] **RED:** target 소유 콘텐츠의 팬/AI 댓글·답글 삭제는 해당 row만 비활성화하고 하위 답글은 유지하며, 이미 비활성인 row는 200 no-op인지 고정한다. - [x] **GREEN:** 기존 `AudioContentCommentService`의 조회·작성·수정 의미를 재사용하되 facade에서 target, active owner, 동일 리소스 root parent, actor 권한을 먼저 검증한다. - [x] **CONTRACT TEST:** 미지 필드, 잘못된 page/size/timezone, cross-resource ID의 400 envelope와 모든 mutation의 `data: null`을 확인한다. - [x] **REFACTOR:** 댓글 전용 추상화나 cascade 로직을 추가하지 않고 content/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest ./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 git diff --check ``` #### Phase 3 후속 기능 Gate **Goal 실행 `P3-R13-GATE`:** `REV-045`의 댓글 actor·owner·parent·soft delete 경계를 재검토한다. - [x] **`P3-R13-GATE` 완료:** `P3-R13` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R13` 완료. - **완료 증거:** 5개 operation, 레거시 목록 parity, AI 작성·수정 제한, owner 범위 row soft delete, cross-resource no-side-effect 회귀 성공. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 3 UTC 날짜 계약 보완 - [x] **Task 3.24: 오디오 생성·상세·댓글의 timezone 제거와 UTC 계약 정합화** **Goal 실행 `P3-R14`:** 신규 관리자 오디오 생성에서 `timezone`을 제거하고 nullable `releaseDate`를 UTC `date-time`으로 받으며, 상세·댓글·답글 조회도 `timezone` 없이 기존 날짜 필드를 ISO-8601 UTC(`Z`)로 반환한다. - **추적 review ID:** `REV-050`. - **시작 조건:** `P3-R13-GATE` 완료와 `DEC-UTC-DATE-001` 및 OpenAPI 2.2.0 계약 존재. - **완료 증거:** 오디오 생성·상세·댓글·답글 4개 actual operation의 query/body·UTC exact JSON RED/GREEN, 기존 상세 `releaseDate` null/노출 조건·댓글 pagination/ownership 보존, legacy/public 회귀와 Progress 기록. - **범위 밖:** 오디오 목록의 날짜 필드 변경, legacy/public request/response 변경, 로컬 시각+timezone 병행 지원, 신규 dependency·DDL, 댓글 mutation 의미 변경. **Interfaces:** - `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`: multipart `request`에 `timezone`이 없고, optional/nullable `releaseDate`는 ISO-8601 UTC(`Z`)다. - `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`: query 없음. nullable `releaseDate`는 기존 미래 예약일 노출·현재/과거 null 조건을 유지하고 값이 있으면 ISO-8601 UTC(`Z`)다. - `GET .../audio-contents/{contentId}/comments`와 `GET .../comments/{commentId}/replies`: query는 `page`, `size`만 사용하고 `totalCount`, `items`와 각 item의 기존 `date` 필드명을 유지한다. `date` 값은 ISO-8601 UTC(`Z`)다. **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/AiCharacterAdminAudioContentDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCommentTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `timezone` 없는 상세·댓글·답글 GET이 현재 400이고, 현재 상세 `releaseDate`와 댓글 `date`가 locale/legacy 문자열인 계약 차이를 actual endpoint로 고정한다. - [x] **RED:** `timezone` 없는 생성 request의 UTC `releaseDate`가 현재 legacy 형식 검증에서 거부되는 것과, 로컬 문자열·non-UTC offset·`timezone` 미지 필드가 400/no upload/no DB/no event인지 고정한다. - [x] **GREEN:** v2 전용 생성 DTO에서 `timezone`을 제거하고 UTC instant를 한 번 파싱한다. 초 단위를 버리는 문자열 재포맷을 하지 않고 UTC `LocalDateTime`을 내부 생성 경계에 전달하되, 기존 legacy 생성 진입점의 외부 계약은 유지한다. - [x] **GREEN:** controller/facade의 세 GET signature와 timezone 검증을 제거하고, 상세 mapper와 v2 owner-scoped 댓글 query/mapping에서 기존 `toUtcIso()`를 재사용해 `releaseDate`/`date`만 UTC로 직렬화한다. - [x] **CONTRACT TEST:** 생성 `releaseDate` 생략·null·정상 UTC, 상세 미래 UTC·현재/과거 null, root/reply UTC `date`, `totalCount/items`, page/size와 target/owner/block/secret 필터가 기존 의미를 유지하는지 확인한다. - [x] **REFACTOR:** legacy/public controller·DTO·timezone 동작을 변경하지 않고 v2 경계의 최소 분기만 남긴다. content/common 및 직접 영향 legacy 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest ./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 \ --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 36 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 34 and ([$operations[] | select(.["x-implementation-status"] == "alignment-required")] | length) == 2 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 3 UTC 날짜 계약 Gate **Goal 실행 `P3-R14-GATE`:** `REV-050`의 오디오 생성·상세·댓글 UTC 계약과 legacy/public 격리를 재검토한다. - [x] **`P3-R14-GATE` 완료:** `P3-R14` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R14` 완료. - **완료 증거:** 오디오 4개 operation의 timezone 제거·UTC `releaseDate`/`date`, 기존 null/노출·pagination·ownership 및 legacy/public 계약 회귀 성공, OpenAPI 해당 4개 operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API schema 변경. - **`P3-R14` / `P3-R14-GATE` 검증(2026-07-29):** RED는 create/query/comment focused 명령에서 48개 중 9개가 기존 `timezone` 필수·legacy 날짜 포맷 차이로 실패해 `BUILD FAILED in 1m 1s`였다. v2 전용 생성 DTO와 UTC 내부 생성 경계, 세 GET query 제거·거부, `toUtcIso()` 응답 mapping 후 같은 focused 명령은 `BUILD SUCCESSFUL in 2m 13s`였다. content/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 23s`였다. OpenAPI 36개 operation 상태는 `implemented` 34개와 `alignment-required` 2개를 확인했고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 v2 audio/content-common 및 legacy service 회귀가 직접 영향 범위를 포함하므로 생략했다. #### Phase 3 pagination 계약 후속 보완 - [x] **Task 3.25: 오디오 댓글·답글 목록의 optional page/size 기본값 복구** **Goal 실행 `P3-R15`:** OpenAPI 공통 `Page`, `Size` 계약대로 오디오 댓글·답글 목록에서 `page`, `size` 생략과 부분 생략을 허용하고 각각 `0`, `20`을 적용한다. - **추적 review ID:** `REV-052`. - **시작 조건:** `P2-R10-GATE`, `P3-R14-GATE` 완료와 OpenAPI의 optional `Page`/`Size` 계약 존재. - **완료 증거:** 두 actual GET에서 query 전체 생략·`page`만 지정·`size`만 지정 시 200과 기본값이 적용되고, 음수 page·1 미만 size·미지 query는 400이며 기존 pagination·UTC date·ownership/filter 결과가 유지된다. - **범위 밖:** OpenAPI pagination schema 변경, FanTalk 보정 정책 적용, 댓글 mutation·legacy/public API 변경, 신규 dependency·DDL. **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` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCommentTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 댓글·답글 GET에서 query 전체 생략과 한쪽만 지정한 요청이 현재 400인 것을 actual endpoint로 고정한다. - [x] **GREEN:** controller의 `page`, `size`에 OpenAPI 기본값을 적용하고 facade의 query 이름 검증은 미지 parameter만 거부하도록 최소 수정한다. - [x] **CONTRACT TEST:** 전체·부분 생략, 유효 page/size, 음수 page, 1 미만 size, `timezone` 등 미지 query와 기존 UTC exact JSON을 확인한다. - [x] **REFACTOR:** 다른 목록 API와 legacy/public pagination은 변경하지 않고 content/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 3 pagination 계약 후속 Gate **Goal 실행 `P3-R15-GATE`:** `REV-052` 수정 뒤 두 GET의 optional pagination과 미지 query 거부 경계를 재검토한다. - [x] **`P3-R15-GATE` 완료:** `P3-R15` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R15` 완료. - **완료 증거:** 두 operation의 기본값·부분 생략·범위 오류·미지 query 및 기존 UTC/ownership 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. - **`P3-R15` / `P3-R15-GATE` 검증(2026-07-29):** RED는 `AiCharacterAdminAudioContentCommentTest` 9건 중 댓글·답글의 전체 생략 `isOk` 기대 2건이 각각 실패해 `BUILD FAILED in 45s`였다. controller의 두 목록 query에 `page=0`, `size=20` 기본값을 적용하고 facade가 known query 이름의 부분집합을 허용하도록 수정한 뒤 같은 focused 명령은 `BUILD SUCCESSFUL in 40s`였다. content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는 controller/facade와 해당 actual endpoint test만 변경했고 직접 영향 범위 회귀가 이를 포함하므로 실행하지 않았다. #### Phase 3 multipart request part 계약 후속 보완 - [x] **Task 3.26: 오디오 생성·수정 request part의 application/json 강제** **Goal 실행 `P3-R16`:** 오디오 생성·수정 multipart의 `request` part가 OpenAPI encoding대로 `application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다. - **추적 review ID:** `REV-056`. - **시작 조건:** `P3-R15-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재. - **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·file/series/UTC 의미를 유지하고, `text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header, S3·DB·processing/event no-side-effect를 반환한다. - **범위 밖:** JSON schema·strict reader·file empty 정책, series/UTC 의미, legacy/public endpoint, OpenAPI·신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - Test: `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: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 현재 200을 기대하는 `text/plain` request part 수정 테스트를 OpenAPI의 415/no-side-effect 계약으로 교정하고 생성·수정 actual endpoint에서 재현한다. - [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String strict reader에 동일 payload를 전달한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON, 필수 part 누락 400 및 기존 UTC/file/series 회귀를 확인한다. - [x] **REFACTOR:** content facade/domain 로직을 변경하지 않고 content/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```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 ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 3 multipart request part 계약 후속 Gate **Goal 실행 `P3-R16-GATE`:** `REV-056` 수정 뒤 AudioContent POST·PUT의 part-level JSON-only·415 경계를 재검토한다. - [x] **`P3-R16-GATE` 완료:** `P3-R16` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R16` 완료. - **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 UTC/file/series 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. - **`P3-R16` / `P3-R16-GATE` 검증(2026-07-29):** RED는 새 POST·PUT의 `text/plain`·content type 누락 KO/EN/JA 415 기대와 기존 controller의 text/plain 성공 기대를 포함해 focused 83건 중 13건이 200 응답으로 실패해 `BUILD FAILED in 1m 15s`였다. controller의 request multipart header만 `application/json` 호환 여부를 확인하도록 하고 기존 strict String reader와 facade를 그대로 둔 뒤 focused는 `BUILD SUCCESSFUL in 56s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 16s`였다. OpenAPI 두 AudioContent multipart encoding은 `application/json`으로 정적 대조했고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 v2 AudioContent controller와 실제 endpoint test에 변경을 한정했고 content/common 영향 범위 회귀가 이를 포함하므로 실행하지 않았다. #### Phase 3 multipart part 이름 계약 후속 보완 - [x] **Task 3.27: 오디오 생성·수정의 미정의 multipart part 거부** **Goal 실행 `P3-R17`:** AudioContent 생성은 `contentFile`, `coverImage`, `request`, 수정은 `coverImage`, `request` 외 multipart part를 business mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-061`. - **시작 조건:** `P2-R11-GATE` 완료와 `phase3-audio-content-review.md` 13차 정적 리뷰 판정 존재. - **완료 증거:** POST·PUT의 미정의 part 400 KO/EN/JA envelope와 S3·DB·processing/event no-side-effect, 수정의 기존 `audioFile`·`contentFile` 거부 및 정상/필수 part/415 회귀. - **범위 밖:** OpenAPI schema, file empty·UTC·series 의미, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성·수정에 `unexpected` part를 추가해 현재 정상 mutation으로 진행되는 경로와 side effect를 actual endpoint로 고정한다. - [x] **GREEN:** 실제 part 이름 집합을 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}`와 비교해 초과 이름을 공통 400으로 거부한다. 수정 controller의 기존 `audioFile`·`contentFile` 인자는 제거하고 같은 미정의 part 검증으로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수 part 누락, 빈 파일, request part 415, 수정 파일 교체 미지원과 no-side-effect를 확인한다. - [x] **REFACTOR:** content controller/test만 최소 변경하고 content/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. **처리 기록 (2026-07-29 / P3-R17):** - RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects'` → 새 테스트 6개가 400 기대 대비 기존 정상 mutation 경로로 실패. - GREEN/focused: 동일 focused 명령 재실행 → `BUILD SUCCESSFUL in 1m 33s`. - 파일 교체 회귀: `audioFile`, `contentFile` 수정 part를 `shouldRejectFileReplacementPartBeforeSideEffects`로 통합 확인, focused 재실행 → `BUILD SUCCESSFUL in 1m 59s`. - 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 27s`. - 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 31s`; `git diff --check` → 출력 없음. - OpenAPI 대조: `api-contract.openapi.json`의 AudioContent 생성 schema는 required `{contentFile, coverImage, request}`, 수정 schema는 `{coverImage, request}` 및 `additionalProperties: false` 유지 확인. #### Phase 3 multipart part 이름 계약 후속 Gate **Goal 실행 `P3-R17-GATE`:** `REV-061` 수정 뒤 AudioContent POST·PUT의 exact part 이름과 기존 파일·media type 경계를 재검토한다. - [x] **`P3-R17-GATE` 완료:** `P3-R17` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R17` 완료. - **완료 증거:** 미정의 part 400/no-side-effect, 정상·필수·빈 파일·415·수정 교체 미지원 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. **Gate 기록 (2026-07-29 / P3-R17-GATE):** - Focused: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests '*shouldRejectFileReplacementPartBeforeSideEffects'` → `BUILD SUCCESSFUL in 1m 30s`. - 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 14s`. - 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 1s`; `git diff --check` → 출력 없음. - 판정: AudioContent POST·PUT exact multipart part 이름과 기존 필수/빈 파일/request 415/파일 교체 미지원 회귀가 모두 통과해 Phase 3 완료. #### Phase 3 multipart 일반 form-field part 후속 보완 - [x] **Task 3.28: 오디오 콘텐츠 생성·수정의 전체 multipart part 이름 검증** **Goal 실행 `P3-R18`:** AudioContent POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한 모든 multipart part 이름을 검사해 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 이름을 mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-066`. - **시작 조건:** `P2-R12-GATE` 완료와 `phase3-audio-content-review.md` 8차 정적 리뷰 판정 존재. - **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·processing·event no-side-effect, 기존 파일형 미정의 part·수정 파일 교체 거부·필수/빈 파일·request part 415 회귀 성공. - **범위 밖:** OpenAPI schema, 파일 교체 지원, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재 `fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다. - [x] **GREEN:** servlet request의 전체 part 이름 집합을 operation별 허용 집합과 비교해 초과 이름을 facade 진입 전에 공통 400으로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 수정 파일 교체 거부, 정상·필수·빈 파일, request part 415와 no-side-effect를 확인한다. - [x] **REFACTOR:** AudioContent controller/test만 최소 변경하고 content/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 3 multipart 전체 part 이름 후속 Gate **Goal 실행 `P3-R18-GATE`:** `REV-066` 수정 뒤 AudioContent POST·PUT의 파일·일반 form-field를 포함한 전체 part 이름과 기존 파일·media type 경계를 재검토한다. - [x] **`P3-R18-GATE` 완료:** `P3-R18` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3 리뷰와 Progress를 갱신한다. - **시작 조건:** `P3-R18` 완료. - **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` - 검증 기록(RED): 무엇: AudioContent POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다. - 검증 기록(GREEN): 무엇: AudioContent multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다. - 검증 기록(GATE): content/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다. #### Phase 3 15차 리뷰 보완 - [x] **Task 3.29: v2 오디오 생성 preview 시간 검증 복구** **Goal 실행 `P3-R19`:** `REV-072`에 따라 v2 오디오 생성도 기존 creator 생성과 동일하게 `previewStartTime`·`previewEndTime`의 쌍, 형식, 최소 15초 규칙을 파일 업로드와 DB·event 부작용 전에 검증한다. - **추적 review ID:** `REV-072`. - **시작 조건:** Phase 1~7 9차 정적 리뷰 판정과 `phase3-audio-content-review.md`의 `REV-072` 근거 존재. - **완료 증거:** v2 actual endpoint의 한쪽만 입력, 잘못된 형식, 15초 미만 RED와 `content.error.preview_time_both_required`·`content.error.preview_time_format`· `content.error.preview_time_minimum` 오류 계약, DB/S3/event 0건, 정상 preview 및 legacy/public 회귀, `ktlintCheck`, `git diff --check`, Progress 기록. - **범위 밖:** preview 규칙·오류 key 변경, OpenAPI field/schema 변경, upload/processing pipeline 변경, 관련 없는 `AudioContentService` refactor. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 나머지 필드는 유효한 actual endpoint 생성 request에 preview 시작만 입력, 형식 오류, 15초 미만 구간을 각각 보내 현재 200과 DB/S3/event 부작용이 발생하는 경로를 고정한다. - [x] **GREEN:** 문자열 request overload에만 있던 `validatePreviewTime` 호출을 두 생성 경로가 공유하는 parsed request overload로 이동해 legacy와 v2가 검증을 정확히 한 번 수행하도록 한다. - [x] **CONTRACT TEST:** KO/EN/JA의 세 기존 오류 key와 no-side-effect, 정상 15초 이상 preview의 metadata를 actual endpoint 및 service 단위에서 확인한다. - [x] **REFACTOR:** 공유 검증 호출 위치만 최소 변경하고 v2 content 및 legacy creator content 영향 범위 회귀, `ktlintCheck`, `git diff --check` 결과를 Progress에 기록한다. - 검증 기록: 무엇: v2 오디오 생성 preview 시간 검증을 기존 creator 생성과 동일한 공유 parsed request overload로 복구했다. 왜: `REV-072`처럼 v2 경로가 문자열 request overload의 검증을 우회해 잘못된 preview 입력이 DB/S3/event 경계로 진행될 수 있었기 때문이다. 어떻게: invalid preview actual endpoint 9건은 production 변경 전 400 기대 대비 200/부작용 경로로 실패했고, 테스트 JSON 조립 오류 수정 후 `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime`을 이동했다. 결과: `./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 preview 시간 검증 후속 Gate **Goal 실행 `P3-R19-GATE`:** `REV-072` 수정 뒤 v2·legacy 생성의 preview 검증과 부작용 순서를 재판정한다. - [x] **`P3-R19-GATE` 완료:** `P3-R19` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3 리뷰·finding 상태·Progress를 갱신한다. - **시작 조건:** `P3-R19` 완료. - **완료 증거:** 세 preview 오류 계약과 DB/S3/event no-side-effect, 정상 preview 및 legacy/public 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \ --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \ --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test' ./gradlew ktlintCheck git diff --check ``` - Gate 검증 기록: 무엇: `REV-072` 수정 뒤 Phase 3 content와 legacy AudioContent 영향 범위를 재검증했다. 왜: 공유 service overload 변경이 v2 actual endpoint와 legacy creator 생성 경로를 동시에 통과해야 하기 때문이다. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'`, `./gradlew ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 22s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 32s`, `git diff --check`는 출력 없이 통과했다. --- ### Phase 2·3 후속: 레거시 JSON 계약 정합화 기존 Phase 2·3 완료 이력은 보존한다. 2026-07-28 확정된 레거시 필드명·전체 payload 이관 정책에 따라 문서 계약을 먼저 고정하고, 현재 구현된 캐릭터·오디오 콘텐츠 v2 DTO와 endpoint를 별도 후속 Goal에서 정합화한다. - [x] **Task 3.16: 전체 API OpenAPI 계약 고정** **Goal 실행 `P23-CONTRACT-1`:** 23개 endpoint의 request/response를 레거시 DTO 전체 필드와 직접 대조해 OpenAPI 3.1 JSON과 설명 문서로 고정한다. - **시작 조건:** 사용자 확정 정책과 시리즈 미연결 콘텐츠 검색 endpoint 분리 결정. - **완료 증거:** JSON 문법·OpenAPI lint/validate·TypeScript client 생성 및 `tsc --noEmit` 성공, 23개 operation과 누락 `$ref` 0건, `./gradlew tasks --all` 성공 및 검증 기록. - **범위 밖:** production DTO/controller/test 수정, legacy/public endpoint 변경. - **TDD 예외 사유:** 실행 코드를 변경하지 않는 계약 문서 작성 Task다. - **대체 검증 방법:** 레거시 Kotlin DTO의 생성자 필드와 OpenAPI schema를 대조하고 두 validator와 client generator로 기계 검증한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - [x] 캐릭터, 테마·오디오 콘텐츠, 시리즈·연결 콘텐츠 검색, 커뮤니티, FanTalk의 레거시 DTO 전체 필드를 schema로 옮긴다. - [x] path로 이동한 ID만 body에서 제거하고 나머지 query/body/response 필드명은 레거시와 동일하게 유지한다. - [x] 레거시 mutation의 `data: null`, 오디오 생성의 `data.contentId`, FanTalk 답변 축약 응답 예외를 operation별로 고정한다. - [x] FanTalk 관리자 목록과 시리즈 미연결 콘텐츠 검색을 별도 operation으로 포함해 총 23개 endpoint를 검증한다. - [x] OpenAPI lint/validate, TypeScript Fetch client 생성과 `tsc --noEmit`을 실행하고 결과를 기록한다. - [x] **Task 3.17: Phase 2 캐릭터 runtime 계약 정합화** **Goal 실행 `P23-CONTRACT-2`:** 현재 구현된 캐릭터 4개 endpoint를 `api-contract.openapi.json`의 레거시 필드명·전체 request/response·mutation 응답에 맞춘다. - **시작 조건:** `P23-CONTRACT-1` 완료. - **완료 증거:** 4개 actual endpoint의 exact JSON field/required/nullable/multipart/`data: null` RED/GREEN, Phase 2 focused·legacy 회귀와 Progress 기록. - **범위 안:** `isActive=false`와 다른 optional JSON field의 동시 입력 허용 및 나머지 JSON field 미반영이라는 레거시 request 의미 복구. - **범위 밖:** 외부 API·ownership·soft delete persistence 결과 변경, 신규 character business behavior. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.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/character/LegacyChatCharacterAdminCharacterizationTest.kt` - [x] **RED:** 목록 `content`, 상세 전체 nested 필드, 생성 전체 request와 필수 `image`, 수정 optional 필드, `isActive=false`와 다른 optional JSON field의 동시 입력·미반영, 생성·수정 `data: null` exact JSON 테스트를 작성해 현재 v2 축약/변환 DTO와의 불일치를 확인한다. - [x] **GREEN:** business pipeline을 바꾸지 않는 최소 DTO/controller/facade/mapper 변경으로 OpenAPI 계약을 통과시킨다. - [x] **REFACTOR:** Phase 2 actual endpoint와 legacy characterization, 공통 오류 계약을 회귀하고 결과를 기록한다. ```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 ``` - 검증 기록(RED): 무엇: 캐릭터 4개 actual endpoint의 레거시 list/detail/multipart/mutation/soft-delete 계약. 왜: 현재 v2 DTO와 mutation 응답 및 단독 soft-delete 제한이 OpenAPI 원본과 다른지 실제 실패로 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: test compile은 성공했고 59건 중 계약 불일치 9건이 의도한 assertion에서 실패해 `BUILD FAILED`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: 레거시 DTO 재사용, 필수 생성 image, exact `data: null`, mixed JSON soft-delete와 character/common 회귀. 왜: business pipeline을 유지하면서 runtime 경계만 계약 원본에 맞추고 legacy·인가·오류 회귀를 방지하기 위해. 어떻게: 위 focused 명령을 먼저 실행한 뒤 Task의 character/authorization/error 회귀 명령과 `./gradlew ktlintCheck`를 fresh 실행했다. 결과: focused 59건은 `BUILD SUCCESSFUL in 2m 5s`, 지정 회귀 6개 suite 170건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 2m 36s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 48s`였다. - 검증 기록(독립 리뷰 보완): 무엇: OpenAPI `size` minimum 1과 mixed soft-delete의 미존재 `originalWorkId` 무시 계약. 왜: 독립 코드 리뷰에서 기존 20..50 보정이 Character list 계약과 다르고 soft-delete 검증 경계를 더 직접 고정할 필요가 확인됐기 때문이다. 어떻게: `size=1`에서 두 row 중 `content` 한 건만 반환하는 RED와 `isActive=false` + `originalWorkId=999999` 성공 assertion을 추가했다. 결과: focused 60건 중 pagination 1건이 RED로 실패했고 soft-delete 강화분은 통과했다. size 하한만 1로 바꾼 뒤 focused 60건은 `BUILD SUCCESSFUL in 2m 20s`, 지정 회귀 6개 suite 171건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 2m 21s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다. - [x] **Task 3.18: Phase 3 오디오 콘텐츠 runtime 계약 정합화** **Goal 실행 `P23-CONTRACT-3`:** 현재 구현된 테마·오디오 콘텐츠 5개 endpoint를 `api-contract.openapi.json`의 레거시 필드명·전체 request/response·성공 응답에 맞춘다. - **시작 조건:** `P23-CONTRACT-2` 완료. - **완료 증거:** 5개 actual endpoint의 exact query/multipart/JSON schema RED/GREEN, Phase 3 focused·legacy 회귀와 Progress 기록. - **범위 밖:** upload/processing pipeline, signed URL 정책, series 연결 behavior 변경. **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/AiCharacterAdminAudioContentDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.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/AiCharacterAdminAudioContentCreateTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt` - Confirm: `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/LegacyCreatorAdminAudioContentCharacterizationTest.kt` - [x] **RED:** 테마 `id/theme/image`, 목록 전체 item, 상세 전체 nested DTO, 생성 `contentFile`과 `CreateAudioContentRequest`, 수정 `UpdateCreatorAdminContentRequest` 및 각 성공 `data` 형태를 exact JSON으로 고정한다. - [x] **GREEN:** 기존 business pipeline과 signed URL 계산을 유지하는 최소 DTO/controller/facade/mapper adapter를 구현한다. - [x] **REFACTOR:** Phase 3 actual endpoint·legacy characterization·ownership/error 계약을 회귀하고 결과를 기록한다. - 검증 기록(RED): 무엇: 테마 `id/theme/image`, 목록 `search_word`와 전체 legacy item, 상세 필수 `timezone`과 전체 nested DTO, 생성 `contentFile`·`CreateAudioContentRequest`·`data.contentId`, 수정 `UpdateCreatorAdminContentRequest`·`data: null`. 왜: 현재 v2 alias와 mutation 상세 응답이 확정 OpenAPI 계약과 다른 상태를 실제 실패로 고정하기 위해. 어떻게: 지정된 theme, query, controller, create, update 5개 test class를 production 변경 전에 실행했다. 결과: test compile은 성공했고 61건 중 계약 불일치 21건이 의도한 assertion에서 실패해 `BUILD FAILED in 2m 17s`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: legacy DTO·목록 service 재사용, owner-scoped 상세 mapping, 기존 create/update service 위임, series 연결 보존, ownership·legacy·authorization·error 회귀. 왜: upload/processing·signed URL·ownership 의미는 유지하면서 HTTP 경계만 확정 계약에 맞추기 위해. 어떻게: focused 5개 class를 먼저 실행한 뒤 Task에 명시된 content 전체와 authorization/error 명령 및 `./gradlew ktlintCheck`를 fresh 실행했다. 결과: focused 61건은 `BUILD SUCCESSFUL in 3m 27s`, 최종 10개 suite 206건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 1m 43s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 16s`였다. - 검증 기록(Important review 보완): 무엇: PUT에 계약 밖 `audioFile` 또는 `contentFile` part가 있으면 400으로 거부하고 AudioContent·SeriesContent, S3, event를 변경하지 않는 계약. 왜: `contentFile`은 controller에 bind되지 않아 create-style part를 보낸 PUT이 파일을 무시한 채 200 `data: null`로 처리됐기 때문이다. 어떻게: 기존 `audioFile` no-side-effect test를 두 part parameterized test로 확장하고 controller/facade에 optional `contentFile` binding과 공동 guard만 추가했다. 결과: RED는 2건 중 `contentFile` 1건만 실패해 `BUILD FAILED in 30s`, GREEN은 2건 모두 `BUILD SUCCESSFUL in 33s`였다. content·authorization·error 10개 suite 207건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 1m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 32s`였다. ```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 2·3 계약 정합화 Gate **Goal 실행 `P23-CONTRACT-GATE`:** 문서 계약과 구현된 9개 endpoint의 runtime 응답이 일치하고 Phase 4가 같은 계약을 소비할 수 있는지 판정한다. - [x] **`P23-CONTRACT-GATE` 완료:** `P23-CONTRACT-1`~`P23-CONTRACT-3` 완료 후 character/content focused·legacy 회귀, OpenAPI validate/client 생성과 `ktlintCheck`를 fresh 실행한다. - **범위 밖:** Gate에서 직접 production code 수정, Phase 4 이후 기능 구현. ```bash ./gradlew test --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 ./gradlew ktlintCheck npx --yes @openapitools/openapi-generator-cli validate \ -i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json npx --yes @openapitools/openapi-generator-cli generate \ -i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \ -g typescript-fetch \ -o /tmp/ai-character-admin-typescript-client ``` - 검증 기록: 무엇: `P23-CONTRACT-GATE` runtime/API 계약 Gate. 왜: `P23-CONTRACT-1`~`P23-CONTRACT-3` 완료 후 Phase 4가 소비할 Character·AudioContent 9개 endpoint의 runtime 응답과 OpenAPI 계약이 함께 유효한지 확인하기 위해. 어떻게: 위 네 Gate 명령을 fresh 실행했다. 결과: character/content/common 회귀는 `BUILD SUCCESSFUL in 4m 59s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 45s`, OpenAPI validate는 `No validation issues detected.`, TypeScript Fetch client 생성은 `/tmp/ai-character-admin-typescript-client`에 성공했다. --- ### Phase 4: 시리즈 관리 vertical slice #### 목표 선택한 AI 캐릭터 소유 시리즈 CRUD, soft delete, 콘텐츠 연결/해제/검색/순서 관리를 owner-safe v2 경로로 제공한다. #### 범위와 비범위 - 포함: 시리즈 목록/상세/생성/수정/soft delete, 콘텐츠 연결/해제, 시리즈 콘텐츠 조회/검색, owner-scoped 순서 변경, 기존 creator series behavior parity 특성화. - 제외: 기존 `CreatorAdminContentSeriesController.updateSeriesOrders(ids)` 계약 변경. #### 선행 Phase 및 의존성 - Phase 1 target resolver. - Phase 3 content owner query를 재사용할 수 있다. - 기존 creator series 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior를 통과하는 특성화 테스트로 먼저 고정해야 한다. #### API endpoint와 request/response contract - 정식 전체 schema는 `api-contract.openapi.json`의 Series operation 10개를 따른다. - `GET /api/v2/admin/ai-characters/series-genres`는 활성 장르의 `id`, `genre`, `isAdult` 직접 배열을 반환한다. - `GET /series/{seriesId}`의 `data`는 목록 `items`의 단일 객체와 동일한 11개 필드·타입을 반환한다. - `GET/POST/PUT /api/v2/admin/ai-characters/{characterId}/series...` - `GET /series/{seriesId}/contents` query: `page`, `size`; response: `GetCreatorAdminContentSeriesContentResponse(totalCount, items)` - `GET /series/{seriesId}/contents/search` query: 필수 `search_word`; response: `List` - `POST /series/{seriesId}/contents` request: `AddingContentToTheSeriesRequest(contentIdList: List)` - `DELETE /series/{seriesId}/contents/{contentId}` request body 없음 - `PUT /series/orders` request: `UpdateOrdersRequest(ids: List)` #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: `Series`와 `SeriesContent` owner-scoped query/update adapter 추가. - Service: 신규 series application service에서 기존 CRUD 핵심을 테스트 후 재사용하되, 모든 write 전에 series/content owner를 검증한다. #### DB migration - 없음. #### transaction과 concurrency 고려사항 - 순서 변경은 동일 owner의 모든 series ID를 한 transaction에서 검증 후 갱신한다. - 콘텐츠 연결/해제는 series와 content owner를 모두 검증한 뒤 수행한다. - 동시에 순서 변경 요청이 들어오면 마지막 transaction 결과가 반영되는 기존 단순 정책을 유지하되 cross-owner 갱신은 절대 허용하지 않는다. #### 보안 및 개인정보 위험 - ID-only order update로 다른 creator series를 변경하지 못해야 한다. - 연결 가능한 content 검색은 target owner 범위로 제한한다. #### acceptance criteria - 기존 creator series의 CRUD, soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior parity가 특성화 테스트로 고정된다. - 모든 시리즈/콘텐츠 ID는 target creatorMember 소유일 때만 변경된다. - soft delete는 `isActive=false`이며 활성 조회에서 제외된다. - 기존 owner-less order update 취약 경로가 신규 v2 API에는 없다. #### targeted test - Characterization: `LegacyCreatorAdminSeriesCharacterizationTest`. - V2 RED/GREEN: `AiCharacterAdminSeriesServiceTest`. - Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'` #### 전체 회귀 테스트 영향 - 기존 creator-admin series endpoint 계약은 유지한다. - 기존 series query/order 테스트가 있으면 함께 실행한다. #### rollback 전략 - 신규 series v2 admin route/facade를 제거한다. - 기존 data model 변경이 없으므로 schema rollback은 없다. #### 권장 commit 경계 - `feat: add ai character admin series slice` - [x] **Task 4.1: 기존 series parity 특성화 baseline 고정** **Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다. - **시작 조건:** Phase 2·3 runtime 계약 정합화의 `P23-CONTRACT-GATE` 완료와 사용자 진행 지시. - **완료 증거:** production 변경 전 특성화 테스트 통과, 관찰된 오류·side-effect 정책과 Progress 기록. - **범위 밖:** 신규 v2 series production code 구현. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/LegacyCreatorAdminSeriesCharacterizationTest.kt` - [x] 목록·상세·생성·수정·soft delete와 inactive 조회 baseline test를 작성한다. - [x] 콘텐츠 연결·해제·검색과 순서 변경의 결과·검증·side effect를 고정한다. - [x] Phase 4 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다. - [x] production code 변경 없이 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다. - [x] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다. - **관찰된 legacy baseline:** creator 목록은 owner의 활성 시리즈만 `orders` 오름차순으로 반환하지만 owner 상세는 `isActive=false`도 반환하고, 전역 admin의 `findByIdAndActiveTrue`는 inactive를 제외한다. 생성은 DB save → S3 upload → language detect event 순이며 키워드를 `#` prefix 기준으로 중복 제거한다. 수정은 일반 field와 `isActive=false`를 한 요청에서 모두 반영하고 title/introduction 변경 시 translation event를 발행한다. - **관찰된 연결·검색·순서 baseline:** 콘텐츠 연결은 owned ID만 부분 반영하고 foreign/missing ID를 건너뛰며 전부 무효일 때만 `creator.admin.series.no_content_added`를 던진다. 해제할 link가 없으면 no-op이다. 연결 목록 `totalCount`는 target series가 아닌 owner의 활성 시리즈 전체 link 수이고, missing series 조회도 이 count와 빈 items를 반환한다. 미연결 검색은 processed 또는 reserved owner content만 반환하지만 series 존재·owner를 검증하지 않는다. 순서 변경은 owner·active를 검증하지 않고 존재하는 ID만 요청 index + 1로 갱신하므로 foreign/inactive도 변경되고 missing ID 자리는 순번 gap으로 남는다. - **legacy 오류 표면:** creator-admin security 실패는 401/403 `sendError`이고, controller 이후 `SodaException`은 HTTP 200 `ApiResponse.error`로 노출된다. 아래 Phase 4 v2 결정은 이를 복제하지 않고 신규 prefix의 비2xx envelope 정책을 따른다. - **Phase 4 v2 domain/client 오류 결정:** target/series/content 미존재·inactive·cross-owner, 존재하지 않는 양수 genre, 잘못된 pagination, malformed request, 중복·누락·foreign/inactive order ID, 이미 연결된 content와 없는 link 해제는 mutation 전 400 `common.error.invalid_request`로 실패하고 DB/S3/event side effect는 0건이어야 한다. 빈 `contentIdList` 또는 legacy 규칙상 추가 가능한 ID가 0개인 비소유권 입력은 400 `creator.admin.series.no_content_added`를 유지한다. create/update의 legacy 입력 validation key도 아래 표처럼 400으로 유지한다. 예상하지 못한 DB/S3/event 오류는 500 `common.error.unknown`을 사용하며, transaction DB 변경과 미발행 event는 rollback하지만 이미 성공한 S3 upload는 legacy에 삭제 계약이 없어 보상하지 않는다. | Phase 4 domain/client 경우 | status | message key | KO | EN | JA | |---|---:|---|---|---|---| | missing/inactive/cross-owner resource, pagination·binding·order/link 검증 실패 | 400 | `common.error.invalid_request` | 잘못된 요청입니다. | Invalid request. | 無効なリクエストです。 | | 생성 title 공백 | 400 | `creator.admin.series.title_required` | 시리즈 제목을 입력하세요 | Please enter a series title. | シリーズのタイトルを入力してください。 | | 생성 introduction 공백 | 400 | `creator.admin.series.introduction_required` | 시리즈 소개를 입력하세요 | Please enter a series introduction. | シリーズ紹介を入力してください。 | | 생성 keyword 공백 | 400 | `creator.admin.series.keyword_required` | 시리즈를 설명할 수 있는 키워드를 입력하세요 | Please enter keywords that describe the series. | シリーズを説明できるキーワードを入力してください。 | | 생성 genre ID 0 이하 | 400 | `creator.admin.series.genre_required` | 올바른 장르를 선택하세요 | Please select a valid genre. | 正しいジャンルを選択してください。 | | 생성 published days 비어 있음 | 400 | `creator.admin.series.published_days_required` | 시리즈 연재요일을 선택하세요 | Please select publishing days. | シリーズの連載曜日を選択してください。 | | `RANDOM`과 특정 요일 혼합 | 400 | `creator.admin.series.published_days_random_exclusive` | 랜덤과 연재요일 동시에 선택할 수 없습니다. | You cannot select random and specific days at the same time. | ランダムと連載曜日を同時に選択することはできません。 | | 생성 cover image 누락 | 400 | `creator.admin.series.cover_image_required` | 커버이미지를 선택해 주세요. | Please select a cover image. | カバー画像を選択してください。 | | 수정 field와 image 모두 없음 | 400 | `creator.admin.series.no_changes` | 변경사항이 없습니다. | No changes to update. | 変更データがありません。 | | 추가 가능한 content ID 0개 | 400 | `creator.admin.series.no_content_added` | 추가된 콘텐츠가 없습니다. | No content was added. | 追加されたコンテンツがありません。 | | 예상하지 못한 server/infrastructure 오류 | 500 | `common.error.unknown` | 알 수 없는 오류가 발생했습니다. 다시 시도해 주세요. | An unknown error occurred. try again. | 不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。 | - 검증 기록: 무엇: creator-admin series CRUD/list/inactive, content link/unlink/count/search, owner-less order와 legacy 오류 key characterization. 왜: 신규 v2가 legacy JSON·domain 의미를 재사용하되 legacy의 owner-less·부분 성공·HTTP 200 오류 표면은 안전한 비2xx owner-first 계약으로 분리하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest`와 `./gradlew ktlintCheck`를 production 변경 없이 실행했다. 결과: focused test는 첫 실행부터 `BUILD SUCCESSFUL in 47s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 19s`였다. 전체 `./gradlew test`는 production 변경이 없고 지정 focused test가 실제 service/repository/S3/event 경계를 포함하므로 실행하지 않았다. - [x] **Task 4.2: 시리즈 목록·상세 조회 구현** **Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 레거시 response 계약으로 제공한다. - **시작 조건:** `P4-T1` 완료와 Phase 4 오류 계약의 계획 반영. - **완료 증거:** 목록·상세 전체 필드, inactive·cross-owner·pagination RED/GREEN과 Progress 기록. - **범위 밖:** 시리즈 mutation, 콘텐츠 연결, 순서 변경. **Files:** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesQueryTest.kt` - [x] 미구현 목록·상세, inactive 제외, cross-owner 거부와 pagination 경계 실패 test를 작성한다. - [x] owner-scoped 목록·상세 최소 구현으로 focused test를 통과시킨다. - [x] 목록 `totalCount/items`와 item 전체 필드, 상세의 문자열 `publishedDaysOfWeek/genre/keywords/state`를 `api-contract.openapi.json`과 exact JSON으로 검증한다. - [x] focused test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(RED): 무엇: v2 series 목록·상세 전체 legacy 필드, 활성 owner 범위, missing/inactive/cross-owner 상세와 `page=1&size=1`, 음수 page·0 size 경계. 왜: 신규 route 미구현과 Phase 4 오류 결정을 실제 HTTP 계약 실패로 고정하기 위해. 어떻게: production 파일 생성 전에 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesQueryTest`를 실행했다. 결과: test compile은 성공했고 5건 모두 기대 status 200/400 대신 미구현 404로 실패해 `BUILD FAILED in 44s`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: target resolver 선행, 활성 owner 목록·상세, exact legacy DTO, pagination과 legacy characterization 회귀. 왜: 기존 creator series 동작과 응답 형태는 재사용하면서 inactive·cross-owner 상세만 신규 400 계약으로 제한하기 위해. 어떻게: 같은 focused 명령, series package 회귀와 `./gradlew ktlintCheck`를 실행했다. 결과: focused 5건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 34s`, Task 4.1 포함 series 12건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다. - 검증 기록(독립 리뷰 보완): 무엇: inactive `ChatCharacter` target의 목록·상세 400 계약. 왜: resolver는 role/memberKind만 검증하므로 Phase 4의 target inactive 결정이 series facade에서 누락됐기 때문이다. 어떻게: inactive target 목록·상세 테스트를 추가해 focused 명령을 실행한 뒤 facade의 공통 active target guard를 적용하고 focused/series 회귀와 `ktlintCheck`를 재실행했다. 결과: 보완 RED는 7건 중 기존 5건은 통과하고 신규 2건만 400 기대 대비 200으로 실패해 `BUILD FAILED in 35s`였다. 보완 후 focused 7건은 `BUILD SUCCESSFUL in 44s`, Task 4.1 포함 series 14건은 `BUILD SUCCESSFUL in 49s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 23s`였다. - [x] **Task 4.3: 시리즈 생성·수정·soft delete 구현** **Goal 실행 `P4-T3`:** target owner의 시리즈 생성·수정·soft delete를 기존 creator parity로 제공한다. - **시작 조건:** `P4-T1`, `P4-T2` 완료. - **완료 증거:** CRUD RED/GREEN, ownership·inactive·no-side-effect와 legacy 회귀 기록. - **범위 밖:** 콘텐츠 연결·해제, 순서 변경, hard delete. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - [x] 생성·수정·soft delete와 cross-owner mutation 실패 test를 작성한다. - [x] owner 검증 후 최소 CRUD 구현으로 test를 통과시킨다. - [x] `isActive=false`와 활성 조회 제외, invalid target의 DB/event no-side-effect를 검증한다. - [x] focused/legacy test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(RED): 무엇: v2 series 생성·수정·DELETE soft delete, cross-owner·missing·inactive·no-change와 DB/S3/event 부작용 0건. 왜: mutation route 미구현과 owner-first 계약을 실제 HTTP 경계로 고정하기 위해. 어떻게: production 변경 전에 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`를 실행했다. 결과: 12건 모두 기대 200/400 대신 미구현 method의 405로 실패해 `BUILD FAILED in 51s`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: legacy 생성·수정 위임, path `seriesId` 조립, 활성 target/owned series 선검증과 null 성공 envelope. 왜: keyword/S3/genre/event/entity 갱신을 복제하지 않고 creator parity를 유지하기 위해. 어떻게: focused test, `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`, `./gradlew ktlintCheck`와 `git diff --check`를 실행했다. 결과: focused 12건과 series 회귀 26건은 failure/error/skipped 0, fresh 회귀는 `BUILD SUCCESSFUL in 4m 36s`, ktlint는 import 정렬 1건 수정 후 `BUILD SUCCESSFUL in 21s`, diff check는 오류가 없었다. - 검증 기록(독립 리뷰 보완): 무엇: 존재하지 않는 양수 `genreId` 생성·이미지 포함 수정 요청을 legacy 호출 전에 400으로 차단하고 DB/S3/event 부작용 0건을 보장했다. 왜: legacy 수정 path는 이미지 업로드 후 genre를 조회하므로 Phase 4의 mutation 전 검증 결정을 위반할 수 있었기 때문이다. 어떻게: 누락 genre 생성·수정 테스트 2건을 추가해 focused 14건 중 신규 2건만 RED로 실패함을 확인한 뒤 active genre 사전 guard를 추가했다. 결과: focused 14건은 `BUILD SUCCESSFUL in 35s`, series 회귀 28건은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 49s`, `git diff --check`는 오류가 없었다. - [x] **Task 4.4: 시리즈 콘텐츠 조회·검색·연결·해제 구현** **Goal 실행 `P4-T4`:** 동일 owner의 시리즈와 콘텐츠만 검색·연결·해제할 수 있도록 한다. - **시작 조건:** `P4-T2`, `P4-T3`와 Phase 3 owner query 계약 완료. - **완료 증거:** 연결 목록·미연결 검색의 분리된 응답, pagination·전체 ID 사전 검증·원자적 연결/해제 RED/GREEN과 Progress 기록. - **범위 밖:** 콘텐츠 자체 수정, 시리즈 순서 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt` - [x] `GET .../contents`와 `GET .../contents/search?search_word=...`의 별도 응답, 연결·해제와 cross-owner ID 실패 test를 작성한다. - [x] 모든 series/content ID를 mutation 전에 검증하는 최소 구현을 통과시킨다. - [x] `contentIdList` 필드와 mutation `data: null`, 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다. - [x] focused/Phase 3 owner query 회귀와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(RED): 무엇: 연결 목록·미연결 검색·pagination·원자적 연결/해제와 invalid series/content 경계. 왜: 신규 v2 route와 legacy 부분 성공/no-op을 owner-first 400 계약으로 바꾸기 위해. 어떻게: production 변경 전에 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest`를 실행했다. 결과: 7건 모두 미구현 route의 404/405로 기대한 200/400을 충족하지 못해 `BUILD FAILED in 54s`를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: legacy 목록/검색 DTO 재사용, `contentIdList` 전체 선검증, 연결/해제 `data: null`과 invalid 요청의 무변경 경계. 왜: legacy의 owner 전체 link count와 응답 형태는 유지하면서 foreign/missing/inactive/already-linked ID와 없는 link 해제의 부분 성공을 막기 위해. 어떻게: focused, Phase 4 series 회귀, Phase 3 content owner-query 회귀와 `ktlintCheck`를 실행했다. 결과: focused 7건은 `BUILD SUCCESSFUL in 1m 23s`, series 회귀는 `BUILD SUCCESSFUL in 1m 55s`, content 회귀는 `BUILD SUCCESSFUL in 1m 24s`, ktlint는 `BUILD SUCCESSFUL in 36s`였다. - [x] **Task 4.5: owner-scoped 시리즈 순서 변경 구현** **Goal 실행 `P4-T5`:** 요청된 모든 series ID의 owner를 먼저 검증한 뒤 한 transaction에서 순서를 변경한다. - **시작 조건:** `P4-T3` 완료. - **완료 증거:** 정상·중복/누락·cross-owner·동시 요청 RED/GREEN과 owner-less 경로 비사용 증거. - **범위 밖:** 기존 `CreatorAdminContentSeriesController.updateSeriesOrders(ids)` 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesOrderTest.kt` - [x] 정상 순서와 cross-owner ID-only 취약 경로를 재현하는 실패 test를 작성한다. - [x] 동일 owner 전체 검증 후 한 transaction에서 갱신하는 최소 구현을 통과시킨다. - [x] 검증 실패 시 update 0건과 동시 요청의 기존 last-transaction 정책을 확인한다. - [x] focused/legacy order test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(GREEN/REFACTOR): 무엇: v2 series order endpoint와 owner active series 선검증, duplicate/missing/cross-owner/inactive/empty ID 거부, 동시 순서 변경용 ID 오름차순 pessimistic lock, last-request 결과를 고정했다. 왜: legacy `updateSeriesOrders(ids)`는 owner-less로 요청 순서대로 row를 수정하므로 신규 v2 경계에서 owner 검증과 lock 순서를 먼저 보장해야 하기 때문이다. 어떻게: `AiCharacterAdminSeriesOrderTest` RED 후 `PUT /api/v2/admin/ai-characters/{characterId}/series/orders`를 추가하고, `CreatorAdminContentSeriesRepository.findActiveByCreatorIdAndIdInForUpdate`로 같은 transaction 안에서 대상 row를 선잠금한 뒤 legacy update를 재사용했다. 결과: focused order test는 `BUILD SUCCESSFUL in 34s`, Phase 4 series 회귀는 `BUILD SUCCESSFUL in 55s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 리뷰 재확인에서 blocking/important/minor finding 0건을 확인했다. - [x] **Task 4.6: Phase 4 보안·오류·회귀 검증** **Goal 실행 `P4-T6`:** 모든 series endpoint의 ADMIN·오류·ownership 계약과 legacy 회귀를 고정한다. - **시작 조건:** `P4-T2`~`P4-T5` 완료. - **완료 증거:** endpoint 권한 매트릭스, domain 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록. - **범위 밖:** Phase 5 community 기능. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContractTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - [x] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다. - [x] target/series/content/pagination 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다. - [x] invalid ownership의 DB/event side effect 0건과 legacy creator series 계약을 검증한다. - [x] Phase 4 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. #### Phase 4 Gate **Goal 실행 `P4-GATE`:** Phase 4 series 사용자 흐름과 ownership·회귀 품질을 최종 판정한다. - [x] **`P4-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P4-T1`~`P4-T6` 완료. - **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`와 `./gradlew ktlintCheck` 성공, Progress 기록. - **범위 밖:** 실패와 무관한 Phase 5 구현. #### Phase 4 후속 리뷰 보완 - [x] **Task 4.7: 시리즈 HTTP 경계를 OpenAPI 9개 operation과 정합화** **Goal 실행 `P4-R1`:** `REV-023`의 계약 밖 시리즈 DELETE endpoint를 제거하고 `REV-024`의 JSON body 두 곳에서 `additionalProperties: false`를 실제로 강제한다. - **추적 review ID:** `REV-023`, `REV-024`. - **시작 조건:** `P3-R9-GATE` 완료와 `phase4-series-review.md` 판정 존재. - **완료 증거:** `DELETE /series/{seriesId}`가 405이고 `PUT /series/{seriesId}`의 `isActive=false`가 soft delete를 담당하는 actual endpoint 테스트, 콘텐츠 추가·순서 변경 request의 미지 필드 400/no-side-effect 테스트, Series 9개 operation mapping 정적 대조와 Progress 기록. - **범위 밖:** OpenAPI operation 추가, legacy controller 변경, 시리즈 CRUD/ownership/lock 정책 변경, 다른 Phase JSON 경계. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesOrderTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `DELETE /series/{seriesId}` 405와 `PUT` `isActive=false` soft delete 성공을 actual endpoint로 고정한다. - [x] **RED:** 콘텐츠 추가와 순서 변경 JSON에 계약 밖 필드를 추가하면 400 `common.error.invalid_request`이고 DB/event 부작용이 0회임을 확인한다. - [x] **GREEN:** 계약 밖 DELETE controller/facade 경로를 제거하고 두 JSON body만 strict reader로 역직렬화한다. - [x] **REFACTOR:** OpenAPI Series operation 9개와 controller mapping을 대조하고 series/common 영향 범위 회귀, `ktlintCheck`, diff check를 실행한다. - 검증 기록(RED): 무엇: 계약 밖 `DELETE /series/{seriesId}` 제거 기대와 `PUT isActive=false` soft delete, 콘텐츠 추가·순서 변경 미지 필드 거부를 actual endpoint로 고정했다. 왜: `REV-023`~`REV-024`가 실제 실패를 내는지 확인하기 위해. 어떻게: 아래 focused series mutation/content/order 명령을 production 변경 전 실행했다. 결과: 28개 중 6개가 기존 DELETE 200 또는 unknown-field 성공 때문에 실패해 RED를 확인했다. - 검증 기록(GREEN): 무엇: `DELETE /series/{seriesId}` controller/facade 경로를 제거하고 content add/order JSON body를 facade strict reader로 파싱했다. 왜: OpenAPI 9개 operation과 `additionalProperties: false` 계약을 runtime에 맞추기 위해. 어떻게: 같은 focused 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 25s`였다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesOrderTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 후속 리뷰 Gate **Goal 실행 `P4-R1-GATE`:** `REV-023`~`REV-024` 수정 뒤 Series controller가 OpenAPI 9개 operation과 일치하는지 재검토한다. - [x] **`P4-R1-GATE` 완료:** `P4-R1` 완료 후 mapping 정적 대조와 series/common 회귀, lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P4-R1` 완료. - **완료 증거:** 계약 밖 DELETE 제거, 두 JSON body 미지 필드 거부, 9개 operation 일치, 영향 범위 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. - 검증 기록: 무엇: `P4-R1-GATE`에서 Series controller mapping과 회귀 품질을 재판정했다. 왜: `REV-023`~`REV-024` 처리 후 OpenAPI 9개 operation과 runtime JSON 경계가 일치하는지 확인하기 위해. 어떻게: `rg -n "@(Get|Post|Put|Delete)Mapping|fun delete\(|facade\.delete\(|@RequestBody request:" "src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series"`로 9개 mapping과 두 `@RequestBody String`, 삭제 facade 부재를 확인했고, series/common 회귀와 `ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 정적 대조는 9개 mapping만 출력했고 `DELETE /series/{seriesId}`와 `facade.delete`는 출력되지 않았다. 회귀 명령은 `BUILD SUCCESSFUL in 2m 19s`, 최초 `ktlintCheck`는 blank line 1건으로 실패했으나 포맷 수정 후 재실행은 `BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력이 없었다. #### Phase 4 2차 후속 리뷰 보완 - [x] **Task 4.8: 시리즈 필수 이미지와 연결 해제 경계 복구** **Goal 실행 `P4-R2`:** `REV-031`의 soft-delete 콘텐츠 연결 해제를 복구하고, `REV-032`의 생성 필수 `image` part를 공통 multipart binding 계약에 맞춘다. - **추적 review ID:** `REV-031`, `REV-032`. - **시작 조건:** `P4-R1-GATE` 완료와 `phase4-series-review.md` 2차 리뷰 판정 존재. - **완료 증거:** 연결 후 soft delete된 owner 콘텐츠 해제 성공, cross-owner/missing link 무변경, 생성 `image` 누락의 exact `MissingServletRequestPartException`·KO/EN/JA 400/no-side-effect RED/GREEN, series/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 콘텐츠 연결 추가·검색의 active/duration 적격성 변경, 빈 `image` 파일 정책 신설, legacy/public series controller 변경, OpenAPI schema 변경. - **계약 판정:** PRD API Expectations와 OpenAPI `SeriesCreateMultipart.required`를 우선한다. 기존 Phase 4 오류 표의 `creator.admin.series.cover_image_required`는 nullable legacy 전달을 기록한 과거 결정이며, 신규 관리자 endpoint의 누락 part는 `common.error.invalid_request`로 정정한다. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** owner 콘텐츠를 시리즈에 연결한 뒤 `isActive=false`, `releaseDate=null`로 soft delete하고 `DELETE .../contents/{contentId}`가 현재 400으로 실패하며 link가 남는지 확인한다. - [x] **RED:** 생성 `image` part 누락을 KO/EN/JA actual endpoint로 보내 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request`, facade/DB/S3/event 0회를 단언한다. - [x] **GREEN:** 해제는 실제 series link와 그 콘텐츠 owner만 검증하고, 연결 추가에만 필요한 active/release/duration 적격성 검사를 해제 경로에서 제거한다. - [x] **GREEN:** 생성 controller/facade의 `image`를 non-null `MultipartFile`로 바꾸고 검증된 파일을 legacy service에 그대로 전달한다. update의 optional `image`는 유지한다. - [x] **REFACTOR:** 정상 연결/해제, missing/cross-owner link, 생성 validation key와 series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다. - 검증 기록(RED): 무엇: soft-delete된 owner content의 기존 series link 해제와 생성 `image` 누락의 공통 binding 오류를 actual endpoint로 고정했다. 왜: `REV-031`~`REV-032`가 실제 runtime에서 실패하는지 확인하기 위해. 어떻게: 아래 focused mutation/content 명령을 production 변경 전 실행했다. 결과: soft-delete unlink 1건은 400, missing image KO/EN/JA 3건은 legacy message 기대 차이로 실패해 RED를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: unlink 경로에서 추가 적격성 guard를 제거하고 실제 owner link만 검증했으며, create `image` part를 non-null binding으로 변경했다. 왜: 연결 추가 조건과 기존 link 해제 조건을 분리하고 OpenAPI required part 계약을 MVC binding 단계에서 강제하기 위해. 어떻게: focused 명령, series/common 영향 범위 회귀, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: focused는 `BUILD SUCCESSFUL`, 영향 범위 회귀는 `BUILD SUCCESSFUL`, `ktlintCheck`는 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 2차 후속 리뷰 Gate **Goal 실행 `P4-R2-GATE`:** `REV-031`~`REV-032`의 unlink 상태 전이와 multipart 필수 part 계약을 재검토한다. - [x] **`P4-R2-GATE` 완료:** `P4-R2` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P4-R2` 완료. - **완료 증거:** 두 review ID 처리 완료, soft-delete 콘텐츠 unlink 성공, 필수 image 누락 exact 400, 기존 연결 추가·update optional image 계약 유지. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록: 무엇: `P4-R2-GATE`에서 `REV-031`~`REV-032` 처리 결과를 재판정했다. 왜: soft-delete linked content 해제, 필수 create image 누락, 기존 link 오류와 optional update image 계약이 동시에 유지되는지 확인하기 위해. 어떻게: focused mutation/content, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 모든 Gradle 명령이 `BUILD SUCCESSFUL`이고 diff check 출력이 없어 Phase 4 후속 리뷰를 완료로 판정했다. #### Phase 4 3차 리뷰 보완 - [x] **Task 4.9: 시리즈 빈 image의 0-byte 업로드 차단** **Goal 실행 `P4-R3`:** 시리즈 생성의 빈 필수 `image`를 부작용 전에 거부하고, 수정의 빈 optional `image`는 생략으로 정규화해 기존 커버를 유지한다. - **추적 review ID:** `REV-037`. - **시작 조건:** `P3-R11-GATE` 완료와 `phase4-series-review.md` 3차 정적 리뷰 판정 존재. - **완료 증거:** 생성 빈 image 400/no-side-effect, 수정 JSON+빈 image의 기존 커버 유지/S3 0회 actual endpoint RED/GREEN, series/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** OpenAPI schema 변경, 레거시 series service 변경, 정상 image 업로드 경로·파일 정책 확장. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 빈 필수 image로 시리즈를 생성하면 현재 0-byte S3 upload와 DB/event 부작용이 발생하는지 actual endpoint로 고정한다. - [x] **RED:** 유효한 수정 JSON과 빈 optional image를 함께 보내면 현재 0-byte cover로 교체되는지 고정한다. - [x] **GREEN:** create facade에서 `image.isEmpty`를 legacy 호출 전에 400 `common.error.invalid_request`로 거부하고, update의 빈 image는 null로 정규화해 legacy service에 전달한다. - [x] **CONTRACT TEST:** 빈 image만 있고 JSON 변경 필드가 없는 update는 기존 `no_changes` 400을 유지하며, 정상 image 생성·교체와 image 생략 수정은 그대로 동작하는지 확인한다. - [x] **REFACTOR:** series package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해 Progress와 리뷰 문서에 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 3차 리뷰 Gate **Goal 실행 `P4-R3-GATE`:** `REV-037`의 생성·수정 empty-file 정책과 기존 정상 upload 계약을 재검토한다. - [x] **`P4-R3-GATE` 완료:** `P4-R3` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P4-R3` 완료. - **완료 증거:** review ID 처리 완료, 생성 빈 image 400/no-side-effect, 수정 빈 image 생략, 정상 upload 회귀 성공. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록(RED): 무엇: 시리즈 생성·수정 empty image. 왜: empty multipart가 legacy service로 전달되어 0-byte S3/cover 변경을 유발하는지 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`를 실행했다. 결과: 신규 3건이 실패해 생성 400 미충족, 수정 cover 유지 미충족, empty-only no_changes 미충족을 확인했다. - 검증 기록(GREEN/GATE): 무엇: 생성 empty image 400/no-side-effect, 수정 empty image 생략, empty-only no_changes 유지. 왜: legacy service 공용 동작 변경 없이 v2 facade 경계만 보정하기 위해. 어떻게: 같은 focused 명령 재실행 후 targeted/전체/lint/OpenAPI/mapping/diff 검증을 실행했다. 결과: focused series mutation과 전체 검증이 모두 성공했다. #### Phase 4 5차 리뷰 보완 - [x] **Task 4.10: 시리즈 생성 primitive 필드의 명시적 null 거부** **Goal 실행 `P4-R4`:** 시리즈 생성의 non-null primitive `genreId`, `isAdult`에 명시적 null이 들어오면 JVM 기본값으로 보정하지 않고 S3·DB·event 전에 400으로 거부하며, 필드 생략 시 기존 기본값은 유지한다. - **추적 review ID:** `REV-042`. - **시작 조건:** `P3-R12-GATE` 완료와 `phase4-series-review.md` 5차 정적 리뷰 판정 존재. - **완료 증거:** 두 필드의 explicit null actual endpoint RED/GREEN/no-side-effect, 생략 기본값·정상 생성 회귀, series/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, genre 유효성 정책 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `genreId: null`, `isAdult: null`이 현재 각각 primitive 기본값으로 역직렬화되는 경로를 actual endpoint로 고정하고 S3·DB·event 결과를 단언한다. - [x] **GREEN:** v2 생성 경계에서 두 non-null primitive의 명시적 null을 `common.error.invalid_request` 400으로 변환한다. - [x] **CONTRACT TEST:** 두 필드 생략 시 `genreId=0`, `isAdult=false` 기본값과 정상 image 생성, 기존 미지 필드·빈 image 검증을 유지한다. - [x] **REFACTOR:** strict parse 결과를 활용한 v2 전용 최소 검증으로 제한하고 series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 5차 리뷰 Gate **Goal 실행 `P4-R4-GATE`:** `REV-042`의 시리즈 생성 primitive nullability와 기본값·부작용 경계를 재검토한다. - [x] **`P4-R4-GATE` 완료:** `P4-R4` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P4-R4` 완료. - **완료 증거:** review ID 처리 완료, explicit null 400/no-side-effect, 생략 기본값과 정상 생성 회귀 성공. - **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경. - 검증 기록: 무엇: `REV-042`의 시리즈 생성 primitive explicit null 경계를 처리했다. 왜: OpenAPI non-null primitive가 Jackson 기본값으로 보정되어 S3·DB·event mutation으로 이어질 수 있기 때문이다. 어떻게: actual multipart POST RED/GREEN, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행하고 리뷰 문서를 갱신했다. 결과: `genreId:null`, `isAdult:null`은 400/no-side-effect로 고정됐고 생략 기본값·정상 생성 회귀는 유지됐다. #### Phase 4 후속 기능 보완 - [x] **Task 4.11: 시리즈 등록용 장르 목록** **Goal 실행 `P4-R5`:** AI 캐릭터 시리즈 등록 화면에서 활성 장르를 `orders` 오름차순으로 조회하고 `id`, `genre`, `isAdult`의 직접 배열로 반환한다. - **추적 review ID:** `REV-046`. - **시작 조건:** `P3-R13-GATE` 완료와 PRD·OpenAPI의 승인된 장르 목록 계약 존재. - **완료 증거:** 활성 장르만 정렬된 exact response, 빈 목록, ADMIN 공통 경계와 Series/common 회귀, OpenAPI `implemented`, Progress 기록. - **범위 밖:** 장르 CRUD·순서 수정, character별 장르 제한, pagination, 레거시 장르 endpoint 변경. **Interfaces:** - `GET /api/v2/admin/ai-characters/series-genres` - Produces: `ApiResponse>`. **Files:** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesReferenceController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesGenreTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** active/inactive와 서로 다른 `orders`를 가진 장르로 exact array·정렬·빈 목록을 actual GET에 고정한다. - [x] **GREEN:** `AdminContentSeriesGenreService.getSeriesGenreList`를 그대로 재사용하고 별도 query·DTO·pagination을 추가하지 않는다. - [x] **CONTRACT TEST:** 정적 `/series-genres`가 character/series 동적 route와 충돌하지 않고 ADMIN 이중 인가와 오류 envelope를 유지하는지 확인한다. - [x] **REFACTOR:** 조회 controller와 facade method만 추가하고 Series/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesGenreTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 장르 목록 Gate **Goal 실행 `P4-R5-GATE`:** `REV-046`의 활성 장르·정렬·직접 배열 계약을 재검토한다. - [x] **`P4-R5-GATE` 완료:** `P4-R5` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R5` 완료. - **완료 증거:** 활성 장르 `orders` 정렬, exact direct array, route·공통 경계 회귀 성공, OpenAPI operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. - [x] **Task 4.12: 시리즈 상세 data를 목록 item과 정합화** **Goal 실행 `P4-R6`:** 시리즈 상세 `data`를 별도 레거시 상세 DTO가 아니라 시리즈 목록 `items` 하나와 동일한 11개 필드·타입으로 반환한다. - **추적 review ID:** `REV-047`. - **시작 조건:** `P4-R5-GATE` 완료와 PRD·OpenAPI의 승인된 시리즈 상세 계약 존재. - **완료 증거:** 목록과 상세의 동일 series exact JSON 대조, owner/active 격리, enum·nullable·cover URL parity, 기존 상세 전용 `genre`, `keywords` 부재와 Progress 기록. - **범위 밖:** 목록 wrapper 변경, 시리즈 entity/legacy detail DTO 변경, 새 필드 추가, public/legacy endpoint 변경. **Interfaces:** - `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}` - Produces `data`: `seriesId`, `title`, `introduction`, `coverImageUrl`, `publishedDaysOfWeek`, `genreId`, `isAdult`, `state`, `isActive`, `writer`, `studio`. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesQueryTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContractTest.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 동일 series의 목록 item과 상세 `data`가 현재 필드·타입이 다른 것을 exact JSON 비교로 고정한다. - [x] **GREEN:** v2 상세 response type을 `AiCharacterAdminSeriesListItem`으로 통일하고 owner 범위에서 조회한 entity를 동일 필드로 매핑한다. - [x] **CONTRACT TEST:** `publishedDaysOfWeek`와 `state` enum, `genreId`, `isActive`, nullable `writer/studio`, cover URL이 목록과 같고 `genre`, `keywords`가 없는지 확인한다. - [x] **REFACTOR:** 레거시 `GetCreatorAdminContentSeriesDetailResponse`와 entity mapper는 변경하지 않고 v2 series 경계만 수정해 Series/common 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesQueryTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContractTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 시리즈 상세 정합화 Gate **Goal 실행 `P4-R6-GATE`:** `REV-047`의 상세 단일 목록-item schema와 runtime parity를 재검토한다. - [x] **`P4-R6-GATE` 완료:** `P4-R6` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R6` 완료. - **완료 증거:** 목록 item/상세 data exact parity, owner/active 경계, 구 상세 필드 제거 회귀 성공, OpenAPI operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 4 multipart request part 계약 후속 보완 - [x] **Task 4.13: 시리즈 생성·수정 request part의 application/json 강제** **Goal 실행 `P4-R7`:** 시리즈 생성·수정 multipart의 `request` part가 OpenAPI encoding대로 `application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다. - **추적 review ID:** `REV-057`. - **시작 조건:** `P3-R16-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재. - **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·image/genre/owner 의미를 유지하고, `text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header, S3·DB·event no-side-effect를 반환한다. - **범위 밖:** JSON schema·strict reader·image/genre/state 의미, legacy/public endpoint, OpenAPI·신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual endpoint와 no-side-effect로 고정한다. - [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String strict reader에 동일 payload를 전달한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON, 필수 part 누락 400 및 기존 image/genre/owner 회귀를 확인한다. - [x] **REFACTOR:** series facade/domain 로직을 변경하지 않고 series/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 4 multipart request part 계약 후속 Gate **Goal 실행 `P4-R7-GATE`:** `REV-057` 수정 뒤 Series POST·PUT의 part-level JSON-only·415 경계를 재검토한다. - [x] **`P4-R7-GATE` 완료:** `P4-R7` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R7` 완료. - **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 image/genre/owner 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 4 multipart part 이름 계약 후속 보완 - [x] **Task 4.14: 시리즈 생성·수정의 미정의 multipart part 거부** **Goal 실행 `P4-R8`:** Series 생성·수정 multipart에서 OpenAPI가 정의한 `image`, `request` 외 part를 business mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-062`. - **시작 조건:** `P3-R17-GATE` 완료와 `phase4-series-review.md` 7차 정적 리뷰 판정 존재. - **완료 증거:** POST·PUT 미정의 part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect, 정상 image/genre/owner·필수 part·415 회귀. - **범위 밖:** OpenAPI schema, image empty·genre/state 의미, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성·수정에 정상 part와 `unexpected` part를 함께 보내 현재 mutation이 성공하는 경로를 actual endpoint와 side effect로 고정한다. - [x] **GREEN:** 실제 part 이름 집합이 POST·PUT 허용 집합 `{image, request}`의 부분집합인지 검사해 초과 이름을 `AiCharacterAdminApiException(HttpStatus.BAD_REQUEST, "common.error.invalid_request")`로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수/빈 image, request part 415, genre/owner 경계와 no-side-effect를 확인한다. - [x] **REFACTOR:** Series controller/test만 최소 변경하고 Series/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. **처리 기록 (2026-07-29 / P4-R8):** - RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectUndefinedCreateMultipartPartBeforeSideEffects' --tests '*shouldRejectUndefinedUpdateMultipartPartBeforeSideEffects'` → 새 테스트 6개가 400 기대 대비 기존 mutation 경로로 실패. - GREEN/focused: 동일 focused 명령 재실행 → `BUILD SUCCESSFUL in 3m 5s`. - 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 9s`. - 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 42s`; `git diff --check` → 출력 없음. - OpenAPI 대조: `api-contract.openapi.json`의 `SeriesCreateMultipart`, `SeriesUpdateMultipart`는 `additionalProperties: false`이고 허용 property가 `image`, `request`임을 확인했다. #### Phase 4 multipart part 이름 계약 후속 Gate **Goal 실행 `P4-R8-GATE`:** `REV-062` 수정 뒤 Series POST·PUT의 허용 part 이름과 image·media type 경계를 재검토한다. - [x] **`P4-R8-GATE` 완료:** `P4-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R8` 완료. - **완료 증거:** 미정의 part 400/no-side-effect, 정상·필수/빈 image·415·genre/owner 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. **Gate 기록 (2026-07-29 / P4-R8-GATE):** - Focused: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectUndefinedCreateMultipartPartBeforeSideEffects' --tests '*shouldRejectUndefinedUpdateMultipartPartBeforeSideEffects'` → `BUILD SUCCESSFUL in 57s`. - 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 1m 52s`. - 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 16s`; `git diff --check` → 출력 없음. - 판정: Series POST·PUT 미정의 part 400/no-side-effect, 정상·필수/빈 image·request 415·genre/owner 회귀가 모두 통과해 Phase 4 완료. #### Phase 4 multipart 일반 form-field part 후속 보완 - [x] **Task 4.15: 시리즈 생성·수정의 전체 multipart part 이름 검증** **Goal 실행 `P4-R9`:** Series POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한 모든 multipart part 이름을 검사해 `{image, request}` 외 이름을 mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-067`. - **시작 조건:** `P3-R18-GATE` 완료와 `phase4-series-review.md` 8차 정적 리뷰 판정 존재. - **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect, 기존 파일형 미정의 part·정상·필수/빈 image·request part 415 회귀 성공. - **범위 밖:** OpenAPI schema, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재 `fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다. - [x] **GREEN:** servlet request의 전체 part 이름 집합을 `{image, request}`와 비교해 초과 이름을 facade 진입 전에 공통 400으로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 정상·필수/빈 image, request part 415와 no-side-effect를 확인한다. - [x] **REFACTOR:** Series controller/test만 최소 변경하고 Series/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 4 multipart 전체 part 이름 후속 Gate **Goal 실행 `P4-R9-GATE`:** `REV-067` 수정 뒤 Series POST·PUT의 파일·일반 form-field를 포함한 전체 part 이름과 기존 image·media type 경계를 재검토한다. - [x] **`P4-R9-GATE` 완료:** `P4-R9` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R9` 완료. - **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectFilenameLessUndefined*MultipartPartBeforeSideEffects*' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` - 검증 기록(RED): 무엇: Series POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다. - 검증 기록(GREEN): 무엇: Series multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 `{image, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다. - 검증 기록(GATE): Series/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다. #### Phase 4 장르 ID domain validation 후속 보완 - [x] **Task 4.16: 시리즈 생성·수정의 0 이하 장르 ID 사전 거부** **Goal 실행 `P4-R10`:** Series POST·PUT의 non-null `genreId`가 0 이하이거나 활성 장르가 아니면 legacy service 호출 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-068`. - **시작 조건:** `P4-R9-GATE` 완료. - **완료 증거:** 생성·수정의 `genreId=0`, 음수, 미존재 양수는 모두 KO/EN/JA 400이고 S3·DB·event no-side-effect이며, 활성 장르와 수정 `genreId=null` 회귀 성공. - **범위 밖:** 장르 조회 정책, OpenAPI schema, legacy repository 반환형, DB constraint, legacy/public endpoint. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성·수정 actual endpoint에 `genreId=0`과 음수를 보내 현재 active-genre 검사를 우회하고 legacy repository의 non-null 경계에서 예외가 발생하는 경로를 400 기대와 no-side-effect로 고정한다. - [x] **GREEN:** `rejectMissingActiveGenre`에서 `genreId <= 0 || !repository.existsActiveGenre(genreId)`를 공통 invalid request로 변환한다. - [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 미존재 양수, 활성 장르, 수정 `genreId=null`, owner/image/media type 경계와 no-side-effect를 확인한다. - [x] **REFACTOR:** Series facade/test만 최소 변경하고 Series/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 4 장르 ID domain validation 후속 Gate **Goal 실행 `P4-R10-GATE`:** `REV-068` 수정 뒤 Series 생성·수정의 장르 ID domain validation과 기존 genre/owner/multipart 경계를 재검토한다. - [x] **`P4-R10-GATE` 완료:** `P4-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4 리뷰와 Progress를 갱신한다. - **시작 조건:** `P4-R10` 완료. - **완료 증거:** 0 이하·미존재 장르 400/no-side-effect와 활성·nullable 수정 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectNonPositiveGenreBeforeSideEffects*' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` - 검증 기록(RED): 무엇: Series POST·PUT `genreId=0/-1`. 왜: 0 이하 장르 ID가 active genre 검사를 우회해 legacy service/repository까지 도달하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다. - 검증 기록(GREEN): 무엇: 0 이하·미존재 장르 ID 사전 거부. 왜: 생성·수정의 non-null `genreId`가 유효한 활성 장르가 아니면 legacy 호출 전에 400이어야 하기 때문이다. 어떻게: `rejectMissingActiveGenre`가 `genreId <= 0 || !existsActiveGenre`를 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다. - 검증 기록(GATE): Series/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다. --- ### Phase 5: 커뮤니티 게시글·댓글 관리 vertical slice #### 목표 선택한 AI 캐릭터 소유 커뮤니티 게시글 등록, 수정, 고정/해제, soft delete, 관리자 조회와 댓글 CRUD를 제공한다. #### 범위와 비범위 - 포함: owner-scoped community query/write, 최대 고정 3개, soft delete 시 fixed 상태 제거, 댓글 root/reply 조회·작성·수정·soft delete, 이미지/오디오/유료 게시글 검증, 기존 알림/최근 소식 side effect parity. - 제외: 구매/좋아요, 캐릭터 직접 댓글, 댓글 hard delete·cascade, public community 조회 정책 변경. #### 선행 Phase 및 의존성 - Phase 1 target resolver. - 기존 community write behavior 특성화 테스트. #### API endpoint와 request/response contract - 정식 전체 schema는 `api-contract.openapi.json`의 Community operation 8개를 따른다. - `GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=&size=` -> `AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`. - `totalCount`는 target creatorMember 소유 active 게시글 전체 개수이고, `items`는 기존 `GetCommunityPostListResponse` item 필드와 고정 우선 정렬을 유지한다. - `POST /api/v2/admin/ai-characters/{characterId}/community-posts`는 optional `audioFile`, optional `postImage`, 필수 `request: CreateCommunityPostRequest`를 받고 `data: null`을 반환한다. - `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`는 optional `postImage`, 필수 request를 받는다. - update request는 두 레거시 update DTO에서 ID를 제외한 `content`, `isCommentAvailable`, `isAdult`, `isActive`, `isFixed`만 포함하고 `data: null`을 반환한다. 수정 `audioFile`, `price`는 레거시 계약에 없어 포함하지 않는다. - 댓글은 `GET|POST .../{postId}/comments`, `PUT|DELETE .../{postId}/comments/{commentId}`, `GET .../{commentId}/replies`의 5개 operation을 사용한다. #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: creatorMember owner-scoped community post query adapter 추가 가능. - Service: 신규 community application service에서 target owner 검증 후 기존 핵심 로직을 선택 재사용한다. #### DB migration - 없음. #### transaction과 concurrency 고려사항 - 고정 게시글 수 검증과 고정 처리는 같은 transaction에서 수행한다. - soft delete는 같은 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 함께 적용한다. - 동시 고정 요청은 기존 최대 3개 정책이 깨지지 않도록 repository count와 update 순서를 테스트한다. #### 보안 및 개인정보 위험 - 다른 character/HUMAN creator 게시글 수정, 고정, soft delete를 차단한다. - 유료 게시글의 접근 정책과 파일 경로 노출 정책을 기존 동작과 맞춘다. #### acceptance criteria - target creatorMember 소유 게시글만 조회/변경된다. - 최대 고정 수 3개 정책이 유지된다. - soft delete된 게시글은 fixed 상태와 fixedAt이 제거된다. - invalid target/ownership 실패 시 DB/S3/event side effect가 없다. #### targeted test - Characterization: `LegacyCommunityPostCharacterizationTest`. - V2 RED/GREEN: `AiCharacterAdminCommunityPostServiceTest`. - Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'` #### 전체 회귀 테스트 영향 - 기존 v2 community 조회와 legacy community write 테스트가 통과해야 한다. #### rollback 전략 - 신규 community v2 admin route/facade를 제거한다. - 신규 DDL이 없으므로 schema rollback은 없다. #### 권장 commit 경계 - `feat: add ai character admin community slice` - [x] **Task 5.1: 기존 community behavior 특성화 baseline 고정** **Goal 실행 `P5-T1`:** 기존 community의 media·유료·고정·soft delete·알림 동작을 신규 v2 비교 기준으로 고정한다. - **시작 조건:** `P4-GATE` 완료. - **완료 증거:** production 변경 전 특성화 테스트 통과, 오류·side-effect·동시성 관찰 결과와 Progress 기록. - **범위 밖:** 신규 v2 community production code 구현. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt` - [x] image/audio/paid post validation과 notification/recent-news side effect baseline을 작성한다. - [x] 최대 고정 3개와 fixed post soft delete clearing baseline을 작성한다. - [x] Phase 5 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다. - [x] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다. - [x] fixture/event spy만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다. - **관찰된 legacy baseline:** 유료 게시글은 `postImage`가 필수이고 오디오 게시글도 `postImage` 없이는 실패한다. 무료 게시글 생성은 FCM `CHANGE_NOTICE`와 home following recent-news를 발행하지만 유료 게시글은 recent-news를 발행하지 않는다. 이미 고정된 게시글의 재고정은 최대 3개 count를 다시 적용하지 않고, 다른 미고정 게시글을 4번째로 고정하려 하면 `creator.community.max_fixed_post_count`로 실패한다. `isActive=false` 수정은 같은 transaction에서 `isFixed=false`, `fixedAt=null`로 정리한다. - **Phase 5 v2 domain/client 오류 결정:** target/post missing·inactive·cross-owner, pagination·binding 실패, 최대 고정 3개 초과, 유료/오디오 게시글 이미지 누락, 이미지 validation 실패는 신규 prefix에서 400으로 반환한다. ownership/validation 실패는 DB/S3/FCM/recent-news side effect 전에 발생해야 한다. 예상하지 못한 S3/event/server 오류는 500 `common.error.unknown`을 사용하며, legacy처럼 recent-news publish 실패는 게시글 생성을 실패시키지 않는다. | Phase 5 domain/client 경우 | status | message key | KO | EN | JA | |---|---:|---|---|---|---| | missing/inactive/cross-owner resource, pagination·binding 검증 실패 | 400 | `common.error.invalid_request` | 잘못된 요청입니다. | Invalid request. | 無効なリクエストです。 | | 유료 게시글 이미지 누락 | 400 | `creator.community.paid_post_image_required` | 유료 게시글은 이미지를 등록해 주세요. | Please add an image for paid posts. | 有料投稿には画像を登録してください。 | | 오디오 게시글 이미지 누락 | 400 | `creator.community.audio_post_image_required` | 오디오 게시글은 이미지를 등록해 주세요. | Please add an image for audio posts. | オーディオ投稿には画像を登録してください。 | | 고정 게시글 3개 초과 | 400 | `creator.community.max_fixed_post_count` | 고정 게시글은 최대 3개까지 가능합니다. | You can pin up to 3 posts. | 固定投稿は最大3件まで可能です。 | | 이미지가 아님 | 400 | `image.error.only_image_allowed` | 이미지만 업로드할 수 있습니다. | Only images can be uploaded. | 画像のみアップロードできます。 | | 유료가 아닌 게시글 GIF 이미지 | 400 | `image.error.gif_paid_only` | GIF 이미지는 유료 게시글에만 등록할 수 있습니다. | GIF images can only be used for paid posts. | GIF画像は有料投稿にのみ登録できます。 | | 예상하지 못한 server/infrastructure 오류 | 500 | `common.error.unknown` | 알 수 없는 오류가 발생했습니다. 다시 시도해 주세요. | An unknown error occurred. try again. | 不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。 | - 검증 기록: 무엇: legacy community media/paid validation, FCM/recent-news side effect, fixed limit와 soft-delete fixed clearing baseline. 왜: Phase 5 v2 구현 전에 재사용할 legacy 동작과 신규 prefix에서 보강할 owner-first 오류 경계를 분리하기 위해. 어떻게: `LegacyCommunityPostCharacterizationTest`를 추가하고 production 변경 없이 focused/community package test, `ktlintCheck`, `git diff --check`를 실행했다. 결과: focused characterization은 `BUILD SUCCESSFUL in 2m 22s`, community package targeted test는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`, `git diff --check`는 출력이 없었다. - [x] **Task 5.2: 관리자 게시글 목록 조회 구현** **Goal 실행 `P5-T2`:** target owner의 관리자용 커뮤니티 게시글 목록을 안전한 전용 DTO와 pagination으로 제공한다. - **시작 조건:** `P5-T1` 완료와 Phase 5 오류 계약의 계획 반영. - **완료 증거:** owner/inactive/pagination/DTO RED/GREEN과 public 조회 무비판적 복제 없음 증거. - **범위 밖:** 게시글 생성·수정·고정·삭제. **Files:** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt` - [x] 미구현 목록, owner 격리, pagination 경계와 관리자 DTO 실패 test를 작성한다. - [x] 최소 owner-scoped query와 `page/size` 보정으로 focused test를 통과시킨다. - [x] 유료/media private 정보와 public viewer 상태를 부적절하게 노출하지 않는지 검증한다. - [x] focused test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록: 무엇: target 소유 활성 게시글 목록, 고정 정렬, pagination, legacy DTO 배열 형태, 유료 오디오 owner signed URL과 오류 계약. 왜: 공개 viewer 정책을 복제하지 않고 target `creatorMember`를 관리자 조회의 owner viewer로 고정하기 위해. 어떻게: RED로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest --rerun-tasks`를 실행해 route 부재로 4개 상태 코드 assertion이 실패함을 확인했다. 리뷰 보완으로 row별 count 조회가 붙은 목록의 `size=51`을 추가 RED로 확인했고, `size` 허용 범위를 1..50으로 제한했다. 최종 GREEN으로 같은 focused 명령은 `BUILD SUCCESSFUL in 3m`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 24s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 27s`, `git diff --check`는 출력이 없었다. 전체 회귀는 task 범위가 신규 관리자 community query에 한정되어 있어 실행하지 않았다. - [x] **Task 5.3: 커뮤니티 게시글 생성 구현** **Goal 실행 `P5-T3`:** target creatorMember 작성자로 이미지·오디오·유료 게시글을 기존 검증과 side effect parity로 생성한다. - **시작 조건:** `P5-T1`, `P5-T2` 완료. - **완료 증거:** 정상/media/paid validation RED/GREEN, writer/owner와 S3/event 결과, Progress 기록. - **범위 밖:** 게시글 수정·고정·soft delete, 구매 기능. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - [x] 정상/image/audio/paid validation과 invalid target 실패 test를 작성한다. - [x] 해석된 creatorMember를 writer/owner로 사용하는 최소 생성 구현을 통과시킨다. - [x] S3 media upload 결과와 validation/target 실패 시 DB·S3 no-side-effect를 검증한다. - [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다. - [x] **Task 5.4: 게시글 수정·고정·soft delete 구현** **Goal 실행 `P5-T4`:** owner 게시글만 수정·고정/해제하고 soft delete 시 고정 상태와 시간을 함께 제거한다. - **시작 조건:** `P5-T1`, `P5-T2` 완료. - **완료 증거:** update/fixed/soft delete/cross-owner RED/GREEN과 transaction 결과 기록. - **범위 밖:** hard delete, 구매·좋아요·댓글 관리. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - [x] 수정·고정/해제·soft delete와 cross-owner 실패 test를 작성한다. - [x] owner 검증 후 최소 mutation 구현으로 test를 통과시킨다. - [x] soft delete가 한 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 적용하는지 검증한다. - [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(RED): 무엇: content/comment/adult 수정, 이미지 교체, 고정/해제, soft delete, target/post/cross-owner/inactive 거부와 request part 누락 실제 endpoint 계약. 왜: `PUT` route와 owner-first mutation이 구현 전에는 존재하지 않음을 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest`를 production 변경 전에 실행했다. 결과: 6개 테스트가 기대 200/400 대신 미구현 `PUT`의 405로 실패해 `BUILD FAILED in 50s`였다. - 검증 기록(GREEN/REFACTOR): 무엇: active target과 active owner post 사전 검증, legacy 수정/고정 위임, soft delete fixed clearing 및 no-side-effect. 왜: legacy media/fixed 정책은 유지하면서 v2 경계의 cross-owner/inactive mutation을 차단하기 위해. 어떻게: focused test, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`, `./gradlew ktlintCheck`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 3m 52s`, community package 회귀는 `BUILD SUCCESSFUL in 59s`, ktlint는 `BUILD SUCCESSFUL in 29s`였다. 전체 `./gradlew test`는 신규 community update slice의 direct focused/community 회귀가 실행됐으므로 실행하지 않았다. - 최종 fresh community 회귀: `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'` 실행 결과 10개 Gradle task가 수행되어 `BUILD SUCCESSFUL in 5m 34s`였다. - 코드 품질 보완 RED: 최대 고정 3개인 owner가 네 번째 게시글을 `postImage`와 `isFixed=true`로 수정할 때 legacy 최대 고정 오류를 반환하면서도 imagePath와 S3 `putObject`가 변경되지 않아야 하는 test를 추가했다. 기존 호출 순서에서 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest`를 실행한 결과 7개 중 새 test 1개가 imagePath 변경 assertion에서 실패해 `BUILD FAILED in 48s`였다. - 코드 품질 보완 GREEN: `isActive != false && isFixed != null`인 고정 호출을 legacy 이미지 수정 앞에 두고 같은 focused 명령을 실행한 결과 7개 test가 `BUILD SUCCESSFUL in 59s`였다. 이어 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`는 `BUILD SUCCESSFUL in 1m 10s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 32s`였다. - [x] **Task 5.5: 최대 고정 수·동시성·side effect 검증** **Goal 실행 `P5-T5`:** 최대 고정 게시글 3개 정책이 동시 요청과 실패에서도 깨지지 않도록 고정한다. - **시작 조건:** `P5-T4` 완료. - **완료 증거:** 세 번째/네 번째 고정, 동시 요청, cross-owner와 DB/S3/event side effect RED/GREEN 기록. - **범위 밖:** 새로운 lock/dependency 도입, 기존 고정 정책 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt` - [x] 순차 고정 요청에서 3개 허용·4번째 거부와 재고정 count 생략을 특성화한다. - [x] 기존 legacy repository count/update 순서를 유지하고 production 변경 없이 test를 통과시킨다. - [x] invalid target/ownership 고정 실패 시 DB/S3/event 0건을 검증한다. - [x] concurrency 제약, focused/community test와 `ktlintCheck` 결과를 Progress에 기록한다. - **TDD 예외/특성화:** `AiCharacterAdminCommunityPostConcurrencyTest`를 production 변경 전에 추가해 첫 focused 실행했으나 3개 test가 모두 통과했다. 이는 P5-T4 facade가 legacy 고정 호출을 수정·이미지 업로드보다 먼저 수행하고, `CreatorCommunityService.updateCommunityPostFixed`가 미고정 post에만 활성 고정 수를 조회하는 기존 동작이 이미 요구를 충족했기 때문이다. 따라서 production 코드, lock, DB constraint, dependency를 추가하지 않았다. - **동시성 관찰 제한:** 현재 legacy 정책은 `countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue` 뒤 entity를 갱신하는 count/update 순서이며 lock 또는 DB constraint가 없다. 서로 다른 미고정 post의 실제 병렬 요청은 두 요청이 같은 count를 읽을 수 있어 결정적으로 재현·검증할 수 없으므로 sleep/flaky test를 추가하지 않고, 2개 고정 상태에서 세 번째 성공 뒤 네 번째 거부되는 순차 특성화만 고정했다. 이 Task 범위는 기존 정책 변경을 포함하지 않는다. - 검증 기록(특성화): 무엇: 세 번째 활성 고정 성공, 네 번째 활성 고정 400 최대 고정 메시지, 최대 상태의 이미 고정된 post 재고정, invalid target/cross-owner fixed multipart 요청의 DB/imagePath/S3/event 무변경. 왜: 기존 legacy 고정 정책과 owner-first side-effect 차단을 production 변경 없이 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest`를 실행했다. 결과: `BUILD SUCCESSFUL in 45s`, 10 actionable tasks 중 3 executed, 7 up-to-date였다. - 검증 기록(영향 범위): 무엇: 전체 v2 admin community package 회귀와 Kotlin lint. 왜: 신규 focused test의 controller/facade와 legacy community 경계 회귀를 확인하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`, `./gradlew ktlintCheck`를 실행했다. 결과: package test는 `BUILD SUCCESSFUL in 1m 14s`, 10 actionable tasks 중 1 executed, 9 up-to-date였고, ktlint는 `BUILD SUCCESSFUL in 23s`, 7 actionable tasks 중 2 executed, 5 up-to-date였다. - [x] **Task 5.6: Phase 5 보안·오류·회귀 검증** **Goal 실행 `P5-T6`:** 모든 community endpoint의 ADMIN·오류·ownership 계약과 기존 public/legacy 회귀를 고정한다. - **시작 조건:** `P5-T2`~`P5-T5` 완료. - **완료 증거:** endpoint 권한 매트릭스, 오류 status/key/KO·EN·JA, legacy/public 회귀와 Progress 기록. - **범위 밖:** Phase 6 FanTalk 기능. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostContractTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - [x] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다. - [x] target/post/media/fixed-count 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다. - [x] HUMAN/cross-character 게시글 mutation 거부와 legacy/public community 계약을 검증한다. - [x] Phase 5 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. - **TDD 예외/특성화 (2026-07-28):** production 변경 전에 `AiCharacterAdminCommunityPostContractTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest --rerun-tasks`를 실행했다. invalid target/post, paid media, 최대 고정 수, 필수 multipart `request` part의 KO/EN/JA `ApiResponse.error`, HUMAN target·cross-character mutation의 DB/S3 무변경이 모두 기존 구현에서 통과했다. 요구 동작이 이미 충족된 순수 특성화이므로 production 코드, dependency, legacy/public endpoint를 변경하지 않았다. - 검증 기록(focused, 2026-07-28): 무엇: community `GET`/`POST`/`PUT`의 JWT 비ADMIN 및 stale ADMIN claim 차단, 실제 endpoint 오류·소유권 계약. 왜: prefix 공통 sample/series 검증만으로는 community mapping 전체를 보장할 수 없기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --rerun-tasks`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest --rerun-tasks`를 실행했다. 결과: 각각 `BUILD SUCCESSFUL in 4m 45s`(10 actionable tasks 모두 실행), `BUILD SUCCESSFUL in 4m 6s`(10 actionable tasks 모두 실행)였다. - 검증 기록(Phase 5 Gate, 2026-07-28): 무엇: legacy 특성화와 v2 community package 회귀, Kotlin lint. 왜: P5-T1~P5-T6의 목록·생성·수정·고정·soft delete 및 기존 community 계약을 최종 확인하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`, `./gradlew ktlintCheck --rerun-tasks`를 실행했다. 결과: package test는 `BUILD SUCCESSFUL in 5m 52s`(10 actionable tasks 모두 실행), ktlint는 `BUILD SUCCESSFUL in 35s`(7 actionable tasks 모두 실행)였다. 전체 `./gradlew test`는 커뮤니티 경계와 공통 인가 production 코드가 변경되지 않아 실행하지 않았다. #### Phase 5 Gate **Goal 실행 `P5-GATE`:** Phase 5 community 사용자 흐름과 고정·side-effect·회귀 품질을 최종 판정한다. - [x] **`P5-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P5-T1`~`P5-T6` 완료. - **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`와 `./gradlew ktlintCheck` 성공, Progress 기록. - **범위 밖:** 실패와 무관한 Phase 6 구현. #### Phase 5 후속 리뷰 보완 - [x] **Task 5.7: 커뮤니티 JSON 오류와 pagination을 OpenAPI에 정합화** **Goal 실행 `P5-R1`:** `REV-025`의 multipart JSON 파싱 실패·미지 필드를 일관된 400으로 처리하고, `REV-026`의 계약에 없는 목록 `size <= 50` 제한을 제거한다. - **추적 review ID:** `REV-025`, `REV-026`. - **시작 조건:** `P4-R1-GATE` 완료와 `phase5-community-review.md` 판정 존재. - **완료 증거:** create/update의 malformed·필수 필드 누락·미지 필드 JSON이 400 `common.error.invalid_request`이고 DB/S3/event 부작용이 0회인 actual endpoint 테스트, `size=51` 요청이 문서 계약대로 상한 검증에 막히지 않는 목록 테스트, community/common 회귀와 Progress 기록. - **범위 밖:** OpenAPI에 pagination 상한 추가, legacy/public community controller 변경, media/fixed/notification 정책 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** create/update `request` part의 malformed JSON, 필수 필드 누락, 미지 필드가 exact 400 envelope과 DB/S3/event 0회를 반환하는지 확인한다. - [x] **RED:** `GET .../community-posts?size=51`이 계약 밖 상한 오류 없이 정상 pagination으로 처리되는지 확인한다. - [x] **GREEN:** legacy service 호출 전에 strict reader로 request DTO를 검증하고 JSON mapping 예외를 `common.error.invalid_request`로 변환하며 목록의 `size > 50` guard만 제거한다. - [x] **REFACTOR:** community/common 영향 범위 회귀, `ktlintCheck`, diff check를 실행한다. - 검증 기록(RED): 무엇: community create/update `request` part의 malformed JSON, create 필수 field 누락, create/update 미지 field와 목록 `size=51` 계약을 actual endpoint로 고정했다. 왜: `REV-025`~`REV-026`이 runtime에서 실제 실패하는지 확인하기 위해. 어떻게: 아래 focused create/update/query 명령을 production 변경 전 실행했다. 결과: 19개 중 3개가 JSON 경계와 `size=51` 상한 때문에 실패해 `BUILD FAILED in 1m 50s`였다. OpenAPI 확인 결과 update request는 required field가 없어 update 필수 field 누락 케이스는 제거했다. - 검증 기록(GREEN/REFACTOR): 무엇: create/update를 legacy service 호출 전 strict reader로 검증하고 Jackson parse/mapping 오류를 400 `common.error.invalid_request`로 변환했으며 목록의 `size <= 50` 상한만 제거했다. 왜: OpenAPI `additionalProperties: false`와 `Size` maximum 부재 계약을 runtime에 맞추기 위해. 어떻게: focused create/update/query 명령, community/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다. 결과: focused 명령은 `BUILD SUCCESSFUL in 1m 24s`, community/common 회귀는 `BUILD SUCCESSFUL in 2m`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1m 2s`, `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 후속 리뷰 Gate **Goal 실행 `P5-R1-GATE`:** `REV-025`~`REV-026` 수정 뒤 Community 3개 operation의 JSON 오류와 pagination 경계를 재검토한다. - [x] **`P5-R1-GATE` 완료:** `P5-R1` 완료 후 actual endpoint no-side-effect와 community/common 회귀, lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P5-R1` 완료. - **완료 증거:** 잘못된 JSON의 400 통일, 미지 필드 거부, 계약 밖 size 상한 제거, 영향 범위 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. - 검증 기록: 무엇: `P5-R1-GATE`에서 Community 3개 operation의 JSON 오류와 pagination 경계를 재판정했다. 왜: `REV-025`~`REV-026` 처리 후 actual endpoint no-side-effect와 영향 범위 회귀를 확인하기 위해. 어떻게: `P5-R1`과 같은 focused/community/common 회귀, `ktlintCheck`, `git diff --check` 증거를 기준으로 리뷰 문서와 Progress를 갱신했다. 결과: 잘못된 JSON의 400 통일, 미지 field 거부, `size=51` 허용과 영향 범위 회귀가 모두 통과했다. #### Phase 5 2차 후속 리뷰 보완 - [x] **Task 5.8: 최대 고정 3개 동시성 보장** **Goal 실행 `P5-R2`:** `REV-033`의 count-then-update 경쟁 조건을 owner 단위로 직렬화해 실제 동시 요청에서도 활성 고정 게시글이 3개를 초과하지 않도록 한다. - **추적 review ID:** `REV-033`. - **시작 조건:** `P5-R1-GATE` 완료와 `phase5-community-review.md` 2차 리뷰 판정 존재. - **완료 증거:** 두 독립 transaction의 결정적 동시 요청 RED, owner lock 순서 증거, 최종 고정 수 3개와 한 요청 성공·한 요청 400, 실패 요청의 S3/event 무변경, community/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 신규 DDL/unique constraint/dependency, 최대 수 정책 변경, legacy/public endpoint 변경, sleep 또는 반복 확률에 의존하는 flaky test. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt` - [x] **RED:** 활성 고정 2개와 서로 다른 미고정 게시글 2개를 준비하고, 두 독립 transaction을 barrier/lock probe로 같은 owner에 동시에 진입시켜 현재 최종 고정 수가 4개가 될 수 있음을 결정적으로 재현한다. - [x] **RED:** 기존 순차 테스트와 별개로 실제 병렬 요청임을 thread/transaction ID와 barrier 도달 assertion으로 확인하고, sleep·무작위 반복으로 성공 확률을 높이는 방식은 사용하지 않는다. - [x] **GREEN:** 기존 `MemberRepository.findByIdForUpdate`를 재사용해 fixed/unfixed count·update 전에 owner row를 잠그고, legacy 최대 3개 검증과 mutation을 같은 transaction에서 직렬화한다. - [x] **GREEN:** 같은 동시성 테스트에서 최종 고정 수 3개, 한 요청의 최대 고정 오류, DB/imagePath/S3/event 결과를 확인한다. - [x] **REFACTOR:** 재고정·해제·soft delete와 순차 세 번째/네 번째 요청을 유지하고 community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다. - 검증 기록(RED): 무엇: 활성 고정 2개 상태에서 서로 다른 미고정 게시글 2개를 병렬 fixed 요청으로 보내 두 요청이 같은 count 경계를 통과하는 race를 고정했다. 왜: `REV-033`의 count-then-update 경쟁 조건을 순차 테스트가 아닌 실제 병렬 요청으로 재현하기 위해. 어떻게: `CreatorCommunityRepository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue` 첫 호출을 latch로 지연하고 두 번째 요청을 진입시킨 뒤 `AiCharacterAdminCommunityPostConcurrencyTest`를 production 변경 전 실행했다. 결과: 신규 병렬 테스트가 기대 `[200, 400]` 대비 `[200, 200]`과 최종 4개 고정으로 실패해 RED를 확인했다. - 검증 기록(GREEN/REFACTOR): 무엇: fixed 변경 요청에서 legacy count/update 전에 `MemberRepository.findByIdForUpdate(creatorMemberId)`로 owner row를 잠그고, 병렬 요청을 직렬화했다. 왜: 신규 DDL 없이 owner 단위 최대 고정 3개 불변식을 같은 transaction 안에서 보장하기 위해. 어떻게: focused concurrency, community/common 영향 범위 회귀, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: focused는 `BUILD SUCCESSFUL in 46s`, community/common 회귀는 `BUILD SUCCESSFUL in 1m 19s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 2차 후속 리뷰 Gate **Goal 실행 `P5-R2-GATE`:** `REV-033`의 owner lock과 최대 고정 수 동시성 불변식을 재검토한다. - [x] **`P5-R2-GATE` 완료:** `P5-R2` 완료 후 동시성 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P5-R2` 완료. - **완료 증거:** `REV-033` 처리 완료, 결정적 병렬 재현, 최대 3개와 실패 no-side-effect, 순차/soft-delete 회귀 성공. - **범위 밖:** Gate에서 production code, DB schema 또는 공개 API 계약 변경. - 검증 기록: 무엇: `P5-R2-GATE`에서 owner lock과 최대 고정 수 동시성 불변식을 재판정했다. 왜: `REV-033` 처리 후 병렬/순차 fixed 정책과 community/common 영향 범위가 모두 유지되는지 확인하기 위해. 어떻게: focused concurrency, community/common 회귀, lint, diff check 결과를 fresh 확인하고 리뷰 문서와 Progress를 갱신했다. 결과: 모든 Gradle 명령이 `BUILD SUCCESSFUL`이고 diff check 출력이 없어 Phase 5 후속 리뷰를 완료로 판정했다. #### Phase 5 5차 리뷰 보완 - [x] **Task 5.9: 커뮤니티 primitive 필드의 required·null 계약 강제** **Goal 실행 `P5-R3`:** 커뮤니티 생성의 필수 boolean 누락·null과 optional `price`의 explicit null, 수정 `isFixed`의 explicit null을 생략 또는 JVM 기본값으로 보정하지 않고 S3·DB·event 전에 400으로 거부한다. - **추적 review ID:** `REV-043`. - **시작 조건:** `P4-R4-GATE` 완료와 `phase5-community-review.md` 5차 정적 리뷰 판정 존재. - **완료 증거:** 생성 required boolean 누락·null, `price: null`, 수정 `isFixed: null`의 actual endpoint RED/GREEN/no-side-effect, optional 생략·정상 mutation 회귀, community/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, community 정책 확장. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreateCommunityPostRequest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성 `isCommentAvailable`, `isAdult`의 누락·null과 `price: null`, 수정 `isFixed: null`이 현재 false·0 또는 생략으로 처리되는지 actual endpoint로 고정하고 DB·S3·event 결과를 단언한다. - [x] **GREEN:** v2 create/update 경계에서 required primitive의 존재와 모든 non-null primitive의 명시적 null을 검증해 `common.error.invalid_request` 400으로 변환한다. - [x] **CONTRACT TEST:** 생성 `price` 생략은 `0`, 수정 `isFixed` 생략은 변경 없음으로 유지하고 정상 create/update/fix/soft delete와 기존 미지 필드 거부를 확인한다. - [x] **REFACTOR:** 기존 strict parse 결과를 재사용하는 최소 검증으로 제한하고 community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 5차 리뷰 Gate **Goal 실행 `P5-R3-GATE`:** `REV-043`의 커뮤니티 primitive required/nullability와 생략 기본값·부작용 경계를 재검토한다. - [x] **`P5-R3-GATE` 완료:** `P5-R3` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P5-R3` 완료. - **완료 증거:** review ID 처리 완료, invalid primitive 400/no-side-effect, optional 생략과 정상 mutation 회귀 성공. - **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경. - 검증 기록: 무엇: `REV-043`의 커뮤니티 primitive required/nullability 경계를 처리했다. 왜: 생성 required boolean과 `price`, 수정 `isFixed`의 null/누락이 기본값 또는 생략으로 보정되면 잘못된 mutation이 진행될 수 있기 때문이다. 어떻게: actual multipart POST/PUT RED/GREEN, create/update focused, community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행하고 리뷰 문서를 갱신했다. 결과: invalid primitive 요청은 400/no-side-effect로 고정됐고 optional 생략·정상 mutation 회귀는 유지됐다. #### Phase 5 목록 계약 변경 보완 - [x] **Task 5.10: 커뮤니티 목록 timezone 제거와 pagination metadata 제공** **Goal 실행 `P5-R4`:** 커뮤니티 목록을 `timezone` 없이 조회하고 active owner 게시글의 전체 개수와 현재 page/size, 다음 페이지 여부, 기존 item 목록을 반환한다. - **추적 근거:** `DEC-P5-LIST-001`. - **시작 조건:** `P5-R3-GATE` 완료와 PRD·OpenAPI의 승인된 목록 계약 존재. - **완료 증거:** timezone 없는 actual GET의 RED/GREEN, `totalCount/page/size/hasNext/items` exact response, 첫·중간·마지막·범위 밖 page와 active owner count 회귀, community/common 영향 범위 회귀와 Progress 기록. - **범위 밖:** 목록 item 필드·정렬 변경, public/legacy community endpoint 변경, 검색/filter 추가, Spring `Page` 공개, 신규 dependency·DDL. **Interfaces:** - Consumes: `characterId`, `page` 기본값 `0`, `size` 기본값 `20`. - Produces: `AiCharacterAdminCommunityPostListResponse(totalCount: Long, page: Int, size: Int, hasNext: Boolean, items: List)`. - `totalCount`: target creatorMember 소유이면서 `isActive=true`인 게시글 전체 개수. - `hasNext`: `pageable.offset + items.size < totalCount`. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostContractTest.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/prd.md` - [x] **RED:** `timezone` 없이 호출한 GET이 현재 400을 반환하는 것과, timezone을 전달한 정상 호출의 `data`가 pagination object가 아니라 직접 배열인 계약 차이를 actual endpoint로 고정한다. - [x] **RED:** active owner 게시글을 `size + 1`개 이상 준비하고 첫 page의 `totalCount`, `page`, `size`, `hasNext=true`, 마지막 page의 `hasNext=false`, 범위 밖 page의 빈 `items`를 exact JSON으로 고정한다. - [x] **GREEN:** controller/facade에서 `timezone` parameter와 사용되지 않는 검증을 제거하고 `page`, `size`만 전달한다. - [x] **GREEN:** repository에 active owner count query 하나를 추가하고 기존 목록 query·고정 우선 정렬은 유지한다. - [x] **GREEN:** facade가 count와 현재 page items로 `AiCharacterAdminCommunityPostListResponse`를 구성하고 `hasNext`를 `pageable.offset + items.size < totalCount`로 계산한다. - [x] **CONTRACT TEST:** item의 기존 18개 필드, owner/inactive 격리, `page < 0`·`size < 1` 400과 문서에 없는 size 상한 부재를 유지하고 OpenAPI status를 `implemented`로 갱신한다. - [x] **REFACTOR:** Spring `Page`나 공용 pagination abstraction을 추가하지 않고 community package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck jq -e ' .paths["/api/v2/admin/ai-characters/{characterId}/community-posts"].get as $operation | ([$operation.parameters[] | .["$ref"]] | index("#/components/parameters/Timezone") | not) and ($operation["x-implementation-status"] == "implemented") and (.components.schemas.CommunityPostListResponse.required == ["totalCount", "page", "size", "hasNext", "items"]) ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 5 목록 계약 변경 Gate **Goal 실행 `P5-R4-GATE`:** `DEC-P5-LIST-001`의 query 제거와 pagination metadata·owner count 계약을 재검토한다. - [x] **`P5-R4-GATE` 완료:** `P5-R4` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R4` 완료. - **완료 증거:** timezone 없는 목록 성공, exact pagination wrapper, active owner total/hasNext와 기존 item·정렬·오류 회귀 성공, OpenAPI `implemented` 복구. - **범위 밖:** Gate에서 production code, item schema 또는 public/legacy API 변경. #### Phase 5 후속 기능 보완 - [x] **Task 5.11: 커뮤니티 댓글 CRUD** **Goal 실행 `P5-R5`:** target AI 캐릭터 소유 활성 커뮤니티 게시글의 원댓글·답글을 조회하고 target AI 명의로 작성·수정하며, 해당 게시글에 달린 댓글·답글은 작성자와 관계없이 row 단위로 soft delete한다. - **추적 review ID:** `REV-048`. - **시작 조건:** `P4-R6-GATE` 완료와 PRD·OpenAPI의 승인된 댓글 행위자·소유권 계약 존재. - **완료 증거:** 5개 actual endpoint, root/reply 조회, target AI 작성, 작성자 제한 수정, owner 범위 삭제, cross-resource/parent/character 격리, idempotent delete와 exact response 회귀. - **범위 밖:** 캐릭터 직접 댓글 삭제, 댓글 hard delete·cascade, 게시글 CRUD 의미 변경, 레거시/public endpoint 변경. **Interfaces:** - `GET|POST /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` - `PUT|DELETE /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}` - `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies` - 조회 query: 필수 `timezone`, `page`, `size`; response `GetCommunityPostCommentListResponse(totalCount, items)`. - 작성 body: 필수 `comment`, optional/nullable `parentId`, optional `isSecret=false`. - 수정 body: 필수 `comment`; mutation 성공 `data: null`. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** root/reply 목록의 `timezone/page/size`, `totalCount/items`, target 소유 활성 게시글 경계를 actual GET으로 고정한다. - [x] **RED:** root와 reply 작성 시 저장된 `member`가 target `creatorMember`이고, `parentId`가 같은 게시글의 활성 root가 아니면 400/no insert/no event인지 고정한다. - [x] **RED:** target AI가 작성한 활성 댓글/답글만 수정되고 팬 작성, 다른 게시글·캐릭터 댓글 수정은 400/no mutation인지 고정한다. - [x] **RED:** target 소유 게시글의 팬/AI 댓글·답글 삭제는 해당 row만 비활성화하고 하위 답글은 유지하며, 이미 비활성인 row는 200 no-op인지 고정한다. - [x] **GREEN:** 기존 `CreatorCommunityService`의 댓글 조회·작성·수정 의미를 재사용하되 facade에서 target, active owner, 동일 리소스 root parent, actor 권한을 먼저 검증한다. - [x] **CONTRACT TEST:** 미지 필드, 잘못된 page/size/timezone, cross-resource ID의 400 envelope와 모든 mutation의 `data: null`을 확인한다. - [x] **REFACTOR:** 댓글 전용 공용 abstraction이나 cascade 로직을 추가하지 않고 community/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 후속 기능 Gate **Goal 실행 `P5-R5-GATE`:** `REV-048`의 댓글 actor·owner·parent·soft delete 경계를 재검토한다. - [x] **`P5-R5-GATE` 완료:** `P5-R5` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R5` 완료. - **완료 증거:** 5개 operation, 레거시 목록 parity, AI 작성·수정 제한, owner 범위 row soft delete, cross-resource no-side-effect 회귀 성공. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 5 UTC 날짜 계약 보완 - [x] **Task 5.12: 커뮤니티 댓글·답글 timezone 제거와 UTC date 정합화** **Goal 실행 `P5-R6`:** 신규 관리자 커뮤니티 댓글·답글 조회에서 `timezone` query를 제거하고 기존 `date` 필드 값을 ISO-8601 UTC(`Z`)로 반환한다. - **추적 review ID:** `REV-051`. - **시작 조건:** `P3-R14-GATE` 완료와 `DEC-UTC-DATE-001` 및 OpenAPI 2.2.0 계약 존재. - **완료 증거:** 커뮤니티 댓글·답글 2개 actual GET의 query·UTC exact JSON RED/GREEN, 기존 `totalCount/items`·page/size·ownership·block/secret 의미 보존, legacy/public 회귀와 Progress 기록. - **범위 밖:** 커뮤니티 게시글 목록 item 날짜 변경, 댓글 mutation 의미 변경, legacy/public request/response 변경, 신규 pagination wrapper·dependency·DDL. **Interfaces:** - `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` - `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies` - 두 GET의 query는 `page`, `size`만 사용한다. response는 기존 `totalCount`, `items`와 item의 `date` 필드명을 유지하며 `date` 값만 ISO-8601 UTC(`Z`)로 고정한다. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** `timezone` 없는 root/reply GET이 현재 400이고, timezone별 로컬 문자열을 반환하는 현재 `date` 계약이 UTC exact JSON과 다른 것을 actual endpoint로 고정한다. - [x] **GREEN:** controller/facade signature와 timezone 검증을 제거하고 v2 owner-scoped 댓글 query/mapping에서 기존 `toUtcIso()`를 재사용해 `createdAt`을 `date`에 UTC로 직렬화한다. - [x] **CONTRACT TEST:** root/reply `date`, `totalCount/items`, page/size, target/owner/cross-post 경계와 기존 block/secret 필터가 유지되고, 추가 `timezone` query가 결과에 영향을 주지 않는지 확인한다. - [x] **REFACTOR:** legacy/public 댓글 repository·service의 timezone 동작은 변경하지 않고 v2 경계의 최소 query/mapping만 둔다. community/common 및 직접 영향 legacy 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. - **`P5-R6` / `P5-R6-GATE` 검증(2026-07-29):** RED는 production 변경 전 focused 댓글 테스트에서 timezone 없는 root/reply GET이 기존 필수 query 때문에 400을 반환해 2건 실패했고 `BUILD FAILED in 38s`였다. controller/facade의 timezone 입력·검증을 제거하고 legacy 조회 결과의 `date`만 `createdAt.toUtcIso()`로 재매핑한 뒤 같은 focused 명령은 `BUILD SUCCESSFUL in 43s`였다. community/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 11s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다. OpenAPI 36개 operation은 모두 `implemented`, `alignment-required`는 0개임을 `jq`로 확인했고, `git diff --check`는 출력 없이 종료했다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest \ --tests kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 36 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36 and ([$operations[] | select(.["x-implementation-status"] == "alignment-required")] | length) == 0 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 5 UTC 날짜 계약 Gate **Goal 실행 `P5-R6-GATE`:** `REV-051`의 커뮤니티 댓글·답글 UTC 계약과 legacy/public 격리를 재검토한다. - [x] **`P5-R6-GATE` 완료:** `P5-R6` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R6` 완료. - **완료 증거:** 커뮤니티 2개 GET의 timezone 제거·UTC `date`, 기존 pagination·ownership·block/secret과 legacy/public 계약 회귀 성공, OpenAPI 해당 2개 operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API schema 변경. #### Phase 5 JSON media type 계약 후속 보완 - [x] **Task 5.13: 커뮤니티 댓글 작성·수정의 application/json 강제** **Goal 실행 `P5-R7`:** 커뮤니티 댓글 작성·수정 endpoint가 OpenAPI의 유일한 request media type인 `application/json`만 받고, 그 밖의 media type은 공통 415 계약으로 거부하도록 정합화한다. - **추적 review ID:** `REV-053`. - **시작 조건:** `P4-R7-GATE` 완료와 OpenAPI의 두 JSON requestBody 및 415 response 계약 존재. - **완료 증거:** POST·PUT actual endpoint가 정상 JSON은 기존처럼 처리하고 `text/plain` 등 미지원 media type은 localized 415 `ApiResponse.error`, 표준 `Accept` header, DB/event no-side-effect를 반환한다. - **범위 밖:** JSON schema·댓글 actor/owner/parent 의미, legacy/public endpoint, 공통 exception handler, 신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** POST·PUT에 유효 JSON 문자열을 `text/plain`으로 보내면 현재 415가 아닌 handler 진입 결과가 나오는지 actual endpoint와 no-side-effect로 고정한다. - [x] **GREEN:** 두 mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, 작성 insert/event 0회와 수정 row 불변, 정상 JSON 회귀를 확인한다. - [x] **REFACTOR:** facade/parser와 댓글 도메인 동작을 변경하지 않고 community/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 JSON media type 계약 후속 Gate **Goal 실행 `P5-R7-GATE`:** `REV-053` 수정 뒤 두 mutation의 JSON-only·415·no-side-effect 경계를 재검토한다. - [x] **`P5-R7-GATE` 완료:** `P5-R7` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R7` 완료. - **완료 증거:** POST·PUT의 정상 JSON과 미지원 media type 415/header/envelope/no-side-effect 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 5 multipart request part 계약 후속 보완 - [x] **Task 5.14: 커뮤니티 게시글 생성·수정 request part의 application/json 강제** **Goal 실행 `P5-R8`:** 커뮤니티 게시글 생성·수정 multipart의 `request` part가 OpenAPI encoding대로 `application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다. - **추적 review ID:** `REV-058`. - **시작 조건:** `P5-R7-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재. - **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·media/fixed/owner 의미를 유지하고, `text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header, S3·DB·event no-side-effect를 반환한다. - **범위 밖:** JSON schema·strict reader·media/fixed/concurrency 의미, 댓글 endpoint, legacy/public endpoint, OpenAPI·신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual endpoint와 no-side-effect로 고정한다. - [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String strict reader에 동일 payload를 전달한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON, 필수 part 누락 400 및 기존 media/fixed/owner 회귀를 확인한다. - [x] **REFACTOR:** community facade/domain 로직을 변경하지 않고 community/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```bash ./gradlew test \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 5 multipart request part 계약 후속 Gate **Goal 실행 `P5-R8-GATE`:** `REV-058` 수정 뒤 Community post POST·PUT의 part-level JSON-only·415 경계를 재검토한다. - [x] **`P5-R8-GATE` 완료:** `P5-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R8` 완료. - **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 media/fixed/owner 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 5 multipart part 이름 계약 후속 보완 - [x] **Task 5.15: 커뮤니티 생성·수정의 미정의 multipart part 거부** **Goal 실행 `P5-R9`:** Community post 생성은 `audioFile`, `postImage`, `request`, 수정은 `postImage`, `request` 외 multipart part를 business mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-063`. - **시작 조건:** `P4-R8-GATE` 완료와 `phase5-community-review.md` 7차 정적 리뷰 판정 존재. - **완료 증거:** POST·PUT 미정의 part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect, 수정의 `audioFile` 거부 및 정상 media/fixed/owner·필수 part·415 회귀. - **범위 밖:** OpenAPI schema, media/fixed/concurrency 의미, 전역 multipart resolver, 댓글·legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 생성·수정에 `unexpected` part를 추가하고, 수정에는 OpenAPI에 없는 `audioFile`을 추가해 현재 정상 mutation으로 진행되는 경로와 side effect를 actual endpoint로 고정한다. - [x] **GREEN:** 실제 part 이름 집합을 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}`와 비교해 초과 이름을 `AiCharacterAdminApiException(HttpStatus.BAD_REQUEST, "common.error.invalid_request")`로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, request part 누락·415, media/fixed/owner 경계와 no-side-effect를 확인한다. - [x] **REFACTOR:** Community controller/test만 최소 변경하고 community/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 5 multipart part 이름 계약 후속 Gate **Goal 실행 `P5-R9-GATE`:** `REV-063` 수정 뒤 Community POST·PUT의 operation별 허용 part 이름과 기존 media type 경계를 재검토한다. - [x] **`P5-R9-GATE` 완료:** `P5-R9` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R9` 완료. - **완료 증거:** 미정의 part와 수정 `audioFile` 400/no-side-effect, 정상 media/fixed/owner·필수 part·415 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 5 multipart 일반 form-field part 후속 보완 - [x] **Task 5.16: 커뮤니티 생성·수정의 전체 multipart part 이름 검증** **Goal 실행 `P5-R10`:** Community post POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한 모든 multipart part 이름을 검사해 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 외 이름을 mutation 전에 400 `common.error.invalid_request`로 거부한다. - **추적 review ID:** `REV-069`. - **시작 조건:** `P4-R10-GATE` 완료와 `phase5-community-review.md` 8차 정적 리뷰 판정 존재. - **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect, 기존 파일형 미정의 part·수정 `audioFile` 거부·정상 media/fixed/owner·필수 part·request part 415 회귀 성공. - **범위 밖:** OpenAPI schema, media/fixed/concurrency 의미, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재 `fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다. - [x] **GREEN:** servlet request의 전체 part 이름 집합을 operation별 허용 집합과 비교해 초과 이름을 facade 진입 전에 공통 400으로 거부한다. - [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 수정 `audioFile` 거부, 정상 media/fixed/owner·필수 part·request part 415와 no-side-effect를 확인한다. - [x] **REFACTOR:** Community controller/test만 최소 변경하고 community/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다. #### Phase 5 multipart 전체 part 이름 후속 Gate **Goal 실행 `P5-R10-GATE`:** `REV-069` 수정 뒤 Community POST·PUT의 파일·일반 form-field를 포함한 전체 part 이름과 operation별 media type 경계를 재검토한다. - [x] **`P5-R10-GATE` 완료:** `P5-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5 리뷰와 Progress를 갱신한다. - **시작 조건:** `P5-R10` 완료. - **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` - 검증 기록(RED): 무엇: Community POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다. - 검증 기록(GREEN): 무엇: Community multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다. - 검증 기록(GATE): community/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다. --- ### Phase 6: FanTalk 목록·답변·팬 원글 삭제 vertical slice #### 목표 선택한 AI 캐릭터의 FanTalk 목록을 관리자 전용 경계에서 조회하고 자신의 활성 root FanTalk에 creator reply를 작성하며 팬 작성 root를 soft delete하는 v2 관리자 API를 제공한다. #### 범위와 비범위 - 포함: 관리자용 root FanTalk/creator reply 목록, root FanTalk 존재/활성/owner 검증, creator reply 저장, 팬 작성 root row soft delete, 언어 감지와 기존 응답 의미 parity. - 제외: FanTalk 원글 작성, nested reply, hard delete·cascade, 구매/댓글형 기능, AI 캐릭터 일반 사용자 활동. #### 선행 Phase 및 의존성 - Phase 1 target resolver. - 기존 FanTalk 저장 엔티티와 응답 DTO 의미 특성화. #### API endpoint와 request/response contract - 정식 전체 schema는 `api-contract.openapi.json`의 FanTalk operation 3개를 따른다. - `GET /api/v2/admin/ai-characters/{characterId}/fan-talks?page=0&size=20` - Response: 공개 v2 `CreatorChannelFanTalkTabResponse`와 동일한 `fanTalkCount`, `fanTalks`, `page`, `size`, `hasNext` 및 nested root/reply 필드 - `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` - Request: `CreateAiCharacterAdminFanTalkReplyRequest(content: String)` - Response: `AiCharacterAdminFanTalkReplyResponse(fanTalkId, replyId, creatorMemberId, content, createdAtUtc)` - `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`는 팬 작성 root만 soft delete하고 `data: null`을 반환한다. #### entity, repository, service 변경 - Entity: 변경 없음. - Repository: 관리자 목록용 owner-scoped root/reply 조회와 root/active/creator owner 검증 adapter를 추가한다. - Service: 관리자 목록은 공개 v2 DTO 형태로 조립하되 viewer/block 필터를 적용하지 않는다. reply는 target 검증 후 기존 저장/언어 감지 로직을 필요한 만큼 재사용한다. #### DB migration - 없음. #### transaction과 concurrency 고려사항 - root FanTalk 조회와 reply 저장은 같은 transaction에서 수행한다. - 중복 답변 허용 여부는 기존 domain 정책을 따른다. 기존 정책이 없다면 이번 API는 별도 중복 차단을 추가하지 않는다. #### 보안 및 개인정보 위험 - 다른 character FanTalk, HUMAN creator FanTalk, inactive FanTalk, nested parent에는 답변하지 않는다. - AI character Member 로그인/impersonation 없이 writer/creator만 해석된 creatorMember로 저장한다. #### acceptance criteria - target AI character는 자신의 활성 root FanTalk에만 답변할 수 있다. - 관리자는 공개 v2와 동일한 필드 형태로 target AI character의 root FanTalk와 creator reply를 조회할 수 있다. - cross-character, nested parent, inactive/missing FanTalk는 4xx이며 reply 저장과 이벤트 발행이 없다. - 저장된 답변의 writer/creator는 해석된 creatorMember와 일관된다. #### targeted test - Characterization: `LegacyFanTalkReplyCharacterizationTest`. - V2 RED/GREEN: `AiCharacterAdminFanTalkQueryTest`, `AiCharacterAdminFanTalkReplyServiceTest`. - Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'` #### 전체 회귀 테스트 영향 - 기존 FanTalk 조회/작성 관련 테스트가 통과해야 한다. #### rollback 전략 - 신규 FanTalk 목록·reply v2 admin route/facade를 제거한다. - 신규 DDL이 없으므로 schema rollback은 없다. #### 권장 commit 경계 - `feat: add ai character admin fan talk slice` - [x] **Task 6.1: 기존 FanTalk reply 의미 특성화 baseline 고정** **Goal 실행 `P6-T1`:** 기존 FanTalk의 root 판별, 언어 감지, 응답 의미와 writer/creator 저장 결과를 비교 기준으로 고정한다. - **시작 조건:** `P5-GATE` 완료. - **완료 증거:** production 변경 전 특성화 테스트 통과, 중복 답변 정책과 Progress 기록. - **범위 밖:** 신규 v2 reply production code 구현. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt` - [x] valid root reply의 언어 감지, response 의미와 writer/creator 저장 baseline을 작성한다. - [x] root/nested/active 판별과 기존 중복 답변 정책을 관찰해 기록한다. - [x] Phase 6 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다. - [x] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다. - [x] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다. - **관찰된 legacy baseline:** `ExplorerService.writeCheers`는 조회된 parent가 root인지 또는 active인지 검증하지 않아 inactive nested parent에도 답변을 연결한다. 조회 결과가 없는 parent ID는 parent 없이 root 글로 저장하고, 같은 root에 대한 creator 답변 중복도 허용한다. `languageCode`가 blank일 때만 `CREATOR_CHEERS` 언어 감지 이벤트를 발행한다. creator 조회 실패는 `SodaException(messageKey = "member.validation.user_not_found")`, 차단은 `explorer.creator.blocked_cheers`로 조립된 `SodaException.message`를 반환한다. 이 service 경계는 HTTP status를 결정하지 않는다. - **Phase 6 v2 domain/client 오류 결정:** target 또는 FanTalk missing/inactive/cross-character/nested parent, 빈 content와 request binding 실패는 저장·이벤트 전에 400 `common.error.invalid_request`로 거부한다. KO `잘못된 요청입니다.`, EN `Invalid request.`, JA `無効なリクエストです。`를 반환한다. 예상하지 못한 server/infrastructure 오류는 500 `common.error.unknown`과 공통 KO/EN/JA message를 사용한다. legacy의 root 전환·nested/inactive 허용과 HTTP status 미결정은 신규 v2에 복제하지 않는다. - **TDD 예외/특성화 (2026-07-28):** `LegacyFanTalkReplyCharacterizationTest`를 production 변경 없이 추가한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`를 실행했다. valid root reply의 parent/member/creator/response/event, missing parent root 전환과 nonblank 언어 이벤트 미발행, inactive nested parent 허용, 중복 답변 허용, missing creator key 및 blocked creator message 6건이 첫 실행부터 통과했다. 기존 구현의 의도된 동작을 고정하는 특성화이므로 production 코드나 dependency를 추가하지 않았다. - **검증 기록:** 무엇: legacy FanTalk reply 의미 특성화와 Kotlin lint. 왜: Phase 6 v2 저장 전에 재사용할 저장·응답·이벤트 동작과 새 경계에서 차단할 legacy 허용 범위를 분리하기 위해. 어떻게: 위 focused test 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 9s`(10 actionable tasks 중 3 executed, 7 up-to-date)였다. 첫 lint 실행은 새 test의 import 정렬 1건으로 `BUILD FAILED in 22s`였고, import만 정렬한 뒤 재실행한 `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`(7 actionable tasks 중 2 executed, 5 up-to-date)였다. production 변경이 없고 focused test가 직접 legacy service 경계를 실행하므로 전체 `./gradlew test`는 실행하지 않았다. - [x] **Task 6.2: FanTalk 관리자 목록 조회 구현** **Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 root FanTalk와 creator reply를 공개 v2 응답 필드 형태로 조회한다. - **시작 조건:** `P6-T1` 완료와 Phase 6 오류 계약의 계획 반영. - **완료 증거:** owner-scoped 목록·nested reply·pagination exact JSON RED/GREEN과 Progress 기록. - **범위 밖:** 공개 v2 endpoint 변경, viewer/block 필터 재사용, FanTalk 원글 작성. **Files:** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDto.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkQueryTest.kt` - [x] **RED:** `fanTalkCount/fanTalks/page/size/hasNext`와 root/reply 전체 필드, target owner, pagination 실패 test를 작성한다. - [x] **GREEN:** 공개 v2 DTO 필드 형태를 유지하면서 관리자 target 정책으로 조회하는 최소 구현을 통과시킨다. - [x] **REFACTOR:** 공개 v2 endpoint 계약 불변, viewer/block 필터 비적용과 ADMIN 인가를 회귀하고 기록한다. - [x] **Task 6.3: FanTalk root reply 저장 구현** **Goal 실행 `P6-T3`:** 선택한 AI 캐릭터의 활성 root FanTalk에 creator reply를 저장하고 전용 응답을 반환한다. - **시작 조건:** `P6-T1`, `P6-T2` 완료와 Phase 6 오류 계약의 계획 반영. - **완료 증거:** 정상 저장·언어 감지·DTO·writer/creator RED/GREEN과 Progress 기록. - **범위 밖:** FanTalk 원글, nested reply, 일반 사용자 대리 작성. **Files:** - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyDto.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyCreateTest.kt` - [x] 미구현 정상 root reply, 언어 감지와 응답 DTO 실패 test를 작성한다. - [x] root 조회와 reply 저장을 같은 transaction에서 수행하는 최소 구현을 통과시킨다. - [x] writer/creator가 resolver의 creatorMember와 일치하고 principal impersonation이 없음을 검증한다. - [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다. - [x] **Task 6.4: FanTalk target·root·ownership 거부 구현** **Goal 실행 `P6-T4`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다. - **시작 조건:** `P6-T1`~`P6-T3` 완료. - **완료 증거:** 네 거부 분기의 정확한 오류 계약, DB/event 0건과 Progress 기록. - **범위 밖:** 새로운 중복 답변 차단 정책. **Files:** - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyOwnershipTest.kt` - [x] cross-character/nested/inactive/missing FanTalk과 inactive target 거부 테스트를 작성하고 현재 guard 동작을 특성화했다. - [x] 기존 `resolveActiveTarget`과 owner-scoped `findActiveRoot`가 저장 전에 target·root·active·owner를 검증함을 확인했다. - [x] 각 실패의 정확한 400 `common.error.invalid_request` KO·EN·JA envelope와 reply insert/event 0건을 검증했다. - [x] focused, reply create, legacy characterization, FanTalk package test와 `ktlintCheck` 결과를 Progress에 기록했다. - **TDD 예외/특성화 (2026-07-28):** production 변경 전 `AiCharacterAdminFanTalkReplyOwnershipTest`를 추가해 처음 실행했으나, cross-character root, nested parent, inactive root, missing FanTalk, inactive target의 KO/EN/JA 400 envelope와 reply row/event 무변경이 모두 통과했다. 이는 P6-T3의 `resolveActiveTarget`과 `findActiveRoot(creatorMemberId, fanTalkId)`가 이미 target active, owner, active, root 조건을 저장·이벤트 전에 보장하기 때문이다. 요구 동작이 충족된 특성화이므로 production code, dependency, legacy/public endpoint, 중복 답변 정책을 변경하지 않았다. - [x] **Task 6.5: Phase 6 보안·오류·회귀 검증** **Goal 실행 `P6-T5`:** FanTalk 목록·reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다. - **시작 조건:** `P6-T2`~`P6-T4` 완료. - **완료 증거:** 권한 매트릭스, request binding/domain 오류, legacy 회귀와 Progress 기록. - **범위 밖:** Phase 7 외 전체 기능 수정. **Files:** - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - [x] endpoint의 ADMIN 이중 인가와 stale claim을 검증한다. - [x] 빈 content/binding/domain 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다. - [x] 기존 FanTalk 조회·작성 계약과 AI 로그인/token/impersonation 부재를 확인한다. - [x] Phase 6 focused test와 `ktlintCheck` 결과를 Progress에 기록한다. - 검증 기록(RED): 무엇: 빈 문자열·공백 reply content의 저장 전 거부와 malformed/missing JSON binding envelope. 왜: 기존 `createReply`가 빈 content를 저장해 Phase 6 오류 계약을 위반했기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest --rerun-tasks`를 실행했다. 결과: 6개 테스트 중 빈 문자열·공백 content의 KO/EN/JA 3개가 400 기대 대비 200으로 실패해 `BUILD FAILED in 5m 42s`였다. - 검증 기록(GREEN/REFACTOR): 무엇: `AiCharacterAdminFanTalkReplyContractTest`의 빈/공백 content, malformed/missing body와 content binding KO/EN/JA `ApiResponse.error` envelope, 실제 FanTalk list/reply의 JWT 비ADMIN·stale ADMIN claim, 기존 query/write/legacy 회귀. 왜: 신규 관리자 endpoint의 이중 인가와 오류·저장 의미를 함께 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`, `./gradlew ktlintCheck`를 순차 실행했다. 결과: contract는 `BUILD SUCCESSFUL in 53s`, authorization은 `BUILD SUCCESSFUL in 46s`, FanTalk package는 `BUILD SUCCESSFUL in 50s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`였다. 첫 `ktlintCheck`는 새 contract test의 unused import 1건으로 `BUILD FAILED in 34s`였고 import 제거 뒤 재실행했다. 기존 `AiCharacterAdminFanTalkReplyCreateTest`는 관리자 principal이 아닌 target `creatorMember`를 reply의 writer·creator로 저장하고 admin과 다름을 단언하므로 AI 로그인/token/impersonation 부재를 중복 없이 확인했다. #### Phase 6 Gate **Goal 실행 `P6-GATE`:** Phase 6 FanTalk 목록·reply의 조회·root·ownership·저장·회귀 품질을 최종 판정한다. - [x] **`P6-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다. - **시작 조건:** `P6-T1`~`P6-T5` 완료. - **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와 `./gradlew ktlintCheck` 성공, Progress 기록. - **범위 밖:** 실패와 무관한 신규 기능. #### Phase 6 후속 리뷰 보완 - [x] **Task 6.6: FanTalk query policy와 reply JSON 경계를 OpenAPI에 정합화** **Goal 실행 `P6-R1`:** `REV-027`의 목록 pagination을 공개 v2와 같은 보정 정책으로 복구하고, `REV-028`의 reply request에서 `additionalProperties: false`를 실제로 강제한다. - **추적 review ID:** `REV-027`, `REV-028`. - **시작 조건:** `P5-R1-GATE` 완료와 `phase6-fantalk-review.md` 판정 존재. - **완료 증거:** `page < 0 -> 0`, `size < 20 -> 20`, `size > 50 -> 50` actual endpoint 테스트, reply 미지 필드 400/no insert/no event 테스트, FanTalk/public policy 정적 대조와 Progress 기록. - **범위 밖:** 공개 v2 FanTalk query policy 변경, reply 저장/언어 감지/ownership 정책 변경, OpenAPI schema 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkQueryTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/domain/CreatorChannelFanTalkQueryPolicy.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 음수 page, 20 미만 size, 50 초과 size가 400이 아니라 각각 0/20/50으로 보정되는 actual 목록 테스트를 작성한다. - [x] **RED:** reply JSON에 계약 밖 필드가 있으면 400 `common.error.invalid_request`이고 reply insert/event가 0회인지 확인한다. - [x] **GREEN:** 관리자 목록에 공개 v2와 동일한 pagination 정규화를 적용하고 reply body만 strict reader로 역직렬화한다. - [x] **REFACTOR:** 중복 정책은 기존 query policy의 가시성과 의존 방향을 확인한 뒤 최소한으로 재사용하고, FanTalk/common 영향 범위 회귀, `ktlintCheck`, diff check를 실행한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkQueryTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 6 후속 리뷰 Gate **Goal 실행 `P6-R1-GATE`:** `REV-027`~`REV-028` 수정 뒤 FanTalk 2개 operation의 query/body 경계를 재검토한다. - [x] **`P6-R1-GATE` 완료:** `P6-R1` 완료 후 actual endpoint no-side-effect와 FanTalk/common 회귀, lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다. - **시작 조건:** `P6-R1` 완료. - **완료 증거:** 공개 v2 pagination 보정과 관리자 목록 일치, reply 미지 필드 거부, 영향 범위 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 6 후속 기능 보완 - [x] **Task 6.7: 팬 작성 FanTalk 원글 soft delete** **Goal 실행 `P6-R2`:** target AI 캐릭터 채널에 팬이 작성한 FanTalk root를 row 단위로 soft delete하고 연결된 creator reply row는 변경하지 않는다. - **추적 review ID:** `REV-049`. - **시작 조건:** `P5-R5-GATE` 완료와 PRD·OpenAPI의 승인된 FanTalk 삭제 계약 존재. - **완료 증거:** 팬 작성 활성 root 삭제, 목록·count 제외, creator reply row 유지, already inactive no-op, creator 작성/root 아닌 row/cross-character no mutation과 Progress 기록. - **범위 밖:** 캐릭터 직접 댓글 삭제, FanTalk hard delete·cascade, 팬 작성 여부 재정의, 공개 v2 endpoint 변경. **Interfaces:** - `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}` - Request body 없음; response `ApiResponse.ok(null)`. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDeleteTest.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** target 채널의 팬 작성 활성 root 삭제가 root `isActive=false`, creator reply row 불변, 목록·`fanTalkCount` 제외, `data: null`인지 actual DELETE/GET으로 고정한다. - [x] **RED:** target AI가 작성한 row, reply row, 다른 채널 root는 400/no mutation이며 같은 target의 이미 비활성인 팬 root는 200 no-op인지 고정한다. - [x] **GREEN:** repository가 target creator, root, fan writer를 함께 식별하고 facade는 활성 row만 `isActive=false`로 변경한다. reply collection과 row는 수정하지 않는다. - [x] **CONTRACT TEST:** body 없는 DELETE, ADMIN 이중 인가, target inactive/invalid ID와 공통 오류 envelope를 확인한다. - [x] **REFACTOR:** 기존 `CreatorCheers` soft-delete field와 현재 repository만 사용하고 FanTalk/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkDeleteTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 6 후속 기능 Gate **Goal 실행 `P6-R2-GATE`:** `REV-049`의 target root·fan writer·row-only soft delete 경계를 재검토한다. - [x] **`P6-R2-GATE` 완료:** `P6-R2` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 6 리뷰와 Progress를 갱신한다. - **시작 조건:** `P6-R2` 완료. - **완료 증거:** 팬 root만 삭제, creator reply row 유지, idempotent delete, cross-target no-side-effect와 OpenAPI operation `implemented`. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 6 JSON media type 계약 후속 보완 - [x] **Task 6.8: FanTalk 답변 작성의 application/json 강제** **Goal 실행 `P6-R3`:** FanTalk 답변 작성 endpoint가 OpenAPI의 유일한 request media type인 `application/json`만 받고, 그 밖의 media type은 공통 415 계약으로 거부하도록 정합화한다. - **추적 review ID:** `REV-054`. - **시작 조건:** `P5-R8-GATE` 완료와 OpenAPI의 reply JSON requestBody 및 415 response 계약 존재. - **완료 증거:** actual POST가 정상 JSON은 기존 축약 응답·저장·event 의미를 유지하고 `text/plain` 등 미지원 media type은 localized 415 `ApiResponse.error`, 표준 `Accept` header, reply insert/event no-side-effect를 반환한다. - **범위 밖:** reply JSON schema·strict parser·root/ownership·언어 감지, FanTalk 목록/삭제, legacy/public endpoint, 신규 dependency·DDL 변경. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** 유효 reply JSON 문자열을 `text/plain`으로 보내면 현재 415가 아닌 handler 진입 결과가 나오는지 actual endpoint와 insert/event no-side-effect로 고정한다. - [x] **GREEN:** reply POST mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가한다. - [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, reply insert/event 0회와 정상 JSON 축약 응답을 확인한다. - [x] **REFACTOR:** facade/parser와 FanTalk 도메인 동작을 변경하지 않고 FanTalk/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck git diff --check ``` #### Phase 6 JSON media type 계약 후속 Gate **Goal 실행 `P6-R3-GATE`:** `REV-054` 수정 뒤 reply POST의 JSON-only·415·no-side-effect 경계를 재검토한다. - [x] **`P6-R3-GATE` 완료:** `P6-R3` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 6 리뷰와 Progress를 갱신한다. - **시작 조건:** `P6-R3` 완료. - **완료 증거:** 정상 JSON과 미지원 media type 415/header/envelope/no-side-effect 회귀 성공. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 6 FanTalk 답변 수정 후속 기능 - [x] **Task 6.9: 레거시 계약을 유지하는 FanTalk 답변 수정 API** **Goal 실행 `P6-R4`:** `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`에서 target AI가 작성하고 target의 활성 root에 직접 연결된 reply만 레거시 `PUT /explorer/profile/cheers` 의미로 수정한다. - **추적 review ID:** `REV-059`. - **시작 조건:** `P6-R3-GATE` 완료와 PRD·OpenAPI 2.3.0의 확정 답변 수정 계약 존재. - **완료 증거:** optional/nullable `content`·`isActive`, 동시 입력, 빈 객체 no-op, 비활성 reply 재활성화, target/root/direct-reply ownership, JSON-only·strict body, ADMIN/common 오류, 레거시 성공 `data`, no-event를 actual endpoint와 영향 범위 회귀로 확인하고 OpenAPI 상태를 `implemented`로 갱신. - **범위 밖:** FanTalk 원글 작성, nested reply 작성, 별도 reply DELETE/hard delete, root cascade, 언어 감지· `languageCode` 변경, legacy/public endpoint, 신규 dependency·DDL 변경. **Interfaces:** - `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` - Request JSON: `content?: string | null`, `isActive?: boolean | null`; 두 필드 동시 입력 허용, `{}`와 explicit null은 성공 no-op, 미지 필드는 400. - Response: `ApiResponse`. `data.fanTalkId`는 수정한 `replyId`, `creatorReplies`는 빈 배열. **Files:** - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDto.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyUpdateTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyUpdateContractTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **RED:** content-only, isActive-only, 두 필드 동시 수정, `{}`·explicit null no-op과 응답의 reply ID·빈 `creatorReplies`를 actual PUT으로 고정한다. 빈 문자열·공백 content도 레거시처럼 non-null 값으로 반영되는지 포함한다. - [x] **RED:** 비활성 reply의 `isActive=true` 재활성화와 content 수정을 허용하되, 비활성 root, 다른 character/root의 reply, 팬 작성 row, root row, 잘못된 direct-parent 관계는 400/no mutation인지 고정한다. - [x] **RED:** malformed JSON, 미지 필드, 미지원 media type, JWT/DB role 조합과 KO/EN/JA 오류에서 DB/event no-side-effect와 415 `Accept` header를 확인한다. - [x] **GREEN:** strict request reader와 JSON `consumes`를 사용하고, repository가 reply ID·target creator/writer·path root ID·활성 root·root parent null을 한 query 경계에서 검증한다. reply의 `isActive`는 조회 조건에 넣지 않는다. - [x] **GREEN:** facade는 non-null `content`와 `isActive`만 entity에 반영하고 `languageCode`와 event를 건드리지 않는다. 응답은 기존 `CreatorChannelFanTalkResponse.from(reply, cloudFrontHost)`를 재사용한다. - [x] **REFACTOR:** 신규 응답 DTO·dependency·DDL·추상화를 만들지 않고 FanTalk/common·legacy modifyCheers 영향 범위 회귀, `ktlintCheck`, OpenAPI 37개 상태와 `git diff --check`를 기록한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyUpdateTest ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest \ --tests kr.co.vividnext.sodalive.explorer.ExplorerServiceTest ./gradlew ktlintCheck git diff --check ``` #### Phase 6 FanTalk 답변 수정 후속 Gate **Goal 실행 `P6-R4-GATE`:** `REV-059` 구현 뒤 레거시 field/state/response parity와 관리자 target/root/reply 경계를 재검토한다. - [x] **`P6-R4-GATE` 완료:** `P6-R4` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고 Phase 6 리뷰와 Progress를 갱신한다. - **시작 조건:** `P6-R4` 완료. - **완료 증거:** 답변 수정 정상·no-op·재활성화·cross-target no-side-effect, ADMIN/common 오류, 37번째 controller mapping과 `implemented` 상태 일치. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 6 FanTalk 비활성 root 삭제 문서 계약 후속 보완 - [x] **Task 6.10: FanTalk 비활성 root 삭제의 no-op 문서 정합화** **Goal 실행 `P6-R5`:** `DELETE /fan-talks/{fanTalkId}`의 동일 target·팬 작성 root가 이미 비활성인 경우 성공 no-op이라는 OpenAPI·구현·회귀 테스트의 현재 계약에 맞춰 PRD와 `api-contract.md`의 상충 설명을 동기화한다. - **추적 review ID:** `REV-070`. - **시작 조건:** `phase6-fantalk-review.md` 8차 정적 리뷰 판정과 OpenAPI/구현/test의 동일한 no-op 근거 존재. - **완료 증거:** PRD Edge Cases와 `api-contract.md`의 FanTalk delete 설명이 동일 target 비활성 팬 root 200 `data: null` no-op, 미존재·다른 target·creator root·reply 400으로 일치하고 OpenAPI diff는 없음. - **범위 밖:** runtime/controller/facade/repository/test 변경, OpenAPI schema/path/status 변경, reply 삭제 의미, legacy/public endpoint. - **결정 근거:** OpenAPI를 기계 계약 원본으로 두고 현재 구현·회귀와 일치하는 no-op을 유지한다. PRD의 400 문장이 최신 제품 의도라면 이 Goal을 시작하지 않고 OpenAPI·runtime/test 변경 범위를 먼저 재확정한다. - **TDD 예외 사유:** 실행 동작을 변경하지 않고 상충하는 설명 문서만 현재 기계 계약과 구현 증거에 맞추는 문서 Task다. - **대체 검증 방법:** OpenAPI DELETE description, facade/repository 분기, `shouldNoopInactiveFanRootInSameTarget` 테스트 소스와 두 설명 문서를 정적으로 대조한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDeleteTest.kt` - [x] **DOCUMENT:** PRD와 `api-contract.md`의 상충 문장을 OpenAPI·runtime의 비활성 동일-target 팬 root 성공 no-op 계약으로 최소 수정한다. - [x] **STATIC:** 미존재·다른 target·creator root·reply는 400이라는 구분과 root row-only soft delete 의미가 유지되는지 대조한다. - [x] **SCOPE:** production/test/OpenAPI diff가 없고 문서 링크·용어·상태가 일치하는지 `git diff --check`와 정적 검색으로 확인한다. #### Phase 6 FanTalk 삭제 문서 계약 후속 Gate **Goal 실행 `P6-R5-GATE`:** `REV-070` 수정 뒤 PRD·plan·OpenAPI·계약 설명·구현 증거의 FanTalk 삭제 의미를 재검토한다. - [x] **`P6-R5-GATE` 완료:** `P6-R5` 완료 후 정적 대조와 diff check를 fresh 실행하고 Phase 6 리뷰와 Progress를 갱신한다. - **시작 조건:** `P6-R5` 완료. - **완료 증거:** 비활성 동일-target 팬 root no-op와 나머지 거부 경계가 모든 규범 문서에서 일치. - **범위 밖:** Gate에서 production/test/OpenAPI 변경. ```bash rg -n '비활성|no-op|FanTalk.*삭제' \ docs/20260724_AI캐릭터_관리자_API/prd.md \ docs/20260724_AI캐릭터_관리자_API/api-contract.md \ docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json ./gradlew tasks --all git diff --check ``` - 검증 기록(DOCUMENT): 무엇: FanTalk 비활성 root 삭제의 no-op 문서 정합화. 왜: OpenAPI·구현·회귀는 같은 target 비활성 팬 root 삭제를 200 `data:null` no-op으로 고정하지만 PRD 일부 문장이 400 거부로 설명했기 때문이다. 어떻게: PRD와 `api-contract.md`의 삭제 설명을 같은 target 비활성 팬 root no-op, creator root·reply·다른 target·미존재 root 400으로 동기화했다. 결과: runtime/test/OpenAPI 변경 없이 설명 문서만 갱신했다. - 검증 기록(GATE): 정적 대조와 diff check 결과는 `P7-R10-GATE`에 통합 기록한다. --- ### Phase 7: Final Integration & Quality Gate #### 목표 신규 AI 캐릭터 관리자 API 전체가 source spec을 충족하고 기존 legacy/public 계약을 회귀시키지 않았음을 검증한다. #### 범위와 비범위 - 포함: 전체 targeted test, 기존 회귀 테스트, ktlint, dependency/DDL/API contract 점검, 문서 검증 기록 누적. - 제외: 신규 기능 추가, unrelated refactor. #### 선행 Phase 및 의존성 - Phase 1~6 완료. #### API endpoint와 request/response contract - `api-contract.openapi.json`의 37개 operation이 모두 구현되어야 한다. - 모든 신규 endpoint가 JWT ADMIN + 현재 DB ADMIN 이중 인가와 공통 오류 envelope/i18n을 공유해야 한다. - legacy/public endpoint URI와 성공·오류 status/body/message diff가 없어야 한다. #### entity, repository, service 변경 - 신규 변경 없음. Phase 1~6 변경의 누락 import, unused code, package 의존 방향만 정리한다. - 신규 v2 application/domain이 기존 controller 또는 v2 API response DTO를 역참조하지 않는지 점검한다. #### DB migration - 없음. 새 DDL/migration 파일이 없는지 확인한다. #### transaction과 concurrency 고려사항 - 각 write slice의 transaction 시작부 target/ownership 검증이 유지되는지 점검한다. - 동시성 관련 targeted test가 실패 없이 통과해야 한다. #### 보안 및 개인정보 위험 - JWT 또는 현재 DB role이 비ADMIN인 접근, stale ADMIN claim, AI login/token/impersonation, private path 노출, cross-owner write가 없는지 전체 점검한다. #### acceptance criteria - Phase별 targeted test가 모두 통과한다. - 모든 신규 endpoint에서 stale ADMIN claim은 403이고, 등록된 모든 API 오류 분기는 정확한 비2xx status + `ApiResponse.error` + KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403 body는 envelope 계약의 예외다. - legacy/public 401/403/domain 오류의 status/body/message가 특성화 baseline과 동일하다. - 전체 회귀 필요성 판정 결과 실행 대상이면 `./gradlew test`가 통과하고, 생략 대상이면 근거와 대체 targeted/영향 범위 회귀 결과가 기록된다. - Kotlin 파일 변경이 있으면 `./gradlew ktlintCheck`가 통과한다. - 신규 dependency와 신규 DDL이 없다. - source spec acceptance criteria 25개를 각 Phase 결과와 대조해 누락이 없다. #### targeted test - Run: ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' ./gradlew ktlintCheck ``` - Conditional full regression: 공통 경계·여러 Phase 영향 또는 targeted 결과로 영향 범위를 판단할 수 없을 때만 `./gradlew test`를 추가 실행한다. #### 전체 회귀 테스트 영향 - legacy `/admin/*`, `/creator-admin/*`, public `/api/v2/*` 관련 테스트 전체가 회귀 범위다. - 기존 성공·오류 request/response 계약 변경이 없음을 controller/DTO diff와 legacy 오류 특성화 테스트로 확인한다. #### rollback 전략 - 신규 `/api/v2/admin/ai-characters` controller bean 비활성화 또는 신규 package 제거로 기능 표면을 되돌린다. - DB schema 변경이 없으므로 rollback은 code revert 중심이다. #### 권장 commit 경계 - `test: verify ai character admin api integration` - [x] **Task 7.1: 전체 targeted 및 필요 시 전체 회귀 test 실행** - **Goal 실행 `P7-T1`:** Phase 1~6 targeted test를 실행하고 위험 근거에 따라 전체 회귀 필요성을 판정해 결과를 확정한다. - **시작 조건:** `P2-GATE`~`P6-GATE` 완료와 Phase 1 완료 증거 확인. - **완료 증거:** targeted 결과와 전체 회귀 실행 또는 생략 판정·근거를 Progress와 하단 검증 기록에 누적. - **범위 밖:** 실패와 무관한 기능 추가·리팩터링. - TDD 예외 사유: 구현 완료 후 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다. - 대체 검증 방법: Phase 1~6 targeted test, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, 기존 admin security test를 먼저 실행한다. 공통 경계·여러 Phase 영향 또는 targeted 실패로 영향 범위를 판단할 수 없을 때만 전체 회귀를 실행한다. - REFACTOR: 실패가 있으면 관련 Phase Task로 되돌려 최소 수정 후 다시 실행한다. - Verify: 위 targeted command를 실행하고 전체 회귀 필요성을 판정한다. 실행 시 `./gradlew test` 결과를, 생략 시 근거와 대체 회귀 범위를 이 문서 하단 검증 기록에 남긴다. - [x] Phase 1~6 targeted test를 실행하고 각 결과를 기록한다. - [x] 공통 경계·여러 Phase 변경과 targeted 결과를 근거로 전체 회귀 필요성을 판정한다. - [x] 필요하면 `./gradlew test`의 exit code·실패 수를 기록하고, 불필요하면 생략 근거와 대체 회귀 범위를 기록한다. - [x] 실패가 있으면 소유 Phase에 별도 회귀 수정 Goal을 추가하고 `P7-T1`을 완료 처리하지 않는다. - [x] **Task 7.2: API contract와 변경 범위 점검** - **Goal 실행 `P7-T2`:** API·architecture·dependency·DDL·diff와 문서 추적성을 read-only로 최종 점검한다. - **시작 조건:** `P7-T1` 완료. - **완료 증거:** 아래 점검 체크박스, `ktlintCheck`, source spec acceptance criteria 추적 결과와 Progress 기록. - **범위 밖:** 신규 기능 추가, 확정되지 않은 계약 보정. - TDD 예외 사유: diff/architecture 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다. - 대체 검증 방법: legacy/public 성공·오류 status/body/message 특성화 baseline 통과, controller/DTO schema 변경 없음, 신규 dependency 없음, 신규 DDL 없음, 신규 v2 application/domain에서 기존 controller와 v2 response DTO 역참조 없음. - REFACTOR: 불필요한 import, 역방향 의존, 관련 없는 변경을 제거하고 diff를 다시 확인한다. - Verify: `git diff --name-only`, `./gradlew ktlintCheck` - [x] `git diff --name-only`와 `git diff --check`로 변경 범위와 문서/코드 오류를 확인한다. - [x] `build.gradle.kts`와 migration/DDL 경로를 확인해 신규 dependency·DDL 0건을 기록한다. - [x] legacy/public controller·DTO 외부 계약 diff와 신규 application/domain의 역방향 import 0건을 확인한다. - [x] source spec acceptance criteria 25개를 Phase Goal/Gate 완료 증거에 대조한다. - [x] `./gradlew ktlintCheck`와 `./gradlew tasks --all` 결과를 기록한다. #### Phase 7 Gate **Goal 실행 `P7-GATE`:** 모든 Phase의 완료 증거와 최신 전체 검증을 대조해 AI 캐릭터 관리자 API의 최종 완료 여부를 판정한다. - [x] **`P7-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-T1`, `P7-T2` 완료. - **완료 증거:** 미완료 Goal·미처리 review finding·보류 없는 차단 사항 0건, 아래 완료 조건과 최종 Progress 기록. - **범위 밖:** Gate에서 직접 production code 수정, test 삭제·skip·완화. - [x] Phase 1~6의 Task/Gate 완료 증거와 하단 검증 기록이 일치한다. - [x] 모든 확정 review finding이 수정 완료 또는 근거 있는 제외로 종결됐다. - [x] 최신 targeted·ktlint·문서 명령이 성공했고, 전체 회귀는 필요성 판정에 따라 성공 결과 또는 생략 근거가 기록됐다. - [x] 남은 항목과 최종 상태를 Progress 및 최종 보고 형식으로 기록한다. #### Phase 7 후속 리뷰 보완 - [x] **Task 7.3: 구현 현황 문서와 23개 operation metadata 동기화** **Goal 실행 `P7-R1`:** `REV-029`의 계획·계약 설명·OpenAPI 구현 상태 metadata를 모든 Phase 후속 Gate가 완료된 실제 23개 controller mapping과 동기화한다. - **추적 review ID:** `REV-029`. - **시작 조건:** `P2-R6-GATE`, `P3-R9-GATE`, `P4-R1-GATE`, `P5-R1-GATE`, `P6-R1-GATE` 완료. - **완료 증거:** plan 상태표/Endpoint Contract Summary와 `api-contract.md`가 23개 구현 완료를 표시하고, OpenAPI 23개 operation의 `x-implementation-status`가 모두 `implemented`이며 controller mapping도 정확히 23개인 정적 대조, OpenAPI validate/client 생성과 Progress 기록. - **범위 밖:** path/request/response schema 변경, operation 추가·삭제, production 코드 변경, 과거 완료 기록 삭제. - **TDD 예외 사유:** 실행 코드를 변경하지 않는 구현 현황·계약 metadata 문서 정합성 Task다. - **대체 검증 방법:** OpenAPI operation/status 개수와 controller mapping을 기계 집계하고 validator/client generator로 schema 비변경을 확인한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/**/*Controller.kt` - [x] plan 현재 상태와 Endpoint Contract Summary를 Phase 2~6 후속 Gate 완료 상태로 동기화한다. - [x] `api-contract.md`의 구현/정합화/예정 operation 수를 실제 23개 구현 완료 상태로 동기화한다. - [x] OpenAPI 23개 operation의 `x-implementation-status`를 `implemented`로 바꾸고 operation/status 개수를 단언한다. - [x] 신규 prefix controller mapping이 OpenAPI와 정확히 23개로 일치하고 계약 밖 route가 0개인지 대조한다. - [x] OpenAPI validate, TypeScript client 생성/compile, `./gradlew tasks --all`, diff check 결과를 Progress에 기록한다. ```bash jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 23 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json npx --yes @openapitools/openapi-generator-cli validate \ -i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json npx --yes @openapitools/openapi-generator-cli generate \ -i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \ -g typescript-fetch \ -o /tmp/ai-character-admin-typescript-client ./gradlew tasks --all git diff --check ``` #### Phase 7 후속 리뷰 Gate **Goal 실행 `P7-R1-GATE`:** 모든 후속 리뷰 finding과 23개 operation 구현 현황 문서가 종결됐는지 최종 판정한다. - [x] **`P7-R1-GATE` 완료:** `P7-R1` 완료 후 Phase 2~6 후속 Gate와 문서/OpenAPI/controller 집계를 fresh 대조하고 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R1` 완료. - **완료 증거:** `REV-021`~`REV-029` 처리 완료, 미완료 Goal 0건, 23개 operation 문서/metadata/mapping 일치, validator/client/문서 명령과 diff check 성공. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. #### Phase 7 2차 통합 재판정 - [x] **Task 7.4: Phase 3~5 후속 보완 통합 검증** **Goal 실행 `P7-R2`:** `REV-030`~`REV-033` 수정 후 23개 operation과 공통 오류·ownership·동시성 계약을 다시 통합 검증한다. - **시작 조건:** `P3-R10-GATE`, `P4-R2-GATE`, `P5-R2-GATE` 완료. - **완료 증거:** 네 review ID 처리 완료, Phase 1~6 targeted와 전체 회귀, `ktlintCheck`, OpenAPI/controller 집계, dependency/DDL/diff 점검과 Progress 기록. - **범위 밖:** 신규 기능·operation/schema 추가, 완료된 Phase 1·2·6 동작 변경, unrelated refactor. - **TDD 예외 사유:** 여러 Phase의 회귀 수정 후 검증·판정 전용 Task이며 별도 production 동작을 추가하지 않는다. - **대체 검증 방법:** 각 소유 Phase Gate의 RED/GREEN 증거를 재사용하지 않고 통합 명령을 fresh 실행한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] `REV-030`~`REV-033`이 모두 처리 완료이고 미완료 소유 Goal이 없는지 확인한다. - [x] Phase 1~6 targeted 명령과 `./gradlew test` 전체 회귀를 fresh 실행한다. 여러 domain/Phase의 production 수정과 final Gate이므로 이번에는 전체 회귀를 생략하지 않는다. - [x] `./gradlew ktlintCheck`, OpenAPI 23개 operation/status, controller mapping 23개, 신규 dependency/DDL 0건과 `git diff --check`를 확인한다. - [x] Phase별 리뷰의 수정 후 검증 기록과 현재 상태표·Progress를 동기화한다. - 검증 기록: 무엇: `REV-030`~`REV-033` 수정 후 Phase 1~6 targeted, 전체 회귀, lint, OpenAPI/controller/dependency/DDL/diff 상태를 fresh 재검증했다. 왜: Phase 3~5 후속 보완이 여러 domain production/test를 변경했으므로 최종 Gate에서 전체 회귀를 생략하지 않기 위해. 어떻게: 아래 targeted 명령, `./gradlew test`, `./gradlew ktlintCheck`, `jq` operation/status assertion, controller mapping 23개 assertion, dependency/DDL diff, `git diff --check`를 실행했다. 결과: targeted는 `BUILD SUCCESSFUL in 2m 45s`, 전체 회귀는 `BUILD SUCCESSFUL in 7m 58s`, ktlint는 `BUILD SUCCESSFUL in 1s`, OpenAPI assertion은 `true`, controller mapping 23개 assertion과 dependency/DDL diff, diff check는 출력 없이 통과했다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' ./gradlew test ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 23 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 7 2차 통합 Gate **Goal 실행 `P7-R2-GATE`:** 모든 후속 수정과 검증 증거를 대조해 최종 완료 여부를 재판정한다. - [x] **`P7-R2-GATE` 완료:** `P7-R2` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를 `구현 완료`로 되돌린다. - **시작 조건:** `P7-R2` 완료. - **완료 증거:** `REV-030`~`REV-033` 처리 완료, targeted/전체 회귀/lint 성공, 23개 operation/mapping 유지, dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록: 무엇: `P7-R2-GATE`에서 미완료 Goal·미처리 finding·차단 사항 0건과 최종 문서 상태를 재판정했다. 왜: Phase 3~5 후속 Gate 완료 뒤 최종 완료 상태를 복구하기 위해. 어떻게: 하단 finding 표의 `REV-030`~`REV-033` 처리 완료, Phase 3~5 review 문서 판정, targeted/전체 회귀/lint/OpenAPI/controller/diff 결과를 대조했다. 결과: 후속 finding은 모두 처리 완료이고 23개 operation/mapping/status가 유지되어 문서 상태를 `구현 완료`로 갱신했다. #### Phase 7 3차 통합 재판정 - [x] **Task 7.5: Phase 2~4 후속 보완 통합 검증** **Goal 실행 `P7-R3`:** `REV-034`~`REV-037` 수정 후 23개 operation과 레거시 parity, 공통 오류·ownership 계약을 다시 통합 검증한다. - **시작 조건:** `P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE` 완료. - **완료 증거:** 네 review ID 처리 완료, Phase 1~6 targeted와 전체 회귀, `ktlintCheck`, OpenAPI/controller 집계, dependency/DDL/diff 점검과 Progress 기록. - **범위 밖:** 신규 기능·operation/schema 추가, 완료된 Phase 1·5·6 동작 변경, unrelated refactor. - **TDD 예외 사유:** 여러 Phase의 회귀 수정 후 검증·판정 전용 Task이며 별도 production 동작을 추가하지 않는다. - **대체 검증 방법:** 각 소유 Phase Gate의 증거를 재사용하지 않고 통합 명령을 fresh 실행한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] `REV-034`~`REV-037`이 모두 처리 완료이고 미완료 소유 Goal이 없는지 확인한다. - [x] Phase 1~6 targeted 명령과 `./gradlew test` 전체 회귀를 fresh 실행한다. 세 domain의 production 수정과 final Gate이므로 전체 회귀를 생략하지 않는다. - [x] `./gradlew ktlintCheck`, OpenAPI 23개 operation/status, controller mapping 23개, 신규 dependency/DDL 0건과 `git diff --check`를 확인한다. - [x] Phase별 리뷰의 수정 후 검증 기록과 현재 상태표·Progress를 동기화한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' ./gradlew test ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 23 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 7 3차 통합 Gate **Goal 실행 `P7-R3-GATE`:** 모든 후속 수정과 검증 증거를 대조해 최종 완료 여부를 재판정한다. - [x] **`P7-R3-GATE` 완료:** `P7-R3` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를 `구현 완료`로 되돌린다. - **시작 조건:** `P7-R3` 완료. - **완료 증거:** `REV-034`~`REV-037` 처리 완료, targeted/전체 회귀/lint 성공, 23개 operation/mapping 유지, dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록: 무엇: `REV-034`~`REV-037` 통합 재판정. 왜: 세 domain production 수정 후 23개 operation과 공통 오류·ownership 계약이 유지되는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping `rg` 23개, `git diff --check`, 변경 파일명 기반 dependency/DDL 점검을 실행했다. 결과: targeted와 전체 test, lint, jq가 모두 성공했고 mapping은 23개, `git diff --check`는 출력 없음, 신규 dependency/DDL 파일 변경은 없었다. #### Phase 7 4차 리뷰 보완 - [x] **Task 7.6: 완료 Task 상태와 Phase 4 DELETE 계약 설명 동기화** **Goal 실행 `P7-R4`:** 완료 증거가 존재하는 후속 Task 헤더와 현재 상태표를 동기화하고, Phase 4 시리즈 콘텐츠 해제 설명을 OpenAPI와 실제 controller route에 맞춘다. - **추적 review ID:** `REV-038`, `REV-039`. - **시작 조건:** `P7-R3-GATE` 완료와 `phase7-integration-review.md` 4차 정적 리뷰 판정 존재. - **완료 증거:** `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더가 기존 완료 증거와 같은 `[x]` 상태이고, Phase 4 DELETE 설명이 `/contents/{contentId}` path·request body 없음으로 정정되며 상태표·Progress·리뷰 문서가 다시 완료 상태로 동기화된다. - **범위 밖:** production/test/OpenAPI 변경, 기존 완료 증거 삭제·덮어쓰기, API operation 추가·삭제. - **TDD 예외 사유:** 실행 동작이 아닌 구현 계획의 완료 상태와 이미 확정된 API 설명을 정정하는 문서 전용 Task다. - **대체 검증 방법:** 미완료 Task 헤더 집계, OpenAPI 23개 operation/status, controller mapping과 Phase 4 DELETE route, 문서 명령 및 diff를 정적으로 대조한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt` - [x] 기존 후속 Gate와 2026-07-29 검증 기록을 근거로 완료된 네 Task 헤더만 `[x]`로 동기화한다. - [x] Phase 4 endpoint 설명의 시리즈 콘텐츠 해제를 `DELETE /series/{seriesId}/contents/{contentId}`와 request body 없음으로 정정한다. - [x] 상단 상태표, Goal Progress, 발견된 문제 표와 Phase 7 리뷰 판정을 `구현 완료` 상태로 동기화한다. - [x] `./gradlew tasks --all`, OpenAPI 23개 operation/status `jq`, controller mapping 집계, 미완료 Task header `rg`, `git diff --check` 결과를 누적 기록한다. ```bash ./gradlew tasks --all jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 23 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json rg -n '^- \[ \] \*\*Task' docs/20260724_AI캐릭터_관리자_API/plan-task.md rg -n '@(Get|Post|Put|Delete)Mapping' \ src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter git diff --check ``` #### Phase 7 5차 통합 보완 - [x] **Task 7.7: Phase 2~5 후속 보완 통합 재판정** **Goal 실행 `P7-R5`:** Phase 2~5의 primitive required/nullability와 커뮤니티 목록 계약 보완 뒤 23개 관리자 operation의 계약, 공통 보안·오류, ownership과 legacy 회귀를 통합 재판정한다. - **추적 근거:** `REV-040`~`REV-043`, `DEC-P5-LIST-001`. - **시작 조건:** `P2-R8-GATE`, `P3-R12-GATE`, `P4-R4-GATE`, `P5-R3-GATE`, `P5-R4-GATE` 완료. - **완료 증거:** Phase 2~5 focused/영향 범위 회귀와 통합 회귀·lint 성공, OpenAPI operation/status와 controller mapping 23개 유지, dependency/DDL 추가 없음, Phase별 리뷰·상태표·Progress 동기화. - **범위 밖:** Gate 목적과 무관한 production refactor, 전역 Jackson 정책 변경, 공개 API schema 변경. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - [x] Phase 2~5의 focused와 각 package/common 영향 범위 회귀가 fresh 성공했는지 Gate 증거를 대조한다. - [x] 커뮤니티 목록의 timezone 제거, pagination wrapper와 active owner count·hasNext 계약을 확인한다. - [x] JWT ADMIN 이중 인가, target/owner 오류, JSON 오류 envelope과 no-side-effect 회귀를 통합 범위에서 확인한다. - [x] OpenAPI 23개 operation/status와 controller mapping 23개, dependency/DDL 무변경을 확인한다. - [x] Phase별 review의 finding 상태와 상단 상태표·Progress를 최종 판정에 맞게 동기화한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' ./gradlew test ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 23 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 7 5차 통합 Gate **Goal 실행 `P7-R5-GATE`:** 모든 Phase 2~5 후속 수정과 검증 증거를 대조해 최종 완료 여부를 판정한다. - [x] **`P7-R5-GATE` 완료:** `P7-R5` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R5` 완료. - **완료 증거:** `REV-040`~`REV-043`과 `DEC-P5-LIST-001` 처리 완료, focused/통합/전체 회귀와 lint 성공, 23개 operation/mapping 및 23개 `implemented` 유지, dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화. - **범위 밖:** Gate에서 production code 또는 공개 API schema 변경. - 검증 기록: 무엇: Phase 2~5 후속 보완과 Community 목록 계약 변경 뒤 최종 통합 상태를 재판정했다. 왜: primitive required/nullability와 목록 wrapper 변경이 23개 관리자 operation, 공통 보안·오류, ownership, legacy 회귀를 깨뜨리지 않는지 확인해야 했기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping `rg`, dependency/DDL 경로 diff 검색, `git diff --check`를 fresh 실행했다. 결과: targeted는 `BUILD SUCCESSFUL in 2m 21s`, 전체 test는 `BUILD SUCCESSFUL in 5m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 881ms`, OpenAPI assertion은 `true`, controller mapping은 23개, dependency/DDL 검색과 `git diff --check`는 출력이 없었다. 미완료 Goal·미처리 finding·차단 사항은 0건이다. #### Phase 7 후속 기능 통합 보완 - [x] **Task 7.8: 36개 operation 후속 기능 통합 재판정** **Goal 실행 `P7-R6`:** Phase 2~6의 후속 기능과 시리즈 상세 정합화가 끝난 뒤 36개 관리자 operation의 계약, 공통 보안·오류, actor·ownership, soft delete와 legacy/public 회귀를 통합 재판정한다. - **추적 근거:** `REV-044`~`REV-049`, `DEC-COMMENT-001`, `DEC-CHAR-COMMENT-001`, `DEC-FANTALK-DELETE-001`, `DEC-REGISTRATION-REFERENCE-001`, `DEC-SERIES-DETAIL-001`. - **시작 조건:** `P2-R9-GATE`, `P3-R13-GATE`, `P4-R5-GATE`, `P4-R6-GATE`, `P5-R5-GATE`, `P6-R2-GATE` 완료. - **완료 증거:** Phase 2~6 focused/영향 범위 회귀와 통합·전체 회귀·lint 성공, OpenAPI 36개 `implemented`와 controller mapping 36개 일치, 미구현 캐릭터 직접 댓글 route 없음, dependency/DDL 추가 없음, Phase별 리뷰·상태표·Progress 동기화. - **범위 밖:** 승인 범위 밖 기능, 캐릭터 직접 댓글 API, 전역 refactor, public/legacy API schema 변경. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - [x] Phase 2~6 신규 Goal의 focused와 각 package/common 영향 범위 회귀가 fresh 성공했는지 Gate 증거를 대조한다. - [x] 댓글 actor·parent·owner 경계, row-only soft delete, FanTalk fan root 삭제와 reply row 유지 결과를 통합 확인한다. - [x] 원작·장르 참조 조회와 시리즈 목록/상세 item parity, JWT ADMIN 이중 인가와 공통 오류 계약을 확인한다. - [x] OpenAPI operation/status와 controller mapping이 36개이며 캐릭터 직접 댓글 operation/mapping이 없는지 확인한다. - [x] 신규 dependency/DDL 무변경과 legacy/public 회귀를 확인하고 Phase별 review·상태표·Progress를 최종 동기화한다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' ./gradlew test ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 36 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36 ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 7 후속 기능 통합 Gate **Goal 실행 `P7-R6-GATE`:** 모든 후속 기능·정합화 Goal과 검증 증거를 대조해 최종 완료 여부를 판정한다. - [x] **`P7-R6-GATE` 완료:** `P7-R6` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R6` 완료. - **완료 증거:** `REV-044`~`REV-049` 처리 완료, focused/통합/전체 회귀와 lint 성공, 36개 operation/mapping/`implemented` 일치, 범위 제외 route 0개, dependency/DDL 추가 없음, Phase별 문서 동기화. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 7 UTC 날짜 계약 통합 보완 - [x] **Task 7.9: UTC 날짜 계약 36개 operation 통합 재판정** **Goal 실행 `P7-R7`:** Phase 3·5 UTC 날짜 계약 구현 뒤 36개 관리자 operation의 OpenAPI·runtime·보안·legacy/public 격리를 통합 재판정한다. - **추적 근거:** `REV-050`, `REV-051`, `DEC-UTC-DATE-001`. - **시작 조건:** `P3-R14-GATE`, `P5-R6-GATE` 완료. - **완료 증거:** 36개 operation과 controller mapping 유지, 36개 `implemented`, OpenAPI에서 `timezone` parameter/schema 0건, 영향 6개 operation의 UTC 계약·legacy/public 회귀·lint 성공, Phase별 리뷰·상태표·Progress 동기화. - **범위 밖:** 승인된 6개 operation 밖 날짜 schema 변경, 전역 timezone/Jackson 정책 변경, dependency·DDL 추가, public/legacy API 변경. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - [x] Phase 3 오디오 4개와 Phase 5 커뮤니티 2개 operation의 focused/Gate 증거를 대조한다. - [x] OpenAPI 36개 operation·36개 `implemented`, controller mapping 36개와 timezone parameter/schema 0건을 확인한다. - [x] 오디오 생성·상세·댓글과 커뮤니티 댓글의 UTC exact JSON, 기존 ownership/인가/오류·legacy/public 회귀를 확인한다. - [x] 신규 dependency/DDL·범위 밖 날짜 schema 변경이 없고 Phase별 review finding·상태표·Progress가 일치하는지 확인한다. - **`P7-R7` 검증(2026-07-29):** `P3-R14-GATE`와 `P5-R6-GATE` 증거를 대조했다. OpenAPI는 36개 operation, 36개 `implemented`, 0개 `alignment-required`, query `timezone` parameter 0개, `components.parameters.Timezone` 0개였고, 신규 prefix controller mapping도 36개였다. 영향 6개 operation의 UTC exact JSON은 오디오 focused `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행 `BUILD SUCCESSFUL in 4m 33s`와 각 Gate의 legacy/public 영향 범위 회귀로 확인했다. 내부 legacy 재사용을 위한 `timezone = UTC` 상수 호출 외 신규 관리자 외부 계약의 timezone query/body는 남지 않았다. ```bash ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' \ --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest \ --tests kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest ./gradlew ktlintCheck jq -e ' [.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations | ($operations | length) == 36 and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36 and ([.. | objects | select(.name? == "timezone" and .in? == "query")] | length) == 0 and (.components.parameters.Timezone? == null) ' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` #### Phase 7 UTC 날짜 계약 통합 Gate **Goal 실행 `P7-R7-GATE`:** `P3-R14-GATE`, `P5-R6-GATE`와 통합 증거를 대조해 최신 계약 구현 완료를 판정한다. - [x] **`P7-R7-GATE` 완료:** `P7-R7` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R7` 완료. - **완료 증거:** `REV-050`~`REV-051` 처리 완료, 영향 범위 회귀와 lint 성공, 36개 operation/mapping/`implemented` 일치, timezone parameter/schema 0건, dependency/DDL 추가 없음, Phase별 문서 동기화. - **범위 밖:** Gate에서 production code 또는 public/legacy API 변경. #### Phase 7 HTTP 경계 통합 재판정 - [x] **Task 7.10: optional pagination과 JSON·multipart media type 계약 통합 재판정** **Goal 실행 `P7-R8`:** `P2-R10-GATE`, `P3-R15-GATE`, `P3-R16-GATE`, `P4-R7-GATE`, `P5-R7-GATE`, `P5-R8-GATE`, `P6-R3-GATE`, `P6-R4-GATE`의 결과를 대조해 37개 관리자 operation의 query 기본값, request media type과 FanTalk 답변 수정 계약을 최종 재판정한다. - **추적 review ID:** `REV-052`~`REV-059`. - **시작 조건:** Phase 2~6의 여덟 소유 Gate 완료. - **완료 증거:** OpenAPI 37개 operation/고유 operationId와 controller 37개 mapping 일치, 영향 14개 operation의 optional pagination, JSON-only 또는 multipart part-level JSON/415, FanTalk 답변 수정 계약 및 공통 오류 header/envelope 회귀 성공, 미처리 finding 0건, dependency·DDL 추가 없음과 Phase별 리뷰·Progress 동기화. - **범위 밖:** 확정된 FanTalk 답변 수정 외 신규 route/schema/기능, legacy/public API 변경, 관련 없는 refactor. - [x] **STATIC:** OpenAPI 문법·내부 `$ref`·operationId·request media type·pagination parameter와 controller mapping을 대조한다. - [x] **REGRESSION:** Phase 3·5·6 focused와 공통 error/authorization 영향 범위 결과를 대조하고 전체 회귀 필요성을 판정한다. - [x] **SCOPE:** dependency/DDL/legacy-public 변경이 없고 37개 route가 일치하는지 확인한다. - [x] **DOCUMENT:** `REV-052`~`REV-059`, 상태표, Phase별 리뷰, Goal Progress와 검증 기록을 동기화한다. #### Phase 7 HTTP 경계 통합 Gate **Goal 실행 `P7-R8-GATE`:** `P7-R8` 증거와 미처리 finding을 대조해 구현 완료 복구 여부를 판정한다. - [x] **`P7-R8-GATE` 완료:** 미완료 Goal·미처리 finding·차단 사항 0건일 때만 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R8` 완료. - **완료 증거:** 영향 14개 operation을 포함한 37개 계약/mapping 정합성, 회귀·lint·diff 성공, 문서 동기화. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 7 multipart part 이름·문서 상태 통합 재판정 - [x] **Task 7.11: exact multipart part 계약과 37개 구현 상태 통합 정합화** **Goal 실행 `P7-R9`:** `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE` 결과를 대조해 8개 multipart operation의 허용 part 이름을 OpenAPI와 일치시키고, 완료 상태가 오래된 계획·계약 설명을 실제 37개 구현 상태와 동기화한다. - **추적 review ID:** `REV-060`~`REV-064`. - **시작 조건:** Phase 2~5의 네 소유 Gate 완료. - **완료 증거:** 8개 multipart operation의 미정의 part 400/no-side-effect와 기존 part/media type 회귀, OpenAPI 37개 operation/37개 `implemented`와 controller 37개 mapping 일치, `plan-task.md`와 `api-contract.md`의 route/완료/예정 수·FanTalk 답변 수정 상태 동기화, dependency·DDL 무변경. - **범위 밖:** OpenAPI path/schema 변경, 신규 route, legacy/public API, 전역 multipart resolver, 관련 없는 문서 이력 삭제. - **TDD 예외 사유:** production 동작은 Phase 2~5 소유 Task에서 TDD로 수정하며 이 Task는 통합 검증과 현황 문서 동기화만 수행한다. - **대체 검증 방법:** 소유 Gate의 actual endpoint 증거를 대조하고 OpenAPI status와 controller mapping을 기계 집계한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **STATIC:** OpenAPI의 8개 multipart schema `additionalProperties: false`와 operation별 허용/필수 part를 controller 및 actual endpoint 회귀와 대조한다. - [x] **REGRESSION:** Phase 2~5 focused와 공통 error/authorization 영향 범위, 전체 회귀 필요성을 판정해 실행 결과 또는 생략 근거를 기록한다. - [x] **DOCUMENT:** Endpoint Contract Summary, `api-contract.md` 상단 집계·endpoint 표·client 생성 설명을 37개 route/37개 구현 완료/예정 0개로 동기화하고 과거 완료 이력은 보존한다. - [x] **SCOPE:** OpenAPI 37개 operationId/status와 controller mapping, dependency·DDL·legacy/public 무변경, `ktlintCheck`, `git diff --check`를 확인한다. #### Phase 7 multipart part 이름·문서 상태 통합 Gate **Goal 실행 `P7-R9-GATE`:** `REV-060`~`REV-064`의 소유 Gate와 통합 증거를 대조해 최신 계약 구현 완료 복구 여부를 판정한다. - [x] **`P7-R9-GATE` 완료:** 미정의 part 회귀와 문서 상태가 모두 정합하고 미처리 finding·차단 사항이 0건일 때만 문서 상태를 `구현 완료`로 갱신한다. - **시작 조건:** `P7-R9` 완료. - **완료 증거:** 8개 multipart contract, 37개 operation/mapping/status, 문서 집계, 회귀·lint·diff, dependency·DDL·legacy/public 무변경 확인. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. #### Phase 7 8차 리뷰 후속 통합과 finding 상태 동기화 - [x] **Task 7.12: Phase 2~6 후속 Gate 통합 및 리뷰 상태 정합화** **Goal 실행 `P7-R10`:** `REV-065`~`REV-070` 소유 Gate의 완료 증거를 통합 대조하고, 이미 완료된 `REV-060`~`REV-064`와 신규 finding의 상태·상단 Phase 집계·현재 Goal을 실제 결과에 맞게 동기화한다. - **추적 review ID:** `REV-071`. - **시작 조건:** `P2-R12-GATE`, `P3-R18-GATE`, `P4-R9-GATE`, `P4-R10-GATE`, `P5-R10-GATE`, `P6-R5-GATE` 완료. - **완료 증거:** 파일·일반 form-field를 포함한 8개 multipart operation 회귀, Series 장르 0 이하 경계, FanTalk 비활성 root 삭제 문서 계약, 37개 OpenAPI operation/controller mapping/status, finding 표와 Phase 집계가 모두 일치. - **범위 밖:** 신규 route/schema/기능, legacy/public API, 관련 없는 완료 이력 삭제, 신규 dependency·DDL. - **TDD 예외 사유:** production 수정은 각 소유 Phase Task에서 수행하고 이 Task는 통합 검증과 상태 문서 동기화만 담당한다. - **대체 검증 방법:** 각 소유 Gate의 actual endpoint 증거를 대조하고 OpenAPI operation/status, controller mapping, 문서 finding 상태를 기계 집계한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **INTEGRATION:** `REV-065`~`REV-070` 소유 Gate의 focused/영향 범위 또는 문서 정적 검증 증거를 대조한다. - [x] **STATIC:** OpenAPI 37개 operationId/status와 controller 37개 mapping, 8개 multipart schema, dependency·DDL·legacy/public 무변경을 확인한다. - [x] **DOCUMENT:** `REV-060`~`REV-071` 상태, 상단 Phase 완료 수, 현재 Phase/Goal과 Phase별 리뷰 결론을 실제 완료 상태에 맞춘다. - [x] **SCOPE:** 필요한 범위의 회귀·`ktlintCheck`·`git diff --check` 결과 또는 생략 근거를 기록한다. #### Phase 7 8차 리뷰 후속 통합 Gate **Goal 실행 `P7-R10-GATE`:** 8차 리뷰 finding과 문서 상태가 모두 종결됐는지 최종 판정한다. - [x] **`P7-R10-GATE` 완료:** 미처리 finding·차단 사항이 0건이고 최신 계약·구현·문서가 일치할 때만 문서 상태를 `구현 완료`로 되돌린다. - **시작 조건:** `P7-R10` 완료. - **완료 증거:** Phase 2~6 소유 Gate, OpenAPI/controller 집계, Phase별 리뷰·finding 표·상단 상태 정합. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --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.series.*' \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \ --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \ --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest ./gradlew ktlintCheck ./gradlew tasks --all jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` - 검증 기록(RED): Phase 2~5 multipart 일반 form-field와 Phase 4 `genreId <= 0` focused RED 묶음에서 신규 multipart/genre 36건 실패를 확인했다. 최초 compile error 2회는 community test helper의 `MockPart` 연결 방식 문제였고, production 변경 전 테스트 구성만 고쳐 재실행했다. - 검증 기록(GREEN): 같은 focused 묶음을 재실행해 `BUILD SUCCESSFUL in 1m 17s`를 확인했다. - 검증 기록(INTEGRATION): `./gradlew test --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.series.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest` → `BUILD SUCCESSFUL in 4m 11s`. - 검증 기록(STATIC): OpenAPI 집계 `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`, controller mapping 37개, FanTalk 삭제 no-op 문서·OpenAPI·테스트 정적 대조 완료. - 검증 기록(SCOPE): `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 51s`; `git diff --check` → 출력 없음. 전체 `./gradlew test`는 Phase 2~6 후속 변경의 직접 영향 범위를 위 focused 통합 명령이 포함하므로 생략했다. #### Phase 7 9차 리뷰 후속 통합 재판정 - [x] **Task 7.13: preview 검증 복구 후 37개 operation 통합 재판정** **Goal 실행 `P7-R11`:** `P3-R19-GATE`의 preview 검증 복구 증거를 포함해 Phase 1~7 리뷰 결론, OpenAPI 37개 operation과 controller mapping, finding·상태 문서를 다시 대조한다. - **추적 review ID:** `REV-072`. - **시작 조건:** `P3-R19-GATE` 완료. - **완료 증거:** `REV-072` 처리 완료, Phase 3 preview actual endpoint/legacy 회귀 증거, OpenAPI 37개 operationId와 controller 37개 mapping, 미처리 finding·차단 사항 0건, 상태표·Phase별 리뷰·Progress 동기화. - **범위 밖:** 신규 route/schema/기능, legacy/public 계약 변경, 관련 없는 완료 이력 삭제, 신규 dependency·DDL. - **TDD 예외 사유:** production 보완은 Phase 3에서 TDD로 수행하며 이 Task는 통합 증거와 문서 상태만 재판정한다. - **대체 검증 방법:** `P3-R19-GATE` 결과를 대조하고 OpenAPI operation/status, controller mapping, Phase별 review/finding 상태를 기계 집계한다. **Files:** - Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md` - Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/prd.md` - Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` - [x] **INTEGRATION:** `P3-R19-GATE`의 actual endpoint·legacy 회귀와 no-side-effect 증거를 대조한다. - [x] **STATIC:** OpenAPI 37개 operationId/status와 controller 37개 mapping, dependency·DDL·legacy/public 무변경을 확인한다. - [x] **DOCUMENT:** `REV-072`, 상단 Phase 완료 수, 현재 Phase/Goal, Phase 3·7 리뷰와 Progress를 실제 결과에 맞춘다. - [x] **SCOPE:** 필요한 범위의 회귀·`ktlintCheck`·`git diff --check` 결과 또는 생략 근거를 기록한다. - 검증 기록: 무엇: `P3-R19-GATE` 증거를 포함해 37개 operation 통합 상태를 재판정했다. 왜: `REV-072` 처리 전에는 route/schema 집계만으로 Phase 7 완료 판정을 유지할 수 없었기 때문이다. 어떻게: Phase 3 actual endpoint·legacy 회귀, OpenAPI implemented count, controller mapping count, dependency/DDL diff, `ktlintCheck`, `git diff --check` 결과를 대조했다. 결과: OpenAPI `x-implementation-status=implemented` 37개, controller mapping 37개, 신규 dependency·DDL 변경 없음, 미처리 finding 0건으로 재판정했다. #### Phase 7 9차 리뷰 후속 통합 Gate **Goal 실행 `P7-R11-GATE`:** `REV-072`와 문서 상태가 모두 종결됐는지 최종 판정한다. - [x] **`P7-R11-GATE` 완료:** 미처리 finding·차단 사항이 0건이고 최신 계약·구현·문서가 일치할 때만 문서 상태를 `구현 완료`로 되돌린다. - **시작 조건:** `P7-R11` 완료. - **완료 증거:** Phase 3 소유 Gate, OpenAPI/controller 집계, Phase별 리뷰·finding 표·상단 상태 정합. - **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경. ```bash ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \ --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test' ./gradlew ktlintCheck jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json git diff --check ``` - Gate 검증 기록: 무엇: Phase 3 소유 Gate와 Phase 7 통합 문서 상태를 최종 대조했다. 왜: 최신 계약·구현·문서가 모두 일치할 때만 최종 상태를 `구현 완료`로 되돌릴 수 있기 때문이다. 어떻게: `jq` JSON 문법 확인, OpenAPI implemented 37개 집계, controller mapping 파일별 4/5/10/9/8/1 합계 37개 집계, dependency·DDL diff, 영향 범위 회귀와 lint·diff 결과를 확인했다. 결과: `REV-072`는 처리 완료이고 Phase 1~7 미처리 finding·차단 사항은 0건이므로 Phase 7 Gate를 완료로 판정했다. --- ## 실행 순서와 의존성 | 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 | |---:|---|---|---|---| | 1 | `P2-R1` | `P2-H1`, `P2-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 | | 2 | `P2-T3` → `P2-T4` → `P2-T5` → `P2-T6` | `P2-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 | | 3 | `P2-GATE` | Phase 2 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 | | 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 | `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 | `P23-CONTRACT-1` → `P23-CONTRACT-2` → `P23-CONTRACT-3` → `P23-CONTRACT-GATE` | `P3-R5-GATE`, 사용자 계약 확정 | 아니요 | 문서 또는 runtime 불일치 소유 Goal에서 수정 | | 16 | `P4-T1`~`P4-T6` → `P4-GATE` | `P23-CONTRACT-GATE` | 아니요 | 실패 소유 Task로 되돌림 | | 17 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 | | 18 | `P6-T1`~`P6-T5` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 | | 19 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 | | 20 | `P2-R6` → `P2-R6-GATE` → `P3-R9` → `P3-R9-GATE` → `P4-R1` → `P4-R1-GATE` → `P5-R1` → `P5-R1-GATE` → `P6-R1` → `P6-R1-GATE` → `P7-R1` → `P7-R1-GATE` | 2026-07-28 Phase 1~7 정적 리뷰 | 아니요 | 확정 finding 소유 Goal에서 최소 수정·검증 후 다음 Gate 수행 | | 21 | `P3-R10` → `P3-R10-GATE` → `P4-R2` → `P4-R2-GATE` → `P5-R2` → `P5-R2-GATE` → `P7-R2` → `P7-R2-GATE` | 2026-07-28 Phase별 후속 정적 리뷰 | 아니요 | `REV-030`~`REV-033` 소유 Goal에서 최소 수정·검증 후 통합 재판정 | | 22 | `P2-R7` → `P2-R7-GATE` → `P3-R11` → `P3-R11-GATE` → `P4-R3` → `P4-R3-GATE` → `P7-R3` → `P7-R3-GATE` | 2026-07-28 3차 Phase별 정적 리뷰 | 아니요 | `REV-034`~`REV-037` 소유 Goal에서 최소 수정·검증 후 통합 재판정 | | 23 | `P2-R8` → `P2-R8-GATE` → `P3-R12` → `P3-R12-GATE` → `P4-R4` → `P4-R4-GATE` → `P5-R3` → `P5-R3-GATE` → `P5-R4` → `P5-R4-GATE` → `P7-R5` → `P7-R5-GATE` | 2026-07-29 5차 Phase별 정적 리뷰와 Community 목록 계약 확정 | 아니요 | `REV-040`~`REV-043`과 `DEC-P5-LIST-001`을 최소 보완한 뒤 통합 재판정 | | 24 | `P2-R9` → `P2-R9-GATE` → `P3-R13` → `P3-R13-GATE` → `P4-R5` → `P4-R5-GATE` → `P4-R6` → `P4-R6-GATE` → `P5-R5` → `P5-R5-GATE` → `P6-R2` → `P6-R2-GATE` → `P7-R6` → `P7-R6-GATE` | 2026-07-29 승인 후속 기능과 시리즈 상세 정합화 | 아니요 | `REV-044`~`REV-049` 소유 Goal에서 최소 구현·검증 후 다음 Phase Gate 수행 | | 25 | `P3-R14` → `P3-R14-GATE` → `P5-R6` → `P5-R6-GATE` → `P7-R7` → `P7-R7-GATE` | 2026-07-29 UTC 날짜 계약 확정 | 아니요 | `REV-050`~`REV-051` 소유 Goal에서 6개 operation만 최소 정합화한 뒤 통합 재판정 | | 26 | `P2-R10` → `P2-R10-GATE` → `P3-R15` → `P3-R15-GATE` → `P3-R16` → `P3-R16-GATE` → `P4-R7` → `P4-R7-GATE` → `P5-R7` → `P5-R7-GATE` → `P5-R8` → `P5-R8-GATE` → `P6-R3` → `P6-R3-GATE` → `P6-R4` → `P6-R4-GATE` → `P7-R8` → `P7-R8-GATE` | 2026-07-29 6차 Phase별 정적 리뷰와 FanTalk 답변 수정 계약 확정 | 아니요 | `REV-052`~`REV-059` 소유 HTTP 경계·신규 답변 수정만 최소 보완한 뒤 통합 재판정 | | 27 | `P2-R11` → `P2-R11-GATE` → `P3-R17` → `P3-R17-GATE` → `P4-R8` → `P4-R8-GATE` → `P5-R9` → `P5-R9-GATE` → `P7-R9` → `P7-R9-GATE` | 2026-07-29 7차 Phase별 정적 리뷰 | 아니요 | `REV-060`~`REV-064`의 exact multipart part와 문서 상태만 최소 보완한 뒤 통합 재판정 | | 28 | `P2-R12` → `P2-R12-GATE` → `P3-R18` → `P3-R18-GATE` → `P4-R9` → `P4-R9-GATE` → `P4-R10` → `P4-R10-GATE` → `P5-R10` → `P5-R10-GATE` → `P6-R5` → `P6-R5-GATE` → `P7-R10` → `P7-R10-GATE` | 2026-07-29 8차 Phase별 정적 리뷰 | 아니요 | `REV-065`~`REV-071` 소유 전체 multipart part·장르 ID·FanTalk 문서·상태만 최소 보완한 뒤 통합 재판정 | | 29 | `P3-R19` → `P3-R19-GATE` → `P7-R11` → `P7-R11-GATE` | 2026-07-29 9차 Phase별 정적 리뷰 | 아니요 | `REV-072` preview 검증을 Phase 3에서 복구한 뒤 37개 operation 상태를 통합 재판정 | ## 변경 금지·중단 규칙 - PRD와 Endpoint Contract Summary를 근거 없이 변경하거나 제공되지 않은 DTO·enum·오류 status/key를 추정하지 않는다. - 기존 완료 체크박스, 검증 기록과 Progress를 삭제·되돌리거나 덮어쓰지 않는다. - review 후보는 재현·판정 전 production 수정으로 전환하지 않고, 확정 항목만 관련 Goal 또는 신규 회귀 수정 Goal로 처리한다. - test를 삭제·skip·완화하거나 관련 없는 refactor·dependency·DDL을 추가해 Gate를 통과시키지 않는다. - JWT, token, signed URL 전체, private path와 파일 본문을 문서·fixture·log에 기록하지 않는다. - 일부 체크박스, 일부 test 또는 코드 작성만 끝난 상태에서는 Goal을 `complete`로 갱신하지 않는다. ## Goal Progress 기존 기록을 삭제하거나 덮어쓰지 않고 Goal 실행 결과를 차수별로 누적한다. ### Phase 1~7 10차 정적 리뷰 완료 — 2026-07-30 - 상태: 판정 완료, `구현 완료` 유지 - 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 현재 production/test 소스를 Phase별로 다시 대조했다. - 왜: 기존 컴파일·테스트 통과 기록과 별개로 `REV-072` 처리 뒤 계약·소유권·부작용 경계와 최종 문서 상태가 유지되는지 확인하기 위해서다. - 어떻게: security/CORS/target resolver, Character, AudioContent·댓글, Series, Community·댓글, FanTalk의 controller/facade/service/repository/test를 `rg`·`sed`·`jq`로 정적 검토했다. 사용자 지시에 따라 Gradle 컴파일·테스트는 실행하지 않았다. - Phase별 결과: Phase 1~6은 신규 확정 finding이 없고, Phase 7도 추가 통합 보완이 필요하지 않다. 기존 `REV-001`~`REV-072` 72건은 모두 `처리 완료` 상태다. - 정적 검증: OpenAPI JSON과 내부 `$ref`, operation 37개·고유 operationId 37개·`implemented` 37개, controller mapping Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37을 확인했다. - 계획 전환: 신규 회귀 수정 Task/Goal/Gate 없음. Phase별 완료 수 7/7, 18/18, 29/29, 16/16, 16/16, 10/10, 13/13과 `구현 완료` 상태를 유지한다. ### `P3-R19` / `P3-R19-GATE` / `P7-R11` / `P7-R11-GATE` 완료 — 2026-07-30 - 상태: 완료, 최종 `구현 완료` 재판정 - 무엇을: v2 오디오 생성의 preview 쌍·형식·최소 15초 검증을 legacy creator 생성과 같은 공유 parsed request overload로 복구하고, Phase 3·7 리뷰와 문서 상태를 동기화했다. - 왜: `REV-072`처럼 v2 생성 경로가 문자열 request overload의 `validatePreviewTime`을 우회하면 잘못된 preview 입력이 DB/S3/event 경계로 진행될 수 있기 때문이다. - 어떻게: `AiCharacterAdminAudioContentCreateTest`에 한쪽만 입력·형식 오류·15초 미만 KO/EN/JA actual endpoint 테스트와 정상 preview metadata 검증을 추가했고, `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)`에 검증 호출을 이동했다. - RED: production 변경 전 invalid preview actual endpoint 9건이 400 기대 대비 200/부작용 경로로 실패했다. 테스트 JSON 조립 오류 수정 전에는 400 공통 `invalid_request`가 먼저 발생해 테스트를 바로잡은 뒤 GREEN을 재확인했다. - 검증: `./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`; `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'` → `BUILD SUCCESSFUL in 1m 22s`; `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 32s`; `jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` → 출력 없음; OpenAPI implemented count → `37`; controller mapping count → 파일별 4/5/10/9/8/1 합계 37; `git diff --check` → 출력 없음. - 결과: `REV-072` 처리 완료. Phase 3은 29/29 완료, Phase 7은 13/13 완료이며 미처리 finding·차단 사항은 0건이다. ### Phase 1~7 9차 정적 리뷰 완료 — 2026-07-29 - 상태: 판정 완료, `P3-R19` 후속 구현 대기 - 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 controller/facade/service/repository/test 소스를 대조해 Phase별로 리뷰 결과를 기록했다. - 왜: 컴파일·테스트 통과와 별개로 신규 v2 오디오 생성의 preview 시간 검증이 기존 creator 생성과 동일한지 확인하고, 확정된 회귀만 기존 완료 이력을 보존한 신규 Goal로 전환하기 위해서다. - 어떻게: `AiCharacterAdminAudioContentFacade.create`의 호출 대상과 `AudioContentService` 두 overload의 검증 위치, v2 actual endpoint 테스트, OpenAPI operation/status를 `rg`·`sed`·`jq`로 정적으로 대조했다. 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다. - 결과: 문자열 request overload에만 preview 검증이 있고 v2가 호출하는 parsed request overload에는 검증이 없는 `REV-072`를 High로 확정했다. Phase 1·2·4·5·6은 신규 finding이 없으며 Phase 7은 `P3-R19-GATE` 뒤 통합 재판정이 필요하다. - 계획 전환: `Task 3.29` / `P3-R19` / `P3-R19-GATE`, 이어서 `Task 7.13` / `P7-R11` / `P7-R11-GATE`. - 정적 검증: OpenAPI JSON 문법 정상, operation 37개·고유 operationId 37개·`implemented` 37개를 확인했다. - 다음 Goal: `P3-R19`. ### Phase 1~7 8차 정적 리뷰 완료 — 2026-07-29 - 상태: 판정 완료, 후속 구현 대기 - 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 controller/facade/repository/test 소스를 대조해 `REV-065`~`REV-071`을 확정하고 Phase별 신규 Task/Gate로 전환했다. - 왜: 기존 완료 기록을 되돌리지 않으면서 실제 구현과 multipart/장르/FanTalk 문서/상태 계약의 잔여 불일치를 이어서 수정할 수 있는 goal 단위로 남기기 위해서다. - 어떻게: `rg`·`jq`·`nl`, Spring Web 5.3.29 및 기존 compile output의 `javap` 정적 증거를 사용했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. - 검증: OpenAPI operation 37개·고유 ID 37개·`implemented` 37개, controller mapping 37개, Phase Task 수 7/18/28/16/16/10/12, 신규 Task 정의 각 1개, Markdown code fence 짝을 확인했다. `./gradlew tasks --all`은 task 목록만 조회해 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공했다. - 다음 Goal: `P2-R12`. ### `P3-R15` / `P3-R15-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: 오디오 댓글·답글 목록 두 GET이 `page`, `size` 전체 또는 부분 생략 시 OpenAPI 기본값 `0`, `20`을 적용하도록 복구했다. - 왜: `REV-052`가 controller의 필수 binding과 facade의 정확한 query-name 집합 검증이 optional pagination 계약을 함께 막는다고 확정했기 때문이다. - 어떻게: controller `@RequestParam`에 기본값을 지정하고 facade는 `page`, `size`의 부분집합만 허용하면서 미지 query·음수 page·1 미만 size는 기존 400 경계를 유지했다. actual endpoint 테스트는 댓글·답글 각각의 전체 생략, `page`만, `size`만 요청을 20/1 item 경계로 확인했고 기존 UTC·오류 회귀를 함께 실행했다. - 검증: RED 2건 후 focused 9건 `BUILD SUCCESSFUL in 40s`, content/common error 영향 범위 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck` `BUILD SUCCESSFUL in 37s`. - 전체 회귀: `./gradlew test`는 변경이 v2 오디오 댓글 controller/facade와 focused actual endpoint에 한정되고 영향 범위 회귀가 이를 포함하므로 생략했다. - 다음 Goal: `P3-R16`. ### `P3-R16` / `P3-R16-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: AudioContent POST·PUT multipart `request` part를 `application/json` 호환 값으로만 제한했다. - 왜: `REV-056`가 `@RequestPart String` binding이 text/plain과 Content-Type 누락을 수용해 OpenAPI 415 계약을 위반한다고 확정했기 때문이다. - 어떻게: Character `P2-R10`과 같은 multipart header 검사와 `HttpMediaTypeNotSupportedException`을 controller 경계에만 적용했다. POST·PUT actual endpoint는 KO/EN/JA의 text/plain·누락 media type 415 `ApiResponse.error`, `Accept: application/json`, DB/S3/event 무변경을 검증했고, 기존 JSON strict parse·필수 part 400·UTC/file/series 회귀는 JSON fixture로 유지했다. - 검증: RED 13건 뒤 focused 83건 `BUILD SUCCESSFUL in 56s`, content/common error 영향 범위 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck` `BUILD SUCCESSFUL in 16s`, OpenAPI encoding 정적 대조과 `git diff --check` 출력 없음을 확인했다. - 전체 회귀: `./gradlew test`는 controller와 실제 AudioContent endpoint test에 한정된 변경을 content/common 영향 범위 회귀가 포함하므로 실행하지 않았다. ### `P4-R7` / `P4-R7-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: Series POST·PUT multipart `request` part를 `application/json` 호환 값으로만 제한했다. - 왜: `REV-057`가 `@RequestPart String` binding이 `text/plain`과 Content-Type 누락을 수용해 OpenAPI 415 계약을 위반한다고 확정했기 때문이다. - 어떻게: Character·AudioContent와 같은 multipart header 검사와 `HttpMediaTypeNotSupportedException`을 Series controller 경계에만 적용했다. POST·PUT actual endpoint는 KO/EN/JA의 `text/plain`·누락 media type 415 `ApiResponse.error`, `Accept: application/json`, S3/DB/event 무변경을 검증했고, 필수 `request` part 누락 400 및 기존 JSON strict parse·image/genre/owner 회귀는 유지했다. - 검증: RED 12건 후 focused 36건 `BUILD SUCCESSFUL in 43s`, series/common error 영향 범위 `BUILD SUCCESSFUL in 1m 4s`, `ktlintCheck` `BUILD SUCCESSFUL in 20s`, OpenAPI Series create/update encoding 정적 대조와 `git diff --check` 출력 없음을 확인했다. - 전체 회귀: `./gradlew test`는 v2 Series controller와 실제 Series endpoint test에 한정된 변경이고 series/common error 영향 범위 회귀가 이를 포함하므로 실행하지 않았다. ### FanTalk 답변 수정 계약·구현 계획 보완 — 2026-07-29 - 상태: 계약 확정, 구현 대기 - 무엇을: 레거시 `PUT /explorer/profile/cheers`의 FanTalk 답변 수정 계약을 신규 관리자 경계의 `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`로 이관했다. - 왜: 기존 V2 관리자 FanTalk에는 목록·답변 작성·팬 원글 삭제만 있고, 선택한 AI 캐릭터가 작성한 답변의 내용·활성 상태를 수정할 operation이 없기 때문이다. - 어떻게: `cheersId`만 path `replyId`로 이동하고 optional/nullable `content`, `isActive`, 동시 입력, 빈 객체 no-op, 비활성 reply 재활성화와 레거시 `CreatorChannelFanTalkResponse` 성공 `data`를 유지했다. target AI가 작성하고 path의 활성 root에 직접 연결된 reply만 허용하는 관리자 ownership 경계를 추가했다. - 결과: PRD, OpenAPI 2.3.0, 계약 설명, Phase 6 `Task 6.9` / `P6-R4`·Gate와 Phase 7 `P7-R8` 종결 조건을 동기화했다. 전체 계약은 37개이며 기존 36개는 `implemented`, 신규 답변 수정 1개는 `planned`다. - 검증: 문서와 OpenAPI만 변경했다. JSON 문법·내부 `$ref`·operationId·상태 집계와 diff를 정적으로 검증했고 문서 절차의 `./gradlew tasks --all`만 `BUILD SUCCESSFUL in 742ms`로 확인했다. 사용자 지시에 따라 컴파일·테스트· lint는 실행하지 않았다. - 다음 Goal: 기존 실행 순서의 `P2-R10`; FanTalk 기능 순서는 `P6-R3` → `P6-R4`. ### Phase 1~7 6차 정적 리뷰 완료 — 2026-07-29 - 상태: 후속 보완 Task 등록 완료, 구현 대기 - 무엇을: PRD, OpenAPI 36개 operation, controller/facade와 관련 계약 테스트를 현재 working tree 기준으로 Phase별 정적 대조했다. - Phase 1 결과: 공통 ADMIN 이중 인가, target resolver, 오류/CORS 경계에서 신규 finding 없음. - Phase 2 결과: Character 생성·수정의 `request` part가 OpenAPI의 `application/json` encoding과 달리 `@RequestPart String`으로 media type을 강제하지 않는 `REV-055`를 확정했다. `Task 2.16` / `P2-R10`으로 전환했다. - Phase 3 결과: 오디오 댓글·답글 목록이 OpenAPI optional `page`/`size`와 달리 두 query를 필수로 요구하고, facade도 실제 query 이름을 정확히 두 개 요구하는 `REV-052`를 확정했다. `Task 3.25` / `P3-R15`로 전환했다. 생성·수정 `request` part의 같은 불일치 `REV-056`은 `Task 3.26` / `P3-R16`으로 전환했다. - Phase 4 결과: Series 생성·수정 `request` part의 같은 불일치 `REV-057`을 `Task 4.13` / `P4-R7`로 전환했다. - Phase 5 결과: 커뮤니티 댓글 작성·수정 mapping에 JSON `consumes`가 없어 OpenAPI 415 계약을 보장하지 못하는 `REV-053`을 `Task 5.13` / `P5-R7`로 전환했다. 게시글 생성·수정 `request` part의 같은 불일치 `REV-058`은 `Task 5.14` / `P5-R8`로 전환했다. - Phase 6 결과: FanTalk 답변 작성 mapping에 JSON `consumes`가 없어 OpenAPI 415 계약을 보장하지 못하는 `REV-054`를 확정했다. `Task 6.8` / `P6-R3`으로 전환했다. - Phase 7 결과: 일곱 소유 Phase Gate 뒤 36개 operation을 재판정하는 `Task 7.10` / `P7-R8`을 추가했다. - 검증: OpenAPI JSON 문법, 36개 operationId 고유성, requestBody media type, 공통 pagination parameter, multipart encoding, controller mapping/`consumes`/`@RequestPart`와 dependency·DDL 변경 범위를 정적으로 대조했다. 사용자 지시에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. - 다음 Goal: `P2-R10`. ### `P5-R6` / `P5-R6-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: v2 관리자 커뮤니티 댓글·답글 GET에서 필수 `timezone` query를 제거하고, 기존 `date` 필드만 `createdAt` 기반 ISO-8601 UTC(`Z`)로 정합화했다. - 왜: `REV-051`이 최신 OpenAPI의 page/size-only query 및 UTC date 계약과 실제 timezone 표시 문자열의 불일치를 확정했기 때문이다. - 어떻게: controller/facade의 timezone 입력·검증만 제거하고 legacy service/repository 계약은 유지했다. 기존 `toUtcIso()`로 owner-scoped 조회 결과를 재매핑하고, actual endpoint 테스트로 root/reply UTC exact JSON 및 추가 timezone query 무영향을 고정했다. - 결과: RED 2건 실패 후 focused GREEN, community/common·legacy 영향 범위 회귀, `ktlintCheck`, OpenAPI 36개 `implemented`/0개 `alignment-required`, `git diff --check` 검증을 통과했다. 전체 `./gradlew test`는 v2 커뮤니티 controller/facade/test와 해당 legacy service 경계에 변경을 한정했고 직접 영향 회귀가 이를 포함하므로 실행하지 않았다. - 남은 항목: 없음. 후속 `P7-R7` 통합 재판정도 완료했다. ### `P7-R7` / `P7-R7-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: UTC 날짜 계약 변경 뒤 36개 관리자 operation의 OpenAPI 상태, controller mapping, timezone parameter/schema 제거, Phase 3·5 Gate 증거를 통합 재판정했다. - 왜: `REV-050`과 `REV-051` 처리 뒤 최신 계약 기준으로 Phase 7 완료 상태를 복구해야 했기 때문이다. - 어떻게: OpenAPI `jq` 집계, controller mapping 정적 집계, 오디오·커뮤니티 focused 재실행 및 각 Gate의 영향 범위 회귀·lint·diff 기록을 대조하고 문서 상태를 동기화했다. - 결과: 36개 operation 모두 `implemented`, `alignment-required` 0개, query `timezone` parameter 0개, `components.parameters.Timezone` 0개, controller mapping 36개를 확인했다. 오디오 focused는 `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks`는 `BUILD SUCCESSFUL in 4m 33s`였다. - 남은 항목: 없음. ### `P5-R8` / `P5-R8-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: 커뮤니티 게시글 생성·수정 multipart의 `request` part를 `application/json` 호환 media type으로 제한했다. - 왜: `REV-058`이 OpenAPI multipart encoding과 `@RequestPart String` permissive binding의 불일치를 확정했기 때문이다. - 어떻게: create/update actual endpoint에 KO/EN/JA `text/plain` 및 Content-Type 누락 415 matrix를 먼저 추가해 RED를 확인한 뒤, controller에서 part header만 검사하고 기존 facade strict reader와 domain 로직은 유지했다. - 결과: RED focused는 12개 invocation이 415 기대 실패로 `BUILD FAILED in 56s`, GREEN focused는 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 55s`였다. 전체 `./gradlew test`는 변경 범위가 v2 community 게시글 multipart request part 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 실행하지 않았다. - 남은 항목: 없음. 다음 Goal은 `P6-R3`이다. ### `P5-R9` / `P5-R9-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: Community post 생성·수정 multipart의 실제 part 이름을 operation별 OpenAPI 허용 집합으로 제한했다. - 왜: `REV-063`이 생성과 수정의 허용 part 집합이 다른데 controller가 전체 part 이름을 검사하지 않아 수정 `audioFile` 등 미정의 part를 무시하고 mutation을 진행할 수 있다고 확정했기 때문이다. - 어떻게: controller 경계에서 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` allow-list를 적용하고 초과 part는 400 `common.error.invalid_request`로 거부했다. actual endpoint 테스트로 생성 `unexpected`, 수정 `unexpected`·`audioFile`의 DB/S3 no-side-effect를 고정했다. - 검증: RED 3건 `BUILD FAILED in 3m 23s`, focused GREEN `BUILD SUCCESSFUL in 2m 30s`, community/common 영향 범위 `BUILD SUCCESSFUL in 1m 44s`, `ktlintCheck` `BUILD SUCCESSFUL in 55s`, `git diff --check` 출력 없음을 확인했다. - 전체 회귀: `./gradlew test`는 변경이 v2 Community post controller와 실제 Community endpoint test에 한정되고 community/common 영향 범위 회귀가 이를 포함하므로 실행하지 않았다. - 다음 Goal: `P7-R9`. ### `P6-R3` / `P6-R3-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: FanTalk 답변 작성 POST를 `application/json` request만 받도록 제한했다. - 왜: `REV-054`가 OpenAPI requestBody media type과 controller mapping의 불일치를 확정했기 때문이다. - 어떻게: `text/plain` actual endpoint 415 matrix를 먼저 추가해 RED를 확인한 뒤, reply POST mapping에 JSON `consumes`만 추가했다. - 결과: RED focused는 3개 invocation이 415 기대 실패로 `BUILD FAILED in 33s`, GREEN focused는 `BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 47s`였다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply POST media type 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 실행하지 않았다. - 남은 항목: 없음. 다음 Goal은 `P6-R4`다. ### `P6-R4` / `P6-R4-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: target AI가 작성하고 path의 활성 root에 직접 연결된 FanTalk reply만 수정하는 관리자 PUT endpoint를 추가했다. - 왜: `REV-059`가 레거시 `PUT /explorer/profile/cheers`의 optional/nullable `content`, `isActive`, no-op, 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data` 계약을 V2 관리자 경계에 이관해야 한다고 확정했기 때문이다. - 어떻게: `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`에 JSON `consumes`와 strict body reader를 추가하고, repository에서 reply ID·target creator/writer·active root·direct-parent를 한 query로 검증했다. facade는 non-null `content`와 `isActive`만 반영하고 `languageCode`와 event는 변경하지 않는다. - 결과: RED focused는 15건이 미구현 route 404로 `BUILD FAILED in 49s`, GREEN focused는 `BUILD SUCCESSFUL in 42s`, FanTalk/common/legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`였다. OpenAPI status는 37개 모두 `implemented`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. - 전체 회귀: `./gradlew test`는 변경 범위가 v2 FanTalk reply update와 FanTalk/common/legacy 영향 범위에 포함되므로 실행하지 않았다. - 남은 항목: 없음. 다음 Goal은 `P7-R8`이다. ### `P7-R8` / `P7-R8-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: `REV-052`~`REV-059` 처리 뒤 37개 관리자 operation의 HTTP 경계와 FanTalk 답변 수정 계약을 최종 통합 재판정했다. - 왜: Phase 2~6의 여덟 소유 Gate가 모두 완료되어 OpenAPI, controller mapping, 회귀, lint, diff와 문서 상태를 하나의 최종 Gate에서 대조해야 했기 때문이다. - 어떻게: OpenAPI operationId/status, controller mapping 수, 미처리 finding, dependency/DDL 변경 여부를 정적으로 확인하고, 영향 범위 focused 회귀와 전체 `./gradlew test`, `ktlintCheck`, `git diff --check`를 fresh 실행했다. - 결과: OpenAPI는 operationId 37개/unique 37개/status `implemented` 37개였고 controller mapping은 37개였다. `REV-052`~`REV-059` 미처리 항목과 선행 Gate 미체크 항목은 없었다. 영향 범위 focused 회귀는 `BUILD SUCCESSFUL in 1m 26s`, 전체 `./gradlew test`는 `BUILD SUCCESSFUL in 8m 8s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다. 신규 dependency·DDL 변경도 없다. - 남은 항목: 없음. ### `P7-R9` / `P7-R9-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: `REV-060`~`REV-064` 처리 뒤 8개 multipart operation의 part 이름 계약과 37개 operation 구현 상태 문서를 통합 재판정했다. - 왜: Phase 2~5 Gate에서 operation별 multipart allow-list 보완이 끝났고, `api-contract.md`의 route/완료/예정 수와 FanTalk 답변 수정 상태가 과거 36개 구현/1개 planned로 남아 있었기 때문이다. - 어떻게: 8개 multipart schema의 `additionalProperties: false`, required/props 집계와 Phase 2~5 Gate 증거를 대조하고, OpenAPI operation/status와 controller mapping을 새로 집계한 뒤 `api-contract.md` 상단 집계·endpoint 표·client 생성 설명을 37개 구현 완료로 동기화했다. - 검증: OpenAPI는 operation 37개, 고유 operationId 37개, `implemented` 37개, `alignmentRequired` 0개, `planned` 0개였다. controller mapping은 Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4로 총 37개였다. 8개 multipart schema는 모두 `additionalProperties=false`이고 허용 part 집합은 Character/Series `{image, request}`, AudioContent 생성 `{contentFile, coverImage, request}`·수정 `{coverImage, request}`, Community 생성 `{audioFile, postImage, request}`·수정 `{postImage, request}`로 확인했다. `ktlintCheck`는 `P5-R9-GATE`의 `BUILD SUCCESSFUL in 55s` 기록을 대조했고, 문서 수정 후 `git diff --check`를 재실행했다. - 전체 회귀: production 동작은 `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE`에서 focused와 영향 범위 회귀로 검증 완료했으므로 이 문서 동기화 Task에서는 실행하지 않았다. - 남은 항목: 없음. ### `P2-R1` 실행 준비 — 2026-07-27 - 상태: 대기 - 무엇을: Phase 2~7의 Goal ID, 시작 조건, 완료 증거, 범위 밖, 체크박스와 Gate 구조를 준비했다. - 왜: 기존 Phase 2·3 체크만으로 심층 리뷰와 최종 완료를 판정할 수 없었기 때문이다. - 어떻게: PRD, 기존 계획과 `docs/sample/sample-plan-task.md`, `docs/sample/sample-review.md`를 대조했다. - 남은 항목: `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`이지만 사용자 진행 지시 전까지 시작하지 않는다. ### `P23-CONTRACT-1` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: 캐릭터, 테마·오디오 콘텐츠, 시리즈, 커뮤니티, FanTalk의 신규 관리자 API 23개 request/response를 OpenAPI 3.1 JSON과 설명 문서로 고정했다. - 왜: 신규 path와 관리자 target만 적용하고 클라이언트 JSON 계약은 레거시 필드명·타입·optional/nullable·기본값과 성공 `data` 형태를 그대로 이관해야 하기 때문이다. - 어떻게: 레거시 Kotlin DTO·controller·service와 schema를 대조하고 path ID만 body에서 제거했다. FanTalk 답변 축약 응답, 관리자 FanTalk 목록, 분리된 시리즈 미연결 콘텐츠 검색만 확정 예외로 반영했다. 독립 리뷰에서 확인한 `Accept-Language` fallback, 캐릭터 비활성화 혼합 입력, 시리즈 상세 문자열 `state`, Phase 4 선행 Gate 문제를 교정했다. - 결과: JSON parse, 내부 `$ref` 누락 0건, 고유 operation 23개와 상태 9/14를 확인했다. Redocly lint와 OpenAPI Generator validate가 통과했고 `typescript-fetch` 생성 후 TypeScript 5.9.3 `tsc --noEmit`도 성공했다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였으며 독립 최종 리뷰는 Critical 0, Important 0이었다. - 남은 항목: 현재 구현된 캐릭터 4개 endpoint의 runtime DTO를 맞추는 `P23-CONTRACT-2`. production code는 변경하지 않았다. ### `P23-CONTRACT-2`~`P7-GATE` 실행 계획 보완 — 2026-07-28 - 상태: 구현 시작 준비 완료 - 무엇을: `P23-CONTRACT-2`·`P23-CONTRACT-3`의 실제 facade와 전용 query/theme/ownership/legacy test 범위를 보강하고, Task별 focused 명령을 추가했다. Phase 4~6은 각 OpenAPI tag를 정식 schema로, Phase 7은 OpenAPI 23개 operation을 최종 구현 기준으로 명시했다. - 왜: 확정 계약이 있어도 실제 request parsing·response 조립을 담당하는 facade와 전용 테스트가 계획에서 누락되면 구현 중 범위가 다시 흔들릴 수 있기 때문이다. - 어떻게: OpenAPI operation 23개와 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 분류를 Phase/Goal과 대조하고 기존 source/test 파일 존재, Goal ID 중복, 선행 Gate와 PRD Open Questions를 확인했다. - 결과: 과거 축약 예시는 비규범 이력으로 분리했고 현재 Endpoint Contract Summary는 OpenAPI와 같은 9/14 상태 및 예외만 제공한다. Task 3.17부터 `P7-GATE`까지 시작 조건·파일·RED/GREEN/REFACTOR·검증 명령이 연결됐으며 `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 922ms`였다. - 제외: 생성 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE`에는 추가하지 않았다. `P23-CONTRACT-1`의 일회성 생성·컴파일 검증 기록은 유지한다. - 다음 행동: production 변경 없이 문서 보완만 완료했다. 구현 시작 Goal은 `P23-CONTRACT-2`다. ### `P23-CONTRACT-2` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: 캐릭터 목록·검색은 레거시 `totalCount/content`와 list item 필드를, 상세는 `id/characterUUID`와 전체 nested 필드를 반환하도록 기존 레거시 DTO mapper를 재사용했다. 생성은 `image`를 필수로 받고 생성·수정 성공은 exact `data: null`을 반환하며, `isActive=false`와 다른 optional JSON field의 혼합 요청은 받아 비활성화만 반영하도록 했다. - 왜: 현재 v2 전용 축약 DTO, optional 생성 이미지, mutation 상세 응답과 단독 soft-delete 제한이 `api-contract.openapi.json`의 확정 레거시 runtime 계약과 달랐기 때문이다. - 어떻게: 지정된 두 actual endpoint 테스트에 list/detail exact field, 전체 create/update request, 필수 image, null mutation envelope, mixed soft-delete 미반영 assertion을 RED로 추가한 뒤 controller/DTO/facade/mapper만 최소 변경했다. - 결과: RED는 59건 중 의도한 9건 실패였고, GREEN focused 59건과 character/authorization/error 회귀 170건이 모두 통과했다. `./gradlew ktlintCheck`도 `BUILD SUCCESSFUL in 48s`였다. - 독립 리뷰 보완: Character list의 OpenAPI `size` minimum 1을 실제 pagination에 반영했다. 추가 RED는 focused 60건 중 1건 실패였고, 보완 후 focused 60건과 character/authorization/error 회귀 171건 및 `ktlintCheck`가 모두 통과했다. mixed soft-delete는 미존재 `originalWorkId`까지 검증·반영 없이 무시함을 actual endpoint test로 강화했다. - 남은 항목: `P23-CONTRACT-3`은 시작하지 않았다. Task 3.18과 Phase 4 이후 범위는 변경하지 않았다. ### `P23-CONTRACT-3` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: 테마·오디오 콘텐츠 5개 endpoint를 `id/theme/image`, `search_word`와 legacy 목록 item, 필수 `timezone`과 `GetAudioContentDetailResponse`, 생성 `contentFile`·`CreateAudioContentRequest`·`data.contentId`, 수정 `UpdateCreatorAdminContentRequest`·`data: null` 계약으로 정합화했다. - 왜: 기존 v2 전용 `description`, `releaseDateUtc`, `audioSignedUrl`, `status`, `seriesIds`, `themeName`, `imageUrl` alias와 생성·수정 상세 응답이 `api-contract.openapi.json`의 확정 레거시 runtime 계약과 달랐기 때문이다. - 어떻게: exact JSON·query·multipart RED를 먼저 실행한 뒤 기존 legacy DTO와 목록 service를 재사용하고, 상세 owner guard, create/update service 위임, signed URL 만료 계산과 기존 series row 보존을 유지했다. - 결과: RED는 focused 61건 중 21건이 의도대로 실패했다. GREEN focused 61건과 content·authorization·error 10개 suite 206건이 failure/error/skipped 0으로 통과했고 `ktlintCheck`와 문서 변경 후 `./gradlew tasks --all`도 성공했다. - 남은 항목: `P23-CONTRACT-GATE`. Phase 4 이후 범위는 변경하지 않았다. ### `P4-T1` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `LegacyCreatorAdminSeriesCharacterizationTest`로 기존 creator-admin series 생성·활성 목록·inactive 상세·혼합 수정/soft delete, 콘텐츠 부분 연결·무해한 해제·owner 전체 count·미연결 검색과 owner-less 순서 변경을 고정했다. - 왜: Phase 4 v2가 재사용할 legacy 동작과 그대로 복제하면 안 되는 ownership·부분 성공·오류 status 경계를 production 변경 전에 분리해야 하기 때문이다. - 어떻게: 실제 Spring/JPA/QueryDSL repository와 production service를 사용하고 S3 client와 event publisher만 격리한 focused characterization을 첫 실행했으며, 이어 `ktlintCheck`를 실행했다. - 결과: focused test는 `BUILD SUCCESSFUL in 47s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 19s`였다. production, DB schema, dependency, security와 Task 3.17/3.18 코드는 변경하지 않았다. - 오류 결정: legacy 입력 validation key는 신규 prefix에서 400으로 보존하고, missing/inactive/cross-owner 및 order/link 사전 검증 실패는 400 `common.error.invalid_request`, 예상하지 못한 오류는 500 `common.error.unknown`과 KO/EN/JA envelope로 고정했다. invalid ownership은 모든 DB/S3/event보다 먼저 실패해야 하며 legacy owner-less order path는 신규 v2에서 재사용하지 않는다. - 남은 항목: `P4-T2` 시리즈 목록·상세 조회 구현. 전체 `./gradlew test`는 production 변경이 없는 test-only baseline이고 실제 service/repository를 통과한 focused 검증으로 직접 범위를 확인했으므로 실행하지 않았다. - 테스트 결과 확인: JUnit XML 기준 7건, failure/error/skipped 0건이며 재실행도 `BUILD SUCCESSFUL in 2s`였다. - 문서 명령 유효성: 문서 갱신 후 `./gradlew tasks --all`을 실행해 `test`, `ktlintCheck`, `tasks` 존재와 `BUILD SUCCESSFUL in 940ms`를 확인했다. - 최종 fresh 검증: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest`는 10개 task를 모두 실행해 `BUILD SUCCESSFUL in 4m 19s`, JUnit 7건 failure/error/skipped 0건이었다. `./gradlew ktlintCheck --rerun-tasks`는 7개 task를 모두 실행해 `BUILD SUCCESSFUL in 25s`였다. 컴파일의 기존 deprecated API warning 외 신규 경고·실패는 없었다. ### `P4-T2` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `/api/v2/admin/ai-characters/{characterId}/series` 목록과 `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` 상세를 추가했다. 목록은 target owner의 활성 시리즈만 legacy `totalCount/items`와 전체 item 필드로 반환하고, 상세는 활성 owner 리소스만 legacy 문자열 필드로 반환한다. - 왜: AI 캐릭터 creator member로 로그인하지 않고도 관리자가 target resolver를 통해 해당 캐릭터의 시리즈를 읽되, legacy의 inactive 상세 허용과 cross-owner 접근을 신규 v2 경계로 가져오면 안 되기 때문이다. - 어떻게: `AiCharacterAdminTargetResolver`를 먼저 실행하고 기존 creator series 목록 서비스와 legacy DTO를 재사용했다. 상세는 owner 조회 뒤 `isActive`를 확인하고 기존 entity의 detail mapping을 사용했으며, OpenAPI 최소값대로 `page>=0`, `size>=1`만 허용했다. - 결과: production 전 RED 5건은 모두 미구현 404로 실패했다. 구현 후 focused 5건과 Task 4.1 포함 series 12건이 모두 failure/error/skipped 0으로 통과했고 `ktlintCheck`도 성공했다. mutation, 콘텐츠 연결·검색과 순서 변경은 구현하지 않았다. - 남은 항목: `P4-T3` 시리즈 생성·수정·soft delete 구현. 전체 `./gradlew test`는 변경 범위가 신규 series 조회 slice에 한정되고 실제 Spring MVC/JPA 경계를 통과한 focused·legacy series 12건으로 직접 범위를 확인했으므로 실행하지 않았다. - 문서 명령 유효성: Task 4.2 체크박스와 Progress 갱신 후 `./gradlew tasks --all`을 실행해 `test`, `ktlintCheck`, `tasks`가 존재하고 `BUILD SUCCESSFUL in 930ms`임을 확인했다. - 독립 리뷰 보완: Phase 4 결정에 맞춰 inactive AI character target의 목록·상세를 400 `common.error.invalid_request`로 차단했다. 보완 RED 2건은 200으로 실패했고 공통 active target guard 적용 후 focused 7건, series 전체 14건과 `ktlintCheck`가 모두 통과했다. list item은 동일 DTO serializer를 공유하고 요일 배열 순서는 OpenAPI/legacy에서 고정하지 않으므로 item별 field set 반복과 임의 순서 고정은 추가하지 않았으며, pagination은 최소값·다음 page·잘못된 하한을 이미 직접 검증한다. ### `P4-T3` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: target owner의 시리즈 POST 생성, multipart PUT 수정과 DELETE soft delete를 추가하고 성공 `data: null`을 유지했다. - 왜: legacy creator mutation을 재사용하면서도 inactive target/series와 missing·cross-owner 요청은 mutation 전에 같은 400으로 차단해야 했기 때문이다. - 어떻게: 활성 target과 활성 owned series를 먼저 확인하고, 양수 `genreId`는 active genre 존재를 사전 확인한 뒤 `CreateSeriesRequest`, `ModifySeriesRequest`로 `CreatorAdminContentSeriesService.createSeries`/`modifySeries`를 호출했다. keyword·S3·event·entity 갱신은 복제하지 않았다. - 결과: RED 12건은 미구현 405로 실패했고, GREEN focused 12건과 fresh series 회귀 26건이 통과했다. 독립 리뷰에서 누락 genre 사전 검증을 보완해 focused 14건과 series 회귀 28건 및 `ktlintCheck`, `git diff --check`가 통과했다. invalid mutation의 DB/S3/event 0건과 legacy validation key 400을 실제 endpoint에서 확인했다. - 남은 항목: `P4-T4` 시리즈 콘텐츠 조회·검색·연결·해제. Task 4.4 이후 범위는 변경하지 않았다. ### `P4-T4` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `/series/{seriesId}/contents` 연결 목록, `/contents/search` 미연결 검색, JSON `contentIdList` 연결과 JSON `contentId` 해제를 추가했다. 조회는 legacy DTO/owner 전체 link `totalCount`를 유지하고 mutation은 `data: null`을 반환한다. - 왜: legacy 연결은 foreign/missing ID를 건너뛰고 해제 없는 link를 no-op 처리하므로, 신규 v2 경계에서 target·active owned series와 모든 content/link 상태를 mutation 전에 검증해 부분 반영을 막아야 했기 때문이다. - 어떻게: legacy 목록/검색/연결/해제 service를 재사용하되, v2 repository에서 owner의 processed 또는 reserved eligible content를 확인하고 이미 연결된 content, duplicate ID, 없는 link를 `common.error.invalid_request`로 차단했다. 빈 목록은 legacy와 같이 `creator.admin.series.no_content_added`를 유지했다. - 결과: production 전 RED 7건은 미구현 route의 404/405로 실패했고, 구현 후 focused 7건, series 회귀, Phase 3 content 회귀와 `ktlintCheck`가 모두 통과했다. Task 4.5 순서 변경과 Task 4.6 보안 matrix는 변경하지 않았다. ### `P4-T6` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: Phase 4 series endpoint의 ADMIN 이중 인가/stale claim matrix, target·series·content·pagination·order·binding 오류 envelope, cross-owner mutation no-side-effect와 legacy series 회귀를 고정했다. - 왜: Phase 4 series API가 legacy creator endpoint를 재사용하더라도 신규 관리자 prefix에서는 owner-first validation, KO/EN/JA `common.error.invalid_request` envelope와 legacy regression이 endpoint 단위로 증명되어야 하기 때문이다. - 어떻게: `AiCharacterAdminAuthorizationTest`에 series list/detail/contents/search/link/unlink/order/create/update/delete endpoint matrix를 추가했고, `AiCharacterAdminSeriesContractTest`에 inactive target과 active target의 missing/inactive/cross-owner series 오류를 분리해 검증했다. Cross-owner PUT은 DB title, event publisher, S3 `putObject`가 변하지 않음을 단언한다. - 결과: contract focused test는 `BUILD SUCCESSFUL in 3m 41s`, authorization focused test는 순차 재실행에서 `BUILD SUCCESSFUL in 3m 3s`, Phase 4 series 회귀는 `BUILD SUCCESSFUL in 3m 23s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 병렬 Gradle 실행 중 한 authorization run은 unrelated `DefaultHomeRecommendationQueryRepository` QueryDSL 참조 compile 오류로 실패했으나 동일 명령 순차 재실행은 통과했다. - 남은 항목: `P4-GATE`. ### `P4-GATE` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: Phase 4 series vertical slice의 조회·mutation·content link·order·보안/오류 계약과 legacy 회귀를 최종 판정했다. - 왜: Phase 5 community 구현으로 넘어가기 전에 `P4-T1`~`P4-T6`의 완료 증거와 Gate 명령 성공을 문서와 실제 검증으로 맞춰야 하기 때문이다. - 어떻게: Gate에 명시된 Phase 4 series 전체 focused 회귀와 `ktlintCheck`를 `--rerun-tasks`로 fresh 실행하고 `git diff --check`를 확인했다. - 결과: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 21s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 24s`, `git diff --check`는 출력이 없었다. - 남은 항목: `P5-T1` 커뮤니티 기존 parity 특성화 baseline. Phase 5 production 구현은 아직 시작하지 않았다. ### `P5-T1` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: 기존 community 생성 validation, media upload, FCM/recent-news side effect, 최대 고정 3개와 soft delete fixed clearing을 `LegacyCommunityPostCharacterizationTest`로 고정했다. - 왜: 신규 v2 community 관리자 구현 전에 legacy parity와 신규 owner-first 오류 정책을 분리해 Phase 5 구현 기준을 흔들리지 않게 하기 위해서다. - 어떻게: 기존 `CreatorCommunityService`를 mock dependency로 직접 실행하는 characterization test를 추가하고 production code는 변경하지 않았다. - 결과: focused characterization은 `BUILD SUCCESSFUL in 2m 22s`, community package targeted test는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`, `git diff --check`는 출력이 없었다. - 남은 항목: `P5-T2` 관리자 게시글 목록 조회 구현. ### `P6-T2` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `/api/v2/admin/ai-characters/{characterId}/fan-talks`를 추가해 target owner의 활성 root FanTalk와 활성 creator reply를 공개 v2 `CreatorChannelFanTalkTabResponse` 필드 형태로 반환했다. - 왜: 공개 v2 조회는 viewer/block 조건을 적용하므로, 관리자 endpoint는 target resolver가 해석한 `creatorMember` 기준의 별도 query가 필요하다. - 어떻게: root는 `createdAt desc, id desc`, reply는 `createdAt asc, id asc`으로 조회하고 page/size/hasNext/count를 전용 facade에서 조립했다. test는 차단 관계가 있는 관리자 fixture에서도 writer root가 포함되는지, 다른 target·inactive·fan reply·inactive/nested reply가 제외되는지 확인했다. - 결과: RED는 4건 모두 미매핑 404로 `BUILD FAILED in 1m 6s`였고, 구현 후 focused 재실행은 `BUILD SUCCESSFUL in 1m 3s`였다. legacy characterization은 `BUILD SUCCESSFUL in 9s`, FanTalk package 회귀는 `BUILD SUCCESSFUL in 51s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 24s`였다. - 남은 항목: `P6-T3` FanTalk root reply 저장 구현. ### `P6-T3` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`가 선택 target의 활성 root에만 creator reply를 저장하고 축약 응답을 반환하도록 구현했다. - 왜: 관리자 principal이 아닌 target의 `creatorMember`를 writer/creator로 저장하고, 레거시와 같은 `CREATOR_CHEERS` 언어 감지를 유지해야 하기 때문이다. - 어떻게: `AiCharacterAdminFanTalkFacade`의 write transaction 안에서 active target과 owner-scoped active root를 조회한 뒤 `CreatorCheers(languageCode = null)`를 저장하고 `LanguageDetectEvent`를 발행했다. `AiCharacterAdminFanTalkReplyCreateTest`는 response, parent/member/creator row, blank languageCode와 event payload를 실제 MVC/JPA 경계에서 검증했다. - 검증 기록(RED): production 변경 전 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyCreateTest`를 실행했다. 기대한 200 대신 미구현 endpoint 때문에 `AiCharacterAdminFanTalkReplyCreateTest.kt:98`에서 실패했고 `BUILD FAILED in 48s`였다. - 검증 기록(GREEN): 같은 focused 명령을 다시 실행해 `BUILD SUCCESSFUL in 49s`를 확인했다. - 검증 기록(legacy): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`를 실행해 `BUILD SUCCESSFUL in 8s`를 확인했다. - 검증 기록(package): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`를 최종 실행해 `BUILD SUCCESSFUL in 11s`를 확인했다. - 검증 기록(lint): `./gradlew ktlintCheck`를 실행해 `BUILD SUCCESSFUL in 32s`를 확인했다. - 검증 기록(diff): `git diff --check`를 실행해 출력 없이 exit code 0을 확인했다. - 전체 `./gradlew test`는 신규 관리자 FanTalk reply slice에 변경을 한정했고 focused, legacy characterization, FanTalk package 회귀가 직접 범위를 포함하므로 실행하지 않았다. - 남은 항목: `P6-T4` target/root/ownership 거부 구현. ### `P6-T4` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `AiCharacterAdminFanTalkReplyOwnershipTest`로 cross-character root, nested parent, inactive root, missing FanTalk, inactive target의 답변 거부를 KO/EN/JA로 고정했다. - 왜: P6-T3의 owner-scoped active-root 조회와 target active guard가 reply 저장과 `LanguageDetectEvent` 발행보다 앞서는지 실제 MVC/JPA 경계에서 증명하기 위해서다. - 어떻게: 각 거부 요청에서 `ApiResponse.error` 400 `common.error.invalid_request` locale envelope, `CreatorCheers` row count 무변경, reflection으로 교체한 실제 facade `ApplicationEventPublisher`의 무호출을 단언했다. - TDD 예외/특성화: production 변경 전 새 ownership test의 첫 실행이 `BUILD SUCCESSFUL in 1m 1s`였고, 10 actionable tasks 중 3 executed, 7 up-to-date였다. 이는 요구한 거부 분기가 이미 P6-T3에 존재함을 확인한 결과이므로 production code를 변경하지 않았다. - 검증 기록(focused): import 정리 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyOwnershipTest`를 재실행해 `BUILD SUCCESSFUL in 55s`, 10 actionable tasks 중 3 executed, 7 up-to-date를 확인했다. - 검증 기록(reply create/legacy): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`는 `BUILD SUCCESSFUL in 56s`, 10 actionable tasks 중 1 executed, 9 up-to-date였다. - 검증 기록(package): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`는 `BUILD SUCCESSFUL in 55s`, 10 actionable tasks 중 1 executed, 9 up-to-date였다. - 검증 기록(lint): 첫 `./gradlew ktlintCheck`는 새 test의 unused import 1건으로 `BUILD FAILED in 16s`였고, 해당 import만 제거한 뒤 재실행은 `BUILD SUCCESSFUL in 26s`, 7 actionable tasks 중 2 executed, 5 up-to-date였다. - 검증 기록(문서 명령): `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, 1 actionable task executed였고, `test`, `ktlintCheck`, `tasks`가 존재함을 확인했다. - 검증 기록(diff): `git diff --check`는 출력 없이 종료했다. - 전체 `./gradlew test`는 production 변경이 없고 focused, reply create, legacy characterization, FanTalk package 회귀가 직접 범위를 포함하므로 실행하지 않았다. - 남은 항목: `P6-T5` FanTalk 보안·오류·회귀 검증. ### Phase 1~7 정적 리뷰 완료 — 2026-07-28 - 상태: 리뷰 완료, 후속 처리 요청(당시 판정) - 무엇을: PRD, plan-task, OpenAPI 23개 operation과 현재 production/test 구현을 Phase별로 정적 대조했다. - 왜: 완료 기록과 실제 HTTP 경계·내부 책임·구현 현황 metadata가 같은 계약을 가리키는지 확인하고 확정 finding을 이어서 실행 가능한 Task로 전환하기 위해서다. - 어떻게: controller/facade/repository/DTO와 관련 테스트의 호출·mapping·JSON parsing·pagination을 정적 추적하고, `rg`, `jq`, `git diff` 기반으로 문서/operation/변경 범위를 대조했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. - Phase 1 결과: 공통 target resolver, ADMIN 이중 인가, 오류/security 경계에서 신규 확정 finding 0건. - Phase 2 결과: mutation 성공 응답이 `data: null`인데 사용하지 않는 facade response mapping 1건을 `REV-021`로 확정했다. - Phase 3 결과: 호출되지 않는 관리자 content repository 확장과 전용 enum 1건을 `REV-022`로 확정했다. - Phase 4 결과: OpenAPI에 없는 시리즈 DELETE route와 두 JSON body의 미지 필드 허용을 `REV-023`~`REV-024`로 확정했다. - Phase 5 결과: multipart JSON parse 실패의 500 가능성·미지 필드 허용과 계약 밖 `size <= 50` 제한을 `REV-025`~`REV-026`으로 확정했다. - Phase 6 결과: 공개 v2와 다른 pagination 거부 정책과 reply 미지 필드 허용을 `REV-027`~`REV-028`로 확정했다. - Phase 7 결과: plan/api-contract/OpenAPI 구현 상태 metadata가 완료 구현과 불일치하는 문제를 `REV-029`로 확정했다. - 다음 Goal: `P2-R6`. - 보완 결과: `P2-R6`, `P2-R6-GATE`를 완료했고 `REV-021`을 처리 완료로 동기화했다. - 다음 Goal: `P3-R9`. - 보완 결과: `P3-R9`, `P3-R9-GATE`를 완료했고 `REV-022`를 처리 완료로 동기화했다. - 다음 Goal: `P4-R1`. - 보완 결과: `P4-R1`을 완료했고 `REV-023`~`REV-024`를 처리 완료로 동기화했다. - 다음 Goal: `P4-R1-GATE`. - 보완 결과: `P4-R1-GATE`를 완료했고 Phase 4 후속 리뷰를 종료했다. - 다음 Goal: `P5-R1`. - 보완 결과: `P5-R1`, `P5-R1-GATE`를 완료했고 `REV-025`~`REV-026`을 처리 완료로 동기화했다. - 다음 Goal: `P6-R1`. - 보완 결과: `P6-R1`을 완료했다. FanTalk 목록은 공개 v2 `CreatorChannelFanTalkQueryPolicy`를 재사용해 `page < 0 -> 0`, `size < 20 -> 20`, `size > 50 -> 50`으로 보정하고, reply body는 strict reader로 미지 필드 400/no insert/no event를 고정했다. - 다음 Goal: `P6-R1-GATE`. - 보완 결과: `P6-R1-GATE`를 완료했고 Phase 6 후속 리뷰를 종료했다. - 다음 Goal: `P7-R1`. - 보완 결과: `P7-R1`을 완료했다. plan/API 설명/OpenAPI status를 23개 구현 완료로 동기화했고 controller mapping 23개와 validator/client 생성·compile을 확인했다. - 다음 Goal: `P7-R1-GATE`. - 보완 결과: `P7-R1-GATE`를 완료했고 `REV-021`~`REV-029` 전체를 처리 완료로 종결했다. - 다음 Goal: 없음. ### Phase 1~7 후속 정적 리뷰 완료 — 2026-07-28 - 상태: 리뷰 완료, `P3-R10` 시작 대기 - 무엇을: PRD, plan-task, OpenAPI와 현재 Phase 1~7 production/test를 완료 기록 이후 상태 기준으로 다시 정적 대조했다. - 왜: 통과한 컴파일·테스트가 다루지 않은 의미 검증, 상태 전이, 필수 multipart binding과 실제 동시성 불변식을 확인하고 확정 항목을 실행 가능한 후속 Task로 전환하기 위해서다. - 어떻게: 요청값에서 controller/facade/legacy service/repository/예외 handler까지 호출 흐름을 역추적하고, 관련 테스트가 실제 병렬·경계 상태를 검증하는지 `rg`, `sed`, `jq`, `git diff`로 확인했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. - Phase 1 결과: 공통 target resolver, 보안, 오류 handler에서 신규 확정 finding 없음. - Phase 2 결과: Character 4개 operation과 mutation pipeline에서 신규 확정 finding 없음. - Phase 3 결과: 생성 `releaseDate`/`timezone`의 의미 오류가 500으로 분류되는 `REV-030`을 확정하고 `Task 3.20` / `P3-R10`으로 전환했다. - Phase 4 결과: soft-delete된 linked content를 해제할 수 없는 `REV-031`, 생성 필수 `image`가 nullable binding인 `REV-032`를 확정하고 `Task 4.8` / `P4-R2`로 전환했다. - Phase 5 결과: 순차 테스트만으로 완료 처리되어 실제 병렬 요청에서 최대 고정 3개를 보장하지 못하는 `REV-033`을 확정하고 `Task 5.8` / `P5-R2`로 전환했다. - Phase 6 결과: FanTalk 2개 operation에서 신규 확정 finding 없음. - Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어 `Task 7.4` / `P7-R2` 통합 재판정을 추가했다. - 다음 Goal: `P3-R10`. ### Phase 1~7 3차 정적 리뷰 완료 — 2026-07-28 - 상태: 리뷰 완료, `P2-R7` 시작 대기 - 무엇을: PRD, plan-task, OpenAPI와 현재 Phase 1~7 production/test를 최신 완료 상태 기준으로 다시 정적 대조했다. - 왜: 통과한 컴파일·테스트가 다루지 않은 empty multipart와 optional boolean, locale별 예약일 표시 의미를 확인하고, 확정 항목을 해당 Phase의 실행 가능한 후속 Task로 전환하기 위해서다. - 어떻게: actual controller에서 facade, mapper, 레거시 controller/service, S3·DB·event 경계까지 호출 흐름을 `rg`, `sed`, `jq`, `git diff`로 추적했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. - Phase 1 결과: 공통 resolver, 보안, 오류/CORS 경계에서 신규 확정 finding 없음. - Phase 2 결과: 생성의 빈 필수 image가 통과하는 `REV-034`, `isActive=true` 단독 수정이 레거시와 달리 거부되는 `REV-035`를 확정하고 `Task 2.13` / `P2-R7`으로 전환했다. - Phase 3 결과: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null인 `REV-036`을 확정하고 `Task 3.21` / `P3-R11`로 전환했다. - Phase 4 결과: 생성·수정의 빈 image가 0-byte upload를 유발하는 `REV-037`을 확정하고 `Task 4.9` / `P4-R3`으로 전환했다. - Phase 5 결과: Community 3개 operation과 owner lock에서 신규 확정 finding 없음. - Phase 6 결과: FanTalk 2개 operation에서 신규 확정 finding 없음. - Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어 `Task 7.5` / `P7-R3` 통합 재판정을 추가했다. - 다음 Goal: `P2-R7`. ### Phase 1~7 4차 정적 리뷰 완료 — 2026-07-29 - 상태: 리뷰 완료, `P7-R4` 시작 대기 - 무엇을: PRD, plan-task, OpenAPI와 최신 Phase 1~7 production/test를 후속 Gate 완료 상태 기준으로 정적 대조했다. - 왜: 컴파일·테스트 통과 이후에도 남을 수 있는 route/schema 의미와 완료 상태 기록 불일치를 확인하기 위해서다. - 어떻게: controller/facade/mapper/repository에서 legacy service까지 호출 경로를 추적하고 `sed`, `rg`, `jq`, `git diff --check`로 문서·23개 operation·controller mapping·dependency/DDL 변경 범위를 확인했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. - Phase 1 결과: resolver, ADMIN 이중 인가, 오류/CORS/firewall 경계에서 신규 확정 finding 없음. - Phase 2 결과: Character 4개 operation의 최신 empty image·`isActive=true` 보완을 확인했고 기능 finding 없음. - Phase 3 결과: AudioContent 5개 operation의 예약 공개일·signed URL·ownership 경계를 확인했고 기능 finding 없음. - Phase 4 결과: Series 9개 runtime operation은 일치하지만 Phase 4 endpoint 설명의 DELETE path/body가 현재 계약과 다른 문서 문제를 `REV-039`로 확정했다. - Phase 5 결과: Community 3개 operation과 owner lock·soft delete에서 신규 확정 finding 없음. - Phase 6 결과: FanTalk 2개 operation과 pagination/root ownership에서 신규 확정 finding 없음. - Phase 7 결과: 완료 증거가 있는 네 Task 헤더가 `[ ]`로 남아 상단 완료 상태와 모순되는 `REV-038`을 확정했다. - plan 전환: 두 문서 정합성 finding을 `Task 7.6` / `P7-R4`로 묶었다. - 다음 Goal: `P7-R4`. ### `P7-R4` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: 완료 증거가 있는 `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더와 상단 상태표를 완료 상태로 동기화하고, Phase 4 시리즈 콘텐츠 해제 설명을 path `contentId`와 request body 없음 계약으로 정정했다. - 왜: `REV-038`~`REV-039`가 기능 문제가 아니라 후속 작업 판단을 오도하는 문서 정합성 문제로 확정됐기 때문이다. - 어떻게: 기존 Gate와 2026-07-29 검증 기록은 보존하고 문서 상태만 갱신한 뒤 `./gradlew tasks --all`, OpenAPI 23개 operation/status `jq`, 미완료 Task header `rg`, controller mapping `rg`, `git diff --check`를 실행했다. - 결과: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 767ms`, OpenAPI assertion은 `true`, 미완료 Task header와 `git diff --check`는 출력이 없었고 controller mapping은 23개였다. `REV-038`~`REV-039`를 처리 완료로 판정했다. - 남은 항목: 없음. ### `P2-R8` / `P2-R8-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: 캐릭터 생성 관계 `importance` 누락·null을 v2 request 경계에서 400 `common.error.invalid_request`로 거부하도록 보완했다. - 왜: OpenAPI required non-null integer가 Jackson/Kotlin primitive 기본값 `0`으로 보정되면 잘못된 관계 입력이 외부 API·DB·S3·event 부작용으로 이어질 수 있기 때문이다. - 어떻게: production 변경은 `AiCharacterAdminCharacterFacade.readRequest()`의 strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가하는 최소 범위로 제한하고, actual multipart POST RED/GREEN과 no-side-effect를 추가했다. - 결과: RED 명령은 신규 2건이 `status().isBadRequest` 기대에서 실패했고, 보완 후 같은 명령과 `AiCharacterAdminCharacterControllerMutationTest`, character/common 영향 범위 회귀, `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. `git diff --check`는 출력이 없었다. - 남은 항목: Phase 3 `P3-R12`. ### `P3-R12` / `P3-R12-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: 오디오 생성 `price` 누락·null과 primitive boolean explicit null을 v2 request 경계에서 400 `common.error.invalid_request`로 거부하도록 보완했다. - 왜: OpenAPI required/non-null primitive가 Jackson/Kotlin 기본값 `0`/`false`로 보정되면 잘못된 생성 요청이 파일 업로드·DB·event 부작용으로 이어질 수 있기 때문이다. - 어떻게: production 변경은 `AiCharacterAdminAudioContentFacade.readRequest()`의 strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`와 `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가하는 최소 범위로 제한하고, actual multipart POST RED/GREEN과 no-side-effect를 추가했다. - 결과: RED 명령은 신규 8개 invocation이 `status().isBadRequest` 기대에서 실패했고, `themeId:null`은 기존 missing-theme guard로 이미 400이었다. 보완 후 같은 명령과 `AiCharacterAdminAudioContentCreateTest`, content/common 영향 범위 회귀, `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. `git diff --check`는 출력이 없었다. - 남은 항목: Phase 4 `P4-R4`. ### Phase 1~7 5차 정적 리뷰 완료 — 2026-07-29 - 상태: 리뷰 완료, `P2-R8` 시작 대기 - 무엇을: PRD, plan-task, OpenAPI와 최신 Phase 1~7 production/test의 JSON request 경계를 primitive required/non-null/default 의미까지 정적 대조했다. - 왜: Kotlin non-null primitive도 Jackson 2.13.5 기본 설정에서는 누락·null이 JVM 기본값으로 보정될 수 있어, 컴파일·기존 테스트 통과만으로 OpenAPI nullability를 보장하지 못하기 때문이다. - 어떻게: 각 multipart `request`의 strict reader 설정, Kotlin DTO primitive 타입, OpenAPI required/nullable/default와 로컬 Jackson Kotlin/databind 2.13.5 source를 역추적했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. - Phase 1 결과: 공통 resolver, security, 오류 handler에서 신규 확정 finding 없음. 전역 mapper 변경도 후속 범위에서 제외했다. - Phase 2 결과: 관계 필수 `importance` 누락·null이 `0`으로 보정될 수 있는 `REV-040`을 확정하고 `Task 2.14` / `P2-R8`로 전환했다. - Phase 3 결과: 생성 필수 `price` 누락·null과 non-null primitive의 explicit null이 기본값으로 보정될 수 있는 `REV-041`을 확정하고 `Task 3.22` / `P3-R12`로 전환했다. - Phase 4 결과: 생성 `genreId`, `isAdult`의 explicit null이 기본값으로 보정될 수 있는 `REV-042`를 확정하고 `Task 4.10` / `P4-R4`로 전환했다. - Phase 5 결과: 생성 필수 boolean·`price`와 수정 `isFixed`의 null/누락이 거부되지 않는 `REV-043`을 확정하고 `Task 5.9` / `P5-R3`로 전환했다. - Phase 6 결과: FanTalk reply는 primitive 요청 필드가 없고 기존 문자열 null/blank 경계가 유지되어 신규 finding 없음. - Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어 `Task 7.7` / `P7-R5` 통합 재판정을 추가했다. - 다음 Goal: `P2-R8`. ### Community 목록 계약 변경 확정 — 2026-07-29 - 상태: `P5-R4` / `P5-R4-GATE` 완료 - 무엇을: Community 목록에서 사용되지 않는 `timezone` query를 제거하고 응답 `data`를 `totalCount`, `page`, `size`, `hasNext`, `items` wrapper로 변경했다. - 왜: 관리자 UI가 active owner 게시글 전체 개수와 다음 page 추가 로딩 필요 여부를 판단해야 하기 때문이다. - 어떻게: 기존 item 필드와 고정 우선 정렬은 유지하고 active owner count query 하나를 추가하는 최소 설계로 PRD, OpenAPI, API 설명과 Phase 5 신규 Task/Gate를 동기화했다. - runtime 상태: controller/facade의 `timezone` query를 제거하고 `data` pagination wrapper를 반환하도록 정합화했다. - plan 전환: `Task 5.10` / `P5-R4`, `P5-R4-GATE`를 완료하고 `P7-R5` 시작 조건을 충족했다. - 다음 Goal: `P7-R5`. ### `P7-R5` / `P7-R5-GATE` 완료 — 2026-07-29 - 상태: 완료 - 무엇을: Phase 2~5 primitive required/nullability 보완과 Community 목록 wrapper 계약을 23개 관리자 operation 기준으로 통합 재판정했다. - 왜: 후속 production/API 계약 변경 뒤 공통 JWT ADMIN 이중 인가, target/owner 오류, JSON 오류 envelope, no-side-effect와 legacy 회귀가 유지되는지 확인하기 위해서다. - 어떻게: targeted 통합, 전체 회귀, lint, OpenAPI 23개 `implemented`, controller mapping 23개, dependency/DDL 무변경, diff whitespace를 fresh 검증했다. - 결과: targeted `BUILD SUCCESSFUL in 2m 21s`, 전체 test `BUILD SUCCESSFUL in 5m 46s`, `ktlintCheck` `BUILD SUCCESSFUL in 881ms`, OpenAPI assertion `true`, mapping 23개, dependency/DDL 검색과 `git diff --check` 출력 없음이었다. - 남은 항목: 없음. ### 후속 기능 계획 확정 — 2026-07-29 - 상태: `P5-R5` / `P5-R5-GATE` 완료 - Phase 2 결과: 캐릭터 등록용 원작 검색을 `Task 2.15` / `P2-R9`로 추가했다. - Phase 3 결과: 오디오 콘텐츠 댓글 CRUD 5개 operation을 `Task 3.23` / `P3-R13`으로 추가했다. - Phase 4 결과: 시리즈 등록용 장르 목록을 `Task 4.11` / `P4-R5`, 상세 `data`의 목록 item 정합화를 `Task 4.12` / `P4-R6`으로 추가했다. - Phase 5 결과: 커뮤니티 댓글 CRUD 5개 operation을 `Task 5.11` / `P5-R5`로 추가했다. - Phase 6 결과: 팬 작성 FanTalk 원글 soft delete를 `Task 6.7` / `P6-R2`로 추가했다. - Phase 7 결과: 기존 23개와 신규 13개를 합한 36개 operation 통합 재판정을 `Task 7.8` / `P7-R6`으로 추가했다. - 제외 결과: 캐릭터에 직접 달리는 레거시 댓글 삭제는 v2 전환 뒤 미사용이라는 사용자 확정에 따라 operation과 Task를 추가하지 않았다. - 검증: 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않고 JSON 문법, operation/status 집계, 내부 `$ref`, 시리즈 상세 schema 참조와 문서 diff만 정적으로 확인한다. - P2-R9 결과: 캐릭터 등록용 원작 검색 endpoint를 기존 `AdminOriginalWorkService.searchOriginalWorksAll`과 `OriginalWorkResponse.from` 재사용으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다. - P2-R9 검증: focused RED 3개 실패 확인 후 GREEN `BUILD SUCCESSFUL`, Character/common 영향 범위 회귀 `BUILD SUCCESSFUL`, `ktlintCheck` `BUILD SUCCESSFUL`, `git diff --check` 출력 없음. - P3-R13 결과: 오디오 콘텐츠 댓글 CRUD 5개 endpoint를 기존 `AudioContentCommentService` 조회·작성·수정 의미 재사용과 v2 facade의 target/owner/parent/actor 선검증으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다. - P3-R13 검증: focused RED 7건은 미구현 route의 404/405로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 3m 16s`, content/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 3m 47s`, `ktlintCheck`는 import 순서 1건 수정 후 `BUILD SUCCESSFUL in 32s`, `git diff --check`는 출력 없음이었다. - P4-R5 결과: 시리즈 등록용 장르 목록 endpoint를 기존 `AdminContentSeriesGenreService.getSeriesGenreList` 재사용으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다. - P4-R5 검증: focused RED 2건은 미구현 route로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 1m 24s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 38s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`, `git diff --check`는 출력 없음이었다. - P4-R6 결과: 시리즈 상세 `data`를 목록 `items` 단일 항목과 동일한 `GetCreatorAdminContentSeriesListItem` schema로 반환하도록 정합화하고 OpenAPI 상태를 `implemented`로 갱신했다. - P4-R6 검증: focused RED는 레거시 상세 필드 차이로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 43s`였다. focused query/contract 회귀는 `BUILD SUCCESSFUL in 38s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 19s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 23s`, `git diff --check`는 출력 없음이었다. - P5-R5 결과: 커뮤니티 댓글 CRUD 5개 endpoint를 기존 `CreatorCommunityService` 조회·작성·수정 의미 재사용과 v2 facade의 target/owner/parent/actor 선검증으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다. - P5-R5 검증: focused RED 7건은 미구현 route로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 2m`였다. community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 26s`, `ktlintCheck`는 import 순서 1건 수정 후 `BUILD SUCCESSFUL in 22s`, `git diff --check`는 출력 없음이었다. - P6-R2 결과: 팬 작성 FanTalk 원글 삭제 endpoint를 `CreatorCheers.isActive` row 단위 soft delete로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다. - P6-R2 검증: focused RED 6건은 DELETE route 미구현으로 실패했고, GREEN 이후 누락 ID를 포함한 focused 7건은 `BUILD SUCCESSFUL in 29s`였다. FanTalk/common 영향 범위 회귀는 DELETE 인가 matrix 보강 후 `BUILD SUCCESSFUL in 58s`였다. - P6-R2-GATE 검증: `AiCharacterAdminAuthorizationTest` 단독은 `BUILD SUCCESSFUL in 29s`, 최종 `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 14s`, `git diff --check`는 출력 없음이었다. - P7-R6 결과: Phase 2~6 후속 기능과 시리즈 상세 정합화 뒤 36개 관리자 operation의 계약·route·문서 상태를 통합 재판정했고, `Task 7.8` / `P7-R6-GATE`를 완료했다. - P7-R6 검증: targeted `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`는 `BUILD SUCCESSFUL in 2m 24s`, 전체 `./gradlew test`는 `BUILD SUCCESSFUL in 8m 9s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. OpenAPI 36개 operation/36개 `implemented` assertion은 `true`, controller mapping은 36개, 캐릭터 직접 댓글 route 검색은 0개, dependency/DDL 추가 검색과 `git diff --check`는 출력 없음이었다. - 다음 Goal: 없음. ### UTC 날짜 계약 변경 확정 — 2026-07-29 - 상태: 구현 완료 - 무엇을: 신규 관리자 오디오 생성 request의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 GET의 `timezone` query를 제거했다. 생성 `releaseDate`는 클라이언트가 보내는 nullable ISO-8601 UTC(`Z`), 상세 `releaseDate`와 댓글 `date`는 기존 필드명·null/노출 조건을 유지한 ISO-8601 UTC(`Z`)로 문서 계약을 확정했다. - 왜: 서버가 클라이언트별 timezone을 받아 표시 문자열을 만들 필요 없이 절대 시각은 UTC로 교환하고 표시 변환은 클라이언트가 담당하도록 단일 계약을 유지하기 위해서다. - 어떻게: production/test는 변경하거나 실행하지 않고 PRD, OpenAPI 2.2.0, 계약 설명, 구현 계획과 Phase 3·5·7 리뷰에 신규 `P3-R14`, `P5-R6`, `P7-R7` Task/Gate를 누적했다. - 결과: 전체 route 36개는 유지된다. 최신 OpenAPI 상태는 `implemented` 36개, `alignment-required` 0개, `planned` 0개다. - 다음 Goal: 없음. ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | |---|---|---|---|---|---| | 2026-07-27 | `DEC-GOAL-001` | 확정 | 기존 Phase 2·3 완료 Task는 이력으로 보존하고 심층 리뷰·세부 보완·Gate Goal을 추가한다. | 기존 검증 이후 후속 보완이 반복됐고 Phase 완료 Gate가 없었다. | `P2-R1`~`P3-GATE` | | 2026-07-28 | `DEC-REVIEW-007` | 확정 | Phase 1~7 정적 리뷰의 확정 finding 9건은 기존 완료 Task를 다시 열지 않고 각 소유 Phase의 신규 Task/Gate로 직렬 처리한다. | 완료 이력을 보존하면서 OpenAPI 단일 원본과 실제 HTTP 경계의 불일치를 최소 범위로 수정해야 한다. | `P2-R6`~`P7-R1-GATE`, Phase별 review 문서 | | 2026-07-28 | `DEC-REVIEW-008` | 확정 | 후속 정적 리뷰의 `REV-030`~`REV-033`은 기존 완료 Task를 다시 열지 않고 Phase 3~5 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | 정상·순차 경로의 테스트 통과와 별개로 의미 오류 500, soft-delete link 상태 전이, 필수 multipart binding, count-then-update 경쟁 조건이 코드·문서 근거로 확정됐다. | `P3-R10`~`P7-R2-GATE`, Phase별 review 문서 | | 2026-07-28 | `DEC-REVIEW-009` | 확정 | 3차 정적 리뷰의 `REV-034`~`REV-037`은 기존 완료 Task를 다시 열지 않고 Phase 2~4 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | required/optional multipart는 part 존재만으로 파일 유효성을 보장하지 않고, optional boolean과 예약일 표시의 레거시 의미가 현재 mapper/facade에서 소실되는 코드 경로가 확정됐다. | `P2-R7`~`P7-R3-GATE`, Phase별 review 문서 | | 2026-07-29 | `DEC-REVIEW-010` | 확정 | 4차 정적 리뷰의 `REV-038`~`REV-039`는 production/OpenAPI를 변경하지 않고 Phase 7 문서 정합성 Task 하나로 처리한다. | 네 후속 Task는 하위 체크리스트·Gate·검증 기록상 완료됐지만 헤더가 미완료이고, Phase 4 DELETE 설명은 OpenAPI/controller와 달라 후속 작업 상태와 route 판단을 오도한다. | `P7-R4`, Phase 7 review 문서 | | 2026-07-29 | `DEC-REVIEW-011` | 확정 | 5차 정적 리뷰의 `REV-040`~`REV-043`은 전역 Jackson 또는 레거시 DTO를 변경하지 않고 각 v2 request 경계의 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | Jackson Kotlin/databind 2.13.5 기본 동작은 Kotlin primitive의 누락·null을 JVM 기본값으로 보정할 수 있고, 현재 strict reader는 미지 필드만 거부해 OpenAPI required/non-null 계약을 강제하지 못한다. | `P2-R8`~`P7-R5-GATE`, Phase별 review 문서 | | 2026-07-29 | `DEC-REVIEW-012` | 확정 | 6차 정적 리뷰의 `REV-052`~`REV-058`은 OpenAPI를 변경하지 않고 각 v2 HTTP 경계의 신규 Task/Gate로 직렬 처리한 뒤 Phase 7에서 통합 재판정한다. | canonical OpenAPI의 optional pagination, JSON-only/415, multipart `request` part `application/json` 계약이 controller binding·mapping과 직접 불일치하며, domain 의미 변경 없이 소유 Phase의 최소 수정으로 해결할 수 있다. | `P2-R10`~`P7-R8-GATE`, Phase별 review 문서 | | 2026-07-29 | `DEC-REVIEW-013` | 확정 | 7차 정적 리뷰의 `REV-060`~`REV-063`은 OpenAPI를 변경하지 않고 Phase 2~5 multipart controller 경계에서 operation별 허용 part 이름을 강제하며, `REV-064` 문서 상태와 함께 Phase 7에서 통합 재판정한다. | 8개 canonical multipart schema는 모두 `additionalProperties: false`지만 controller는 선언된 인자만 binding하고 실제 전체 part 이름을 검증하지 않아 미정의 part를 무시한다. OpenAPI는 37개 모두 `implemented`인데 계획 요약과 계약 설명은 36개 구현·1개 planned로 남아 있다. | `P2-R11`~`P7-R9-GATE`, Phase별 review 문서 | | 2026-07-29 | `DEC-REVIEW-014` | 확정 | 9차 정적 리뷰의 `REV-072`는 preview 규칙을 재구현하지 않고 두 오디오 생성 경로가 공유하는 parsed request overload에서 기존 검증을 정확히 한 번 수행하도록 Phase 3에서 최소 보완한 뒤 Phase 7에서 통합 재판정한다. | v2 facade는 신규 overload를 호출하지만 기존 preview 쌍·형식·최소 15초 검증은 문자열 request overload에만 남아 있어 잘못된 preview 입력이 DB/S3/event 경계로 진행된다. | `P3-R19`~`P7-R11-GATE`, Phase 3·7 review 문서 | | 2026-07-29 | `DEC-FANTALK-REPLY-UPDATE-001` | 확정 | FanTalk 답변 수정은 레거시 `PUT /explorer/profile/cheers`의 optional/nullable `content`, `isActive`, 빈 객체 no-op, 비활성 reply 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지한다. 신규 path ID와 target AI·활성 root·direct reply 검증만 추가한다. | 사용자 요청과 “기존 계약과 동일” 확정, `ExplorerService.modifyCheers`의 상태 전이와 응답 mapper | `P6-R4`, `P6-R4-GATE`, `P7-R8`, PRD, OpenAPI 2.3.0 | | 2026-07-29 | `DEC-P5-LIST-001` | 확정 | Community 관리자 목록은 `timezone` query를 제거하고 `data`를 `totalCount`, `page`, `size`, `hasNext`, `items`로 반환한다. `totalCount`와 `hasNext`는 target creatorMember의 active 게시글만 기준으로 계산한다. | 목록 응답의 상대 시간은 timezone을 사용하지 않으며 관리자 UI가 전체 개수와 다음 page 추가 로딩 여부를 판단해야 한다는 사용자 확정 요구사항을 반영한다. | PRD, OpenAPI, `Task 5.10`, `P5-R4`~`P5-R4-GATE`, `P7-R5` | | 2026-07-29 | `DEC-COMMENT-001` | 확정 | 오디오 콘텐츠·커뮤니티 댓글은 target AI 캐릭터 명의로 작성하고 target 작성 댓글만 수정하며 target 소유 자산의 댓글은 작성자와 관계없이 row 단위 soft delete한다. | 사용자 승인과 기존 콘텐츠·게시글 소유자의 댓글 비활성화 동작을 유지한다. | `P3-R13`, `P5-R5`, OpenAPI | | 2026-07-29 | `DEC-CHAR-COMMENT-001` | 제외 | 사용하지 않는 레거시 캐릭터 직접 댓글 API는 v2로 전환하거나 관리자 삭제 기능을 추가하지 않는다. | 사용자 확인 결과 v2 전환 뒤 사용하지 않는다. | Non-Goals, `P7-R6` | | 2026-07-29 | `DEC-UTC-DATE-001` | 확정 | 신규 관리자 오디오 생성의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 GET의 `timezone` query를 제거한다. 생성 `releaseDate`는 클라이언트가 UTC로 변환해 보내고, 상세 `releaseDate`와 댓글 `date`는 기존 필드명을 유지한 ISO-8601 UTC(`Z`)로 반환한다. 로컬 시각+timezone 입력은 병행 지원하지 않는다. | 서버가 클라이언트 표시 timezone을 해석하지 않고 단일 절대 시각 계약을 유지한다는 사용자 승인 | `P3-R14`, `P5-R6`, `P7-R7`, PRD, OpenAPI 2.2.0 | | 2026-07-29 | `DEC-FANTALK-DELETE-001` | 확정 | 팬 작성 FanTalk root 삭제는 원글만 soft delete하고 연결 creator reply row는 변경하지 않는다. | 사용자 승인과 기존 `CreatorCheers.isActive` 상태 전이를 유지한다. | `P6-R2`, OpenAPI | | 2026-07-29 | `DEC-REGISTRATION-REFERENCE-001` | 확정 | 캐릭터 등록용 원작 검색과 시리즈 등록용 장르 목록을 target 없는 신규 v2 관리자 endpoint로 제공한다. | 캐릭터 관리자 frontend가 동일한 v2 ADMIN/CORS 경계에서 등록 참조 정보를 조회해야 한다. | `P2-R9`, `P4-R5`, OpenAPI | | 2026-07-29 | `DEC-SERIES-DETAIL-001` | 확정 | 시리즈 상세 `data`를 목록 `items`의 단일 항목과 동일한 schema로 변경하고 기존 상세 전용 `genre`, `keywords`를 제거한다. | 사용자 확정과 관리자 목록·상세 DTO 일관성을 반영한다. | `P4-R6`, OpenAPI | | 2026-07-28 | `DEC-P4-R2-001` | 확정 | 시리즈 생성 `image` 누락은 PRD 공통 binding 계약과 OpenAPI required part를 따라 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request`로 처리한다. | nullable binding을 통한 legacy `creator.admin.series.cover_image_required`는 필수 part가 MVC를 통과한 결과이며 PRD §8의 명시적 누락 part 계약과 충돌한다. | `P4-R2`, `REV-032` | | 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`, 하단 발견된 문제 표 | | 2026-07-28 | `DEC-API-CONTRACT-001` | 확정 | 신규 관리자 endpoint는 레거시 request/response의 필드명·타입·optional/nullable·기본값·성공 `data` 형태를 유지하고 path로 이동한 ID만 body에서 제거한다. FanTalk 답변은 축약 응답을 유지한다. | 신규 endpoint의 목적이 로그인 불가능한 AI 캐릭터를 관리자 경계로 대리 관리하는 것이며, 클라이언트 계약까지 임의로 재설계하는 범위가 아니기 때문이다. | PRD, `api-contract.openapi.json`, `P23-CONTRACT-1`~`P23-CONTRACT-GATE` | | 2026-07-28 | `DEC-API-CONTRACT-002` | 확정 | FanTalk 목록은 공개 v2 field 형태를 유지하는 관리자 전용 endpoint로 추가하고, 시리즈 연결 목록과 미연결 검색은 응답 형태가 달라 별도 endpoint로 분리한다. | 공개 v2 직접 호출은 `creatorId`, viewer/block filter와 CORS 경계가 관리자 요구와 다르고, 시리즈 두 legacy API의 응답은 wrapper와 direct array로 서로 다르다. | Phase 4, Phase 6, `api-contract.openapi.json` | | 2026-07-28 | `DEC-API-CONTRACT-003` | 확정 | `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한을 최신 JSON 계약에서 폐기한다. `ChatCharacterUpdateRequest`처럼 다른 optional field와 동시 입력을 허용하고, `isActive=false`이면 레거시 service와 같이 비활성화만 반영한다. | 사용자 확정 원칙은 path ID만 제거하고 레거시 request를 그대로 이관하는 것이다. 레거시 controller는 혼합 request를 받으며 service는 비활성화 분기에서 나머지 JSON field를 적용하지 않는다. | `api-contract.openapi.json`, `P23-CONTRACT-2` | | 2026-07-28 | `DEC-API-CONTRACT-004` | 확정 | 생성된 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE` 완료 조건에는 추가하지 않는다. | 서버 Gate는 실제 HTTP runtime과 OpenAPI validate/client 생성 가능성을 판정하며, 언어별 client compile은 소비 클라이언트의 toolchain에서 검증해야 한다. 계약 작성 Task의 일회성 TypeScript 생성·컴파일 검증은 이미 완료됐다. | `P23-CONTRACT-GATE`, `api-contract.md` | ## 발견된 문제 | ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | |---|---|---|---|---|---| | `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을 단언했다. | | `REV-021` | Low | 처리 완료 | Character POST/PUT이 `data: null`만 반환하는데 facade가 전체 response DTO를 매핑해 controller가 버린다. | `P2-R6`, `P2-R6-GATE` | facade 반환형을 `Unit`으로 축소하고 미사용 mapping을 제거한 뒤 mutation 계약을 회귀했다. | | `REV-022` | Low | 처리 완료 | AudioContent 관리자 repository에 실제 호출되지 않는 조회·series 교체 helper와 그 전용 status enum이 남아 있다. | `P3-R9`, `P3-R9-GATE` | 사용 중인 owner-scoped 상세 조회만 남기고 호출 0건 코드를 제거한 뒤 content 회귀를 실행했다. | | `REV-023` | High | 처리 완료 | OpenAPI Series 9개 operation에 없는 `DELETE /series/{seriesId}`가 구현·테스트되어 공개 API 표면이 계약보다 넓다. | `P4-R1`, `P4-R1-GATE` | 계약 밖 route/facade를 제거하고 DELETE 성공 기대를 405와 `PUT isActive=false` 계약으로 교정했다. | | `REV-024` | Medium | 처리 완료 | Series 콘텐츠 추가·순서 변경 JSON body가 permissive ObjectMapper binding으로 `additionalProperties: false`를 강제하지 않는다. | `P4-R1`, `P4-R1-GATE` | 두 body를 strict parse하고 미지 필드 400/no-side-effect를 actual endpoint로 고정했다. | | `REV-025` | High | 처리 완료 | Community create/update의 수동 JSON parse 오류가 공통 400으로 변환되지 않아 malformed payload가 500이 될 수 있고 미지 필드도 허용된다. | `P5-R1`, `P5-R1-GATE` | legacy 호출 전 strict parse와 mapping 예외 변환을 적용하고 400/no-side-effect를 고정했다. | | `REV-026` | Medium | 처리 완료 | Community 목록이 OpenAPI에 없는 `size <= 50` 상한을 적용해 계약상 유효한 `size=51`을 400으로 거부한다. | `P5-R1`, `P5-R1-GATE` | 문서에 없는 상한 guard를 제거하고 `size=51` actual endpoint 계약을 고정했다. | | `REV-027` | High | 처리 완료 | FanTalk 목록이 공개 v2 계약의 page/size 보정 대신 범위 밖 값을 400으로 거부하고 `size=1`도 허용한다. | `P6-R1`, `P6-R1-GATE` | 공개 v2 query policy를 재사용하고 경계값 actual test를 교정했다. | | `REV-028` | Medium | 처리 완료 | FanTalk reply JSON body가 permissive binding으로 OpenAPI의 `additionalProperties: false`를 강제하지 않는다. | `P6-R1`, `P6-R1-GATE` | reply body를 strict parse하고 미지 필드 400/no insert/no event를 고정했다. | | `REV-029` | Medium | 처리 완료 | plan/API 계약 설명/OpenAPI `x-implementation-status`가 완료된 구현을 여전히 정합화 필요·예정으로 표시한다. | `P7-R1`, `P7-R1-GATE` | 23개 operation metadata를 모두 `implemented`로 동기화하고 controller mapping 23개와 validator/client 생성을 확인했다. | | `REV-030` | Medium | 처리 완료 | AudioContent 생성의 잘못된 `releaseDate` 형식·`timezone`이 legacy Java time 예외로 빠져 500이 된다. | `P3-R10`, `P3-R10-GATE` | strict parse 결과의 날짜·시간대를 legacy 호출 전에 검증하고 400/no-side-effect를 고정했다. | | `REV-031` | Medium | 처리 완료 | 시리즈에 연결된 콘텐츠를 soft delete하면 추가 적격성 guard 때문에 연결을 해제할 수 없다. | `P4-R2`, `P4-R2-GATE` | 해제는 실제 owner link만 검증하고 add/search 전용 active/release/duration 적격성은 적용하지 않는다. | | `REV-032` | Medium | 처리 완료 | OpenAPI 필수 시리즈 생성 `image`가 nullable binding이라 공통 missing-part 오류 계약을 우회한다. | `P4-R2`, `P4-R2-GATE` | 생성 image를 non-null binding으로 바꾸고 exact exception·KO/EN/JA·no-side-effect를 고정했다. | | `REV-033` | High | 처리 완료 | Community 최대 고정 수가 lock 없는 count-then-update라 실제 병렬 요청에서 3개를 초과할 수 있다. | `P5-R2`, `P5-R2-GATE` | 기존 owner row pessimistic lock으로 고정/해제를 직렬화하고 결정적 병렬 회귀 테스트를 추가했다. | | `REV-034` | Medium | 처리 완료 | OpenAPI 필수 캐릭터 생성 `image`를 빈 part로 보내면 외부 생성과 DB 저장이 진행되고 이미지 없는 캐릭터가 생성된다. | `P2-R7`, `P2-R7-GATE` | facade에서 empty file을 외부 API·DB·S3·event 전에 400으로 거부하고 no-side-effect를 고정했다. | | `REV-035` | Medium | 처리 완료 | 캐릭터 수정의 `isActive=true` 단독 요청이 OpenAPI·레거시에서는 유효하지만 v2 no-change guard에서 400으로 거부된다. | `P2-R7`, `P2-R7-GATE` | non-null `isActive`를 변경 요청으로 인정해 레거시 200 `data: null` no-op parity를 복구했다. | | `REV-036` | High | 처리 완료 | 미래 예약 오디오 콘텐츠 상세의 `releaseDate`가 mapper에서 항상 null로 고정되어 레거시 locale별 공개 예정 시각을 숨긴다. | `P3-R11`, `P3-R11-GATE` | 미래/과거와 KO/EN/JA 레거시 표시 규칙을 mapper에 최소 이관하고 상세 회귀를 고정했다. | | `REV-037` | Medium | 처리 완료 | 시리즈 생성·수정의 빈 image가 legacy service로 전달되어 0-byte S3 upload와 cover 생성·교체를 유발한다. | `P4-R3`, `P4-R3-GATE` | 생성 empty file은 거부하고 수정 empty file은 생략으로 정규화해 S3/DB/event 경계를 고정했다. | | `REV-038` | Low | 처리 완료 | 완료 증거와 Gate가 존재하는 `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더가 `[ ]`로 남아 상단 `구현 완료`·완료 수와 모순된다. | `P7-R4` | 기존 완료 기록을 보존하며 네 Task 헤더와 상태표·Progress만 동기화했다. | | `REV-039` | Low | 처리 완료 | Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE .../contents` + request body로 적었지만 OpenAPI와 controller는 `DELETE .../contents/{contentId}` + body 없음이다. | `P7-R4` | Phase 4 설명만 기계 검증 가능한 OpenAPI와 실제 route에 맞췄다. | | `REV-040` | Medium | 처리 완료 | 캐릭터 생성 관계의 OpenAPI 필수 정수 `importance`가 누락·null이어도 Kotlin/Jackson primitive 기본값 `0`으로 역직렬화되어 mutation이 진행될 수 있다. | `P2-R8`, `P2-R8-GATE` | v2 생성 경계에서 primitive null/누락을 400으로 거부하고 400/no-side-effect 회귀를 추가했다. | | `REV-041` | High | 처리 완료 | 오디오 생성의 필수 `price` 누락·null과 non-null primitive의 explicit null이 JVM 기본값으로 보정되어 파일 업로드·DB mutation이 진행될 수 있고 문서 기본값 의미도 바뀔 수 있다. | `P3-R12`, `P3-R12-GATE` | v2 생성 경계에서 primitive null/누락을 400으로 거부하고 optional 생략 기본값과 no-side-effect 회귀를 추가했다. | | `REV-042` | Medium | 처리 완료 | 시리즈 생성의 non-null `genreId`, `isAdult`에 explicit null이 들어와도 primitive 기본값 `0`/`false`로 보정될 수 있으며, 특히 `isAdult: null`은 정상 생성으로 이어질 수 있다. | `P4-R4`, `P4-R4-GATE` | v2 생성 경계에서 explicit null을 400으로 거부하고 필드 생략 기본값은 유지했다. | | `REV-043` | High | 처리 완료 | 커뮤니티 생성 필수 boolean 누락·null과 `price: null`이 false·0으로 보정되고, 수정 `isFixed: null`은 생략과 구분되지 않아 유효 요청처럼 처리될 수 있다. | `P5-R3`, `P5-R3-GATE` | v2 create/update 경계에서 required/non-null primitive를 검증하고 생략 의미와 no-side-effect를 고정했다. | | `REV-044` | High | 처리 완료 | 캐릭터 등록 화면에서 사용할 신규 v2 원작 검색 operation이 없다. | `P2-R9`, `P2-R9-GATE` | 기존 무페이징 검색과 `OriginalWorkResponse`를 재사용하는 관리자 endpoint를 구현했다. | | `REV-045` | High | 처리 완료 | target AI 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 operation이 없다. | `P3-R13`, `P3-R13-GATE` | actor·owner·parent 검증과 row-only soft delete를 포함한 5개 operation을 구현했다. | | `REV-046` | Medium | 처리 완료 | 시리즈 등록 화면에서 사용할 신규 v2 활성 장르 목록 operation이 없다. | `P4-R5`, `P4-R5-GATE` | 기존 활성/`orders` 조회와 장르 response를 재사용하는 관리자 endpoint를 구현했다. | | `REV-047` | Medium | 처리 완료 | 시리즈 상세 `data`가 목록 item과 다른 레거시 상세 schema를 반환한다. | `P4-R6`, `P4-R6-GATE` | 상세를 목록 item의 단일 객체와 동일한 11개 필드·타입으로 정합화했다. | | `REV-048` | High | 처리 완료 | target AI 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 operation이 없다. | `P5-R5`, `P5-R5-GATE` | actor·owner·parent 검증과 row-only soft delete를 포함한 5개 operation을 구현했다. | | `REV-049` | High | 처리 완료 | target AI 채널에서 팬이 작성한 FanTalk root를 삭제할 관리자 operation이 없다. | `P6-R2`, `P6-R2-GATE` | 팬 작성 root만 soft delete하고 creator reply row를 보존하는 DELETE를 구현했다. | | `REV-050` | High | 처리 완료 | 오디오 생성은 `timezone` body와 로컬 날짜를 받고 상세·댓글·답글 GET은 `timezone` query 및 locale/legacy 날짜 문자열을 사용해 최신 UTC 계약과 다르다. | `P3-R14`, `P3-R14-GATE` | v2 생성 DTO·내부 UTC instant 경계와 세 GET의 UTC mapping을 최소 구현하고 legacy/public 회귀를 고정했다. | | `REV-051` | High | 처리 완료 | 커뮤니티 댓글·답글 GET이 `timezone` query와 timezone별 `date` 표시 문자열을 사용해 최신 UTC 계약과 다르다. | `P5-R6`, `P5-R6-GATE` | v2 GET에서 timezone을 제거하고 기존 `date` 필드를 UTC로 mapping하며 legacy/public 회귀를 고정했다. | | `REV-052` | High | 처리 완료 | 오디오 댓글·답글 목록의 `page`, `size`가 OpenAPI에서는 optional 기본값 0/20이지만 controller와 facade는 둘 다 명시한 요청만 허용한다. | `P3-R15`, `P3-R15-GATE` | 두 GET의 전체·부분 생략 기본값을 복구하고 미지 query·범위 오류·UTC/ownership 회귀를 고정했다. | | `REV-053` | High | 처리 완료 | 커뮤니티 댓글 작성·수정 mapping이 `application/json`을 강제하지 않아 OpenAPI의 JSON-only request와 미지원 media type 415 계약을 보장하지 못한다. | `P5-R7`, `P5-R7-GATE` | POST·PUT에 JSON `consumes`를 추가하고 415 header/envelope/no-side-effect를 actual endpoint로 고정했다. | | `REV-054` | High | 처리 완료 | FanTalk 답변 작성 mapping이 `application/json`을 강제하지 않아 OpenAPI의 JSON-only request와 미지원 media type 415 계약을 보장하지 못한다. | `P6-R3`, `P6-R3-GATE` | reply POST에 JSON `consumes`를 추가하고 415 header/envelope/no-side-effect와 정상 축약 응답을 회귀했다. | | `REV-055` | High | 처리 완료 | Character 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P2-R10`, `P2-R10-GATE` | controller part header에서 JSON 호환 여부를 확인해 POST·PUT의 415 `Accept`/envelope와 external/S3/DB/event no-side-effect를 고정했다. | | `REV-056` | High | 처리 완료 | AudioContent 생성·수정 multipart의 `request` part가 같은 이유로 `text/plain`도 수용하며 실제 수정 테스트가 이를 200으로 기대한다. | `P3-R16`, `P3-R16-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3·DB·event no-side-effect를 고정했다. | | `REV-057` | High | 처리 완료 | Series 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P4-R7`, `P4-R7-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3/DB/event no-side-effect를 고정했다. | | `REV-058` | High | 처리 완료 | Community 게시글 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P5-R8`, `P5-R8-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3/DB no-side-effect를 고정했다. | | `REV-059` | High | 처리 완료 | 선택한 AI 캐릭터가 작성한 FanTalk 답변의 내용·활성 상태를 수정할 V2 관리자 operation이 없다. | `P6-R4`, `P6-R4-GATE` | 레거시 `PUT /explorer/profile/cheers` 계약을 유지하고 target AI·활성 root·direct reply로 한정한 PUT을 구현했다. | | `REV-060` | Medium | 처리 완료 | Character 생성·수정 multipart schema는 `additionalProperties: false`지만 controller가 전체 part 이름을 검증하지 않아 `image`, `request` 외 part를 무시하고 mutation을 진행한다. | `P2-R11`, `P2-R11-GATE` | operation별 허용 part 집합을 검사하고 미정의 part 400/no-side-effect를 고정했다. | | `REV-061` | Medium | 처리 완료 | AudioContent 생성·수정도 미정의 multipart part를 무시한다. 수정은 `audioFile`, `contentFile`만 명시적으로 거부해 다른 이름의 추가 part가 정상 mutation을 통과한다. | `P3-R17`, `P3-R17-GATE` | 생성·수정의 서로 다른 허용 part 집합을 검사하고 기존 수정 파일 교체 거부를 같은 경계로 통합했다. | | `REV-062` | Medium | 처리 완료 | Series 생성·수정 multipart schema는 `image`, `request` 외 part를 금지하지만 controller는 추가 part를 검사하지 않는다. | `P4-R8`, `P4-R8-GATE` | operation별 허용 part 집합과 미정의 part 400/no-side-effect 회귀를 추가했다. | | `REV-063` | Medium | 처리 완료 | Community 생성·수정은 허용 part 집합이 다른데 전체 part 이름을 검사하지 않아 특히 수정의 `audioFile` 등 미정의 part를 무시하고 mutation을 진행한다. | `P5-R9`, `P5-R9-GATE` | 생성·수정의 허용 part 집합을 각각 검사하고 미정의 part 400/no-side-effect를 고정했다. | | `REV-064` | Low | 처리 완료 | OpenAPI와 controller는 37개 구현 완료인데 `plan-task.md` Endpoint Contract Summary와 `api-contract.md`는 36개 구현·FanTalk 답변 수정 1개 planned 상태로 남아 있다. | `P7-R9`, `P7-R9-GATE` | 두 규범 설명의 집계·endpoint 상태·client 설명을 37개 구현 완료로 동기화했다. | | `REV-065` | Medium | 처리 완료 | Character 생성·수정의 미정의 multipart 검사가 `fileMap.keys`만 보므로 filename 없는 일반 form-field part가 `{image, request}` allow-list를 우회한다. | `P2-R12`, `P2-R12-GATE` | 파일 map과 servlet 전체 part 이름을 모두 검사하고 일반 form-field 미정의 part 400/no-side-effect를 고정했다. | | `REV-066` | Medium | 처리 완료 | AudioContent 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P3-R18`, `P3-R18-GATE` | operation별 파일 map과 servlet 전체 part 이름 집합, 일반 form-field 회귀를 추가했다. | | `REV-067` | Medium | 처리 완료 | Series 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P4-R9`, `P4-R9-GATE` | 파일 map과 servlet 전체 part 이름을 `{image, request}`와 대조하고 400/no-side-effect를 고정했다. | | `REV-068` | Medium | 처리 완료 | Series 생성·수정의 active genre 검사가 양수에만 존재 여부를 확인해 `genreId <= 0`을 legacy non-null repository 경계로 전달하고 예상 밖 500을 유발할 수 있다. | `P4-R10`, `P4-R10-GATE` | 0 이하 또는 비활성·미존재 장르를 legacy 호출 전에 공통 400으로 거부했다. | | `REV-069` | Medium | 처리 완료 | Community 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P5-R10`, `P5-R10-GATE` | operation별 파일 map과 servlet 전체 part 이름 집합, 일반 form-field 회귀를 추가했다. | | `REV-070` | Low | 처리 완료 | FanTalk 삭제에서 OpenAPI·구현·테스트는 동일 target의 이미 비활성인 팬 root를 성공 no-op으로 처리하지만 PRD는 400으로 기술하고 계약 설명도 상충한다. | `P6-R5`, `P6-R5-GATE` | OpenAPI/runtime의 no-op 계약에 맞춰 PRD와 `api-contract.md` 설명만 동기화했다. | | `REV-071` | Low | 처리 완료 | 완료 Gate가 존재하는 `REV-060`~`REV-064` 일부가 finding 표에서 여전히 `확정`으로 남아 상단 구현 완료 상태와 모순된다. | `P7-R10`, `P7-R10-GATE` | 후속 Gate 완료 뒤 기존·신규 finding 상태와 Phase 집계를 실제 결과에 맞췄다. | | `REV-072` | High | 처리 완료 | v2 오디오 생성은 parsed request overload를 호출하지만 기존 preview 시간 쌍·형식·최소 15초 검증은 문자열 request overload에만 있어 잘못된 입력이 DB 저장과 S3 upload·event publish 경계로 진행된다. | `P3-R19`, `P3-R19-GATE`, `P7-R11`, `P7-R11-GATE` | 기존 `validatePreviewTime` 호출을 두 생성 경로가 공유하는 overload로 이동하고 actual endpoint·legacy 회귀와 no-side-effect를 고정한 뒤 37개 operation 통합 재판정을 완료했다. | ## 검증 기록 - Phase 1~7 9차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재 controller/facade/service/repository/test 소스를 사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다. `AiCharacterAdminAudioContentFacade.create`는 parsed request overload를 호출하지만 기존 creator 생성의 `validatePreviewTime`은 문자열 request overload에서만 호출되는 `REV-072`를 확정했다. 한쪽만 있는 preview, 잘못된 형식, 15초 미만 입력을 거부하는 v2 회귀 테스트는 없고 정상 값만 존재했다. Phase 1·2·4·5·6에는 신규 finding이 없으며 Phase 7 완료 판정은 Phase 3 보완 뒤 재수행한다. OpenAPI JSON 문법, operation 37개·고유 operationId 37개·`implemented` 37개를 `jq`로 확인했다. production/test/PRD/OpenAPI는 변경하지 않았고 `Task 3.29`, `Task 7.13`과 Phase별 리뷰 기록만 추가했다. - Phase 1~7 8차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재 controller/facade/repository/test 소스를 사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다. Character, AudioContent, Series, Community controller의 미정의 multipart 검사가 `fileMap.keys`에 한정되어 filename 없는 일반 form-field part를 확인하지 못하는 `REV-065`~`REV-067`, `REV-069`를 확정했다. Series의 `genreId <= 0`이 active genre 사전 검증을 우회하는 `REV-068`, FanTalk 비활성 root 삭제 설명이 OpenAPI·runtime과 상충하는 `REV-070`, 완료 Gate와 finding 상태가 어긋나는 `REV-071`도 확정했다. Phase 1은 신규 finding이 없다. 테스트 명령은 실행하지 않았고, `rg`·`jq`·`nl`과 `javap`만 사용했다. 로컬 Spring Web 5.3.29 bytecode에서 filename 없는 part가 file map이 아니라 parameter 이름 집합으로 분류됨을, 기존 compile output에서는 legacy genre repository의 Kotlin non-null check를 정적으로 확인했다. production/test/OpenAPI는 변경하지 않았으며 신규 Task와 Phase별 리뷰 문서만 추가했다. OpenAPI는 operation 37개·고유 operationId 37개·`implemented` 37개였고, code fence 짝과 신규 미완료 Task/Gate를 확인했다. `./gradlew tasks --all`은 컴파일·테스트 없이 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공했다. - Phase 1~7 7차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재 controller/facade/test를 사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다. OpenAPI의 8개 multipart schema는 모두 `additionalProperties: false`이고 operation별 허용 part가 명시돼 있지만 네 domain controller는 전체 part 이름 집합을 검증하지 않아 `REV-060`~`REV-063`을 확정했다. OpenAPI 37개 `implemented`와 controller 37개 mapping에 비해 계획 요약·계약 설명이 36개 구현/1개 planned인 `REV-064`도 확정했다. Phase 1·6은 신규 finding이 없으며, production/test/OpenAPI는 변경하지 않았다. 정적 assertion은 37개 operationId·37개 `implemented`·내부 `$ref`, 8개 multipart `additionalProperties: false`, controller mapping 37개를 확인했고, 신규 Goal/Task ID는 각각 1개로 유일했다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`와 trailing whitespace 검사는 출력 없이 성공했으며 문서 code fence도 모두 짝이 맞았다. - `P7-R8` / `P7-R8-GATE` 검증(2026-07-29): `REV-052`~`REV-059` 처리 뒤 OpenAPI 37개 operationId/고유 operationId/status `implemented` 37개와 controller mapping 37개를 대조했다. 선행 Gate 미체크와 `REV-052`~`REV-059` 미처리 항목은 없었고, dependency·DDL 변경 파일도 없었다. 영향 14개 operation과 공통 error/authorization focused 회귀는 `BUILD SUCCESSFUL in 1m 26s`, 전체 `./gradlew test`는 `BUILD SUCCESSFUL in 8m 8s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다. Phase 7 Gate 기준에 따라 문서 상태를 `구현 완료`로 갱신했다. - `P6-R4` / `P6-R4-GATE` 검증(2026-07-29): `AiCharacterAdminFanTalkReplyUpdateTest`와 `AiCharacterAdminFanTalkReplyUpdateContractTest`에 답변 수정 success/no-op/재활성화, target/root/direct-reply ownership, malformed/unknown-field/415 `Accept`/DB·event no-side-effect 회귀를 추가했다. RED는 신규 focused 명령에서 15건이 미구현 route 404로 `BUILD FAILED in 49s`였다. PUT route, strict request DTO, active root와 target AI writer/creator direct reply를 검증하는 repository query, non-null field만 반영하는 facade를 추가한 뒤 분리 focused 재실행은 `BUILD SUCCESSFUL in 42s`, FanTalk/common/legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`였다. OpenAPI status 집계는 37개 operation 모두 `implemented`, `ktlintCheck`는 import/format 수정 뒤 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 신규 dependency·DDL과 legacy/public API 변경은 없다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply update와 FanTalk/common/legacy 영향 범위에 포함되므로 생략했다. - `P6-R3` / `P6-R3-GATE` 검증(2026-07-29): `AiCharacterAdminFanTalkReplyContractTest`에 KO/EN/JA `text/plain` reply POST 415 envelope, `Accept`, reply insert/event no-side-effect 회귀를 추가했다. RED는 focused 명령에서 3개 invocation이 `status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 33s`였다. reply POST mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가한 뒤 같은 focused 재실행은 `BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 47s`였다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply POST media type 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 생략했다. - `P5-R8` / `P5-R8-GATE` 검증(2026-07-29): `AiCharacterAdminCommunityPostCreateTest`와 `AiCharacterAdminCommunityPostUpdateTest`에 KO/EN/JA `text/plain` 및 Content-Type 누락 `request` part 415 envelope, `Accept`, DB/S3 no-side-effect 회귀를 추가했다. RED는 focused 명령에서 12개 invocation이 `status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 56s`였다. POST·PUT controller 경계에 JSON 호환 part media type 검사를 추가한 뒤 같은 focused 재실행은 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 55s`였다. 전체 `./gradlew test`는 변경 범위가 v2 community 게시글 multipart request part 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 생략했다. - `P5-R7` / `P5-R7-GATE` 검증(2026-07-29): `AiCharacterAdminCommunityPostCommentTest`에 KO/EN/JA `text/plain` POST·PUT 415 envelope/`Accept`/작성 insert·수정 row·event no-side-effect 회귀를 추가했다. RED는 같은 focused 명령에서 3개 locale invocation이 `status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 30s`였다. 두 댓글 mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가한 뒤 focused 재실행은 `BUILD SUCCESSFUL in 30s`, community/common 영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 57s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 12s`였다. `git diff --check`는 출력 없음을 확인했다. 전체 `./gradlew test`는 변경 범위가 v2 community 댓글 JSON media type 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 생략했다. - FanTalk 답변 수정 계약·계획 문서 검증(2026-07-29): production/test 파일은 변경하지 않고 PRD, OpenAPI 2.3.0, 계약 설명, plan과 Phase 6·7 리뷰에 `REV-059`, `Task 6.9` / `P6-R4`·Gate를 추가했다. OpenAPI는 전체 37개 operation, 기존 `implemented` 36개와 신규 `planned` 1개로 구성하며 신규 PUT의 request는 optional/nullable `content`, `isActive`, 성공 `data`는 `CreatorChannelFanTalkResponse` 필드 형태다. JSON 문법·내부 `$ref`· operationId·200 response·상태 집계, Markdown 구조와 `git diff --check`를 정적으로 확인했다. `./gradlew tasks --all`은 최초 sandbox cache lock 권한 실패 후 허용된 재실행에서 `BUILD SUCCESSFUL in 742ms`였다. 사용자 지시에 따라 컴파일·테스트·lint는 실행하지 않았다. - Phase 1~7 6차 정적 리뷰 검증(2026-07-29): 현재 working tree를 기준으로 PRD·plan·OpenAPI와 Phase 1~7 production/test 코드를 대조했다. `jq`로 OpenAPI JSON 문법, operation 36개, 고유 operationId 36개를 확인했고 controller mapping도 36개다. OpenAPI 공통 `Page`/`Size`는 optional 기본값 0/20이나 오디오 댓글·답글 GET 두 곳은 기본값이 없고 facade가 query 이름의 정확한 집합을 요구한다. OpenAPI가 `application/json` request와 415를 선언한 mutation 중 커뮤니티 댓글 POST·PUT과 FanTalk reply POST 세 곳은 mapping에 JSON `consumes`가 없다. OpenAPI와 계약 설명이 Character·AudioContent·Series·Community 생성·수정 8개 multipart의 `request` part를 `application/json`으로 고정하지만 모든 controller는 `@RequestPart String`으로 받아 part media type을 강제하지 않는다. AudioContent 수정 테스트에는 `text/plain` request part를 보내 200을 기대하는 실제 증거도 존재한다. 변경 파일 기준 신규 dependency·migration·DDL 경로는 없었다. 문서 정적 검증만 수행했으며 사용자 지시에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. - UTC 날짜 계약 문서 반영 검증(2026-07-29): production/test 파일은 변경하지 않고 PRD, OpenAPI 2.2.0, 계약 설명, plan과 Phase 3·5·7 리뷰만 갱신했다. `jq`로 JSON 문법, 내부 `$ref` 누락 0건, operationId 중복 0건, 200 response 누락 0건, 전체 36개 operation과 `implemented` 30개/`alignment-required` 6개/`planned` 0개, 정확한 영향 operation 6개를 확인했다. query `timezone` parameter와 `components.parameters.Timezone`, 생성 `AudioContentCreateRequest.timezone`은 모두 0건이며 생성·상세 `releaseDate`와 오디오·커뮤니티 댓글 `date`는 `date-time` + `Z$`로 확인했다. plan Task 집계는 Phase 1~7 순서대로 7/7, 15/15, 23/24, 12/12, 11/12, 7/7, 8/9이며 미완료 Task는 `Task 3.24`, `Task 5.12`, `Task 7.9` 세 건이다. `git diff --check`는 출력이 없었고 `./gradlew tasks --all`은 첫 sandbox 실행에서 Gradle cache lock 권한으로 실패한 뒤 허용된 재실행에서 `BUILD SUCCESSFUL in 896ms`였다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. - UTC 날짜 계약 구현 완료 검증(2026-07-29): `P3-R14`, `P5-R6`, `P7-R7`을 완료했다. 오디오 focused 재실행은 `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행은 `BUILD SUCCESSFUL in 4m 33s`였다. OpenAPI 집계는 36개 operation, 36개 `implemented`, 0개 `alignment-required`, query `timezone` parameter 0개, `components.parameters.Timezone` 0개였고, controller mapping은 36개였다. `P3-R14-GATE`와 `P5-R6-GATE`의 영향 범위 회귀와 `ktlintCheck`, `git diff --check` 성공 기록을 대조해 Phase 7 상태를 `구현 완료`로 동기화했다. - `P6-R2` 검증(2026-07-29): 팬 작성 FanTalk root DELETE endpoint를 actual endpoint로 구현했다. RED는 `AiCharacterAdminFanTalkDeleteTest` 신규 6건이 미구현 route로 실패했다. 구현 후 누락 ID 계약을 포함한 focused 재실행은 `BUILD SUCCESSFUL in 29s`였다. Oracle 리뷰에서 DELETE endpoint의 ADMIN 이중 인가 matrix 누락을 지적받아 `AiCharacterAdminAuthorizationTest`에 FanTalk delete request를 추가했고, authorization 단독 재실행은 `BUILD SUCCESSFUL in 29s`, FanTalk/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 58s`였다. 최종 `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 14s`, `git diff --check`는 출력 없음이었다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk 삭제 endpoint와 FanTalk/common 회귀에 포함되어 생략했다. - `P5-R5` / `P5-R5-GATE` 검증(2026-07-29): 커뮤니티 댓글 CRUD 5개 operation을 actual endpoint로 구현했다. RED는 `AiCharacterAdminCommunityPostCommentTest` 신규 7건이 미구현 route로 실패했다. 구현 후 focused 재실행은 `BUILD SUCCESSFUL in 2m`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 26s`였다. `./gradlew ktlintCheck`는 facade import 순서 1건 실패 후 정리해 `BUILD SUCCESSFUL in 22s`였고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community 댓글 endpoint와 community/common 회귀에 포함되어 생략했다. - `P4-R6` / `P4-R6-GATE` 검증(2026-07-29): 시리즈 상세 `data`를 목록 item과 동일한 11개 field/type으로 정합화했다. RED는 `AiCharacterAdminSeriesQueryTest`의 상세 schema 테스트가 기존 레거시 상세 필드와 달라 실패했다. 구현 후 focused 단일 테스트는 `BUILD SUCCESSFUL in 43s`, query/contract focused 회귀는 `BUILD SUCCESSFUL in 38s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 19s`였다. `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 23s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 series 상세 response mapper와 series/common 회귀에 포함되어 생략했다. - `P4-R5` / `P4-R5-GATE` 검증(2026-07-29): 시리즈 등록용 장르 목록 endpoint를 actual endpoint로 구현했다. RED는 `AiCharacterAdminSeriesGenreTest` 신규 2건이 미구현 route로 실패했다. 구현 후 focused 재실행은 `BUILD SUCCESSFUL in 1m 24s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 38s`였다. `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 29s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 series reference endpoint와 series/common 회귀에 포함되어 생략했다. - `P3-R13` / `P3-R13-GATE` 검증(2026-07-29): 오디오 콘텐츠 댓글 CRUD 5개 operation을 actual endpoint로 구현했다. RED는 `AiCharacterAdminAudioContentCommentTest` 신규 7건이 미구현 route의 404/405로 실패했다. 구현 후 focused 재실행은 `BUILD SUCCESSFUL in 3m 16s`, content/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 3m 47s`였다. `./gradlew ktlintCheck`는 신규 테스트 import 순서 1건 실패 후 정리해 `BUILD SUCCESSFUL in 32s`였고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 content 댓글 endpoint와 content/common 회귀에 포함되어 생략했다. - 후속 기능 문서 보완 검증(2026-07-29): 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다. `jq empty`로 OpenAPI JSON 문법을 확인했고 operation/status assertion은 전체 36개, `implemented` 22개, `planned` 13개, `alignment-required` 1개와 고유 operationId 36개를 확인해 `true`였다. 내부 `$ref` 누락은 0건이고 시리즈 상세 `data`는 `SeriesListItem`을 참조하며 구 `SeriesDetailResponse` schema가 없음을 확인했다. Phase별 Task 집계는 1~7 순서로 7/15/23/12/11/7/8이며 상단 완료/전체 수와 일치했고, 계약 표 집계도 22/13/1과 일치했다. 문서 경로의 trailing whitespace 검사와 추적 문서 `git diff --check`는 출력이 없었다. - `P5-R4` / `P5-R4-GATE` 검증(2026-07-29): Community 목록의 `timezone` query 제거와 `totalCount/page/size/hasNext/items` wrapper 계약을 actual endpoint로 고정했다. RED는 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest'`에서 신규 3건이 400/직접 배열 응답 차이로 `BUILD FAILED`였다. controller/facade의 `timezone` parameter와 검증을 제거하고, repository에 active owner count query를 추가해 wrapper를 구성한 뒤 같은 query focused는 `BUILD SUCCESSFUL in 1m 28s`였다. 최종 focused query+contract는 `BUILD SUCCESSFUL in 37s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 6s`, OpenAPI jq assertion은 `true`, `./gradlew ktlintCheck`는 들여쓰기 1건 수정 후 `BUILD SUCCESSFUL in 12s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community 목록과 공통 authorization/error 회귀에 포함되어 생략했다. - `P5-R3` / `P5-R3-GATE` 검증(2026-07-29): 커뮤니티 생성 `isCommentAvailable`/`isAdult` 누락·null, `price:null`, 수정 `isFixed:null` actual endpoint RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest --tests "*shouldRejectMissingOrNullPrimitiveCreateFieldsWithoutSideEffects" --tests "*shouldRejectNullIsFixedUpdateWithoutSideEffects"`에서 신규 6건이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 community strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가하고 `isFixed:null` explicit null guard를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 41s`였다. 리뷰 보완으로 `price` 생략 기본값 0과 `isFixed` 생략 시 고정 상태 보존 assertion을 추가한 뒤 create/update focused는 `BUILD SUCCESSFUL in 34s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 13s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community request reader와 community create/update tests에 한정되고 community/common 회귀가 영향 범위를 포함하므로 생략했다. - `P4-R4` / `P4-R4-GATE` 검증(2026-07-29): 시리즈 생성 `genreId:null`, `isAdult:null` actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests "*shouldRejectNullPrimitiveCreateFieldsBeforeSideEffects"`에서 신규 2개 invocation이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 series strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`는 `BUILD SUCCESSFUL`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 12s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 20s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 series request reader와 series mutation test에 한정되고 series/common 회귀가 영향 범위를 포함하므로 생략했다. - `P2-R8` / `P2-R8-GATE` 검증(2026-07-29): 관계 `importance` 누락·null actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests "*shouldRejectMissingRelationshipImportanceBeforeSideEffects" --tests "*shouldRejectNullRelationshipImportanceBeforeSideEffects"`에서 신규 2건이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 character strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 48s`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`는 `BUILD SUCCESSFUL in 53s`, `./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 38s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 22s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 character request reader와 character mutation test에 한정되고 character/common 회귀가 영향 범위를 포함하므로 생략했다. - `P3-R12` / `P3-R12-GATE` 검증(2026-07-29): 오디오 생성 `price` 누락·null과 primitive field explicit null actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests "*shouldRejectMissingOrNullPriceBeforeUpload" --tests "*shouldRejectNullPrimitiveFieldsBeforeUpload"`에서 8개 invocation이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다. v2 content strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`와 `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 42s`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`는 `BUILD SUCCESSFUL in 36s`, `./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 1m 28s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 18s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 content create request reader와 content create test에 한정되고 content/common 회귀가 영향 범위를 포함하므로 생략했다. - Community 목록 계약·계획 동기화(2026-07-29): 사용자 확정에 따라 `timezone` query를 제거하고 `totalCount/page/size/hasNext/items` schema와 `Task 5.10` / `P5-R4`, `P5-R4-GATE`를 문서에 반영했다. 사용자 요청 범위가 계획 보강이므로 Gradle, 컴파일, 테스트는 실행하지 않았다. `jq empty`, 내부 `$ref` 해석, 23개 operation·고유 operationId를 확인했고 현재 구현 상태는 `implemented` 22개, `implemented-contract-alignment-required` 1개(Community GET)였다. timezone parameter 부재와 response required 필드 5개를 assertion으로 확인했고 Task/의존 순서, Markdown code fence 균형을 점검했다. `git diff --check`는 출력이 없었다. - Phase 1~7 5차 정적 리뷰(2026-07-29): multipart JSON DTO의 primitive required/nullability를 OpenAPI와 production strict reader, 로컬 Jackson Kotlin/databind 2.13.5 source까지 대조해 `REV-040`~`REV-043` 4건을 확정했다. Phase 1·6은 신규 finding 없음, Phase 7은 후속 통합 재판정 요청으로 판정했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. `jq`로 OpenAPI JSON과 23개 operation/status를 확인하고, controller mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개를 확인했다. 신규 Task 5개와 Gate 연결, 문서 code fence 균형을 확인했고 `git diff --check`는 출력이 없었다. - `P7-R4` 문서 정합화 검증(2026-07-29): 완료 증거가 존재하는 네 Task 헤더와 상단 상태표를 완료로 동기화하고, Phase 4 콘텐츠 해제 설명을 `DELETE /series/{seriesId}/contents/{contentId}`와 request body 없음으로 정정했다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 767ms`, OpenAPI 23개 operation/status `jq` assertion은 `true`, 미완료 Task header `rg`와 `git diff --check`는 출력이 없었다. controller mapping `rg`는 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개를 확인했다. production/test/OpenAPI 변경은 없었다. - Phase 1~7 4차 정적 리뷰(2026-07-29): PRD, plan-task, OpenAPI, production/test를 정적 대조해 기능상 신규 finding은 없었고 문서 정합성 `REV-038`~`REV-039`를 확정했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. `jq empty`와 23개 operation·고유 operationId·`implemented`·200 response assertion은 성공했고 controller mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2로 23개였다. `git diff --check`는 출력이 없었고 `build.gradle.kts`, migration/DDL 경로 변경도 없었다. - `P2-R7`~`P7-R3-GATE` 후속 보완 검증(2026-07-29): `REV-034`~`REV-037`을 각각 RED로 재현한 뒤 최소 production 수정으로 GREEN을 확인했다. focused 명령은 character mutation, audio query, series mutation 순서로 모두 최종 `BUILD SUCCESSFUL`이었다. 통합 검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`는 `BUILD SUCCESSFUL in 2m 25s`, `./gradlew test`는 `BUILD SUCCESSFUL in 6m 54s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 18s`였다. OpenAPI 23개 operation/status `jq` assertion은 `true`, controller mapping count는 23, `git diff --check`는 출력이 없었고 신규 dependency/DDL 파일 변경은 없었다. - Phase 1~7 3차 정적 리뷰(2026-07-28): empty multipart, optional boolean, 미래 예약일 표시를 production/legacy/OpenAPI/test까지 역추적해 `REV-034`~`REV-037` 4건을 확정했다. Phase 1·5·6은 신규 finding 없음, Phase 7은 23개 operation/mapping/status 유지와 통합 재판정 요청으로 판정했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았으며 `rg`, `sed`, `jq`, `git diff` 기반 정적 대조만 수행했다. OpenAPI operation/status assertion은 `true`, controller mapping 집계는 23개였고, 변경 문서의 code fence 균형과 `git diff --check`는 출력 없이 통과했다. - Phase 1~7 후속 정적 리뷰(2026-07-28): 정상·순차 경로 밖의 의미 입력, soft-delete link, required multipart, fixed-count concurrency를 production/legacy/test까지 역추적해 `REV-030`~`REV-033` 4건을 확정했다. Phase 1·2·6은 신규 finding 없음, Phase 7은 23개 operation/mapping/status 유지와 통합 재판정 요청으로 판정했다. 사용자가 현재 compile/test 통과 사실을 제공하고 재실행하지 말 것을 요청했으므로 `./gradlew test`와 compile 명령은 실행하지 않았다. 문서 가이드 확인용 `./gradlew tasks --all`만 실행해 `BUILD SUCCESSFUL`을 확인했고, `jq` 집계로 OpenAPI 23개 operation/status `implemented`, `rg` 집계로 관리자 controller mapping 23개를 확인했다. 변경 문서의 `git diff --check`, trailing whitespace와 code fence 균형 점검도 출력·오류 없이 종료했다. - Phase 1~7 정적 리뷰(2026-07-28): PRD, plan-task, OpenAPI 23개 operation과 현재 controller/facade/repository/DTO/test를 정적 대조해 `REV-021`~`REV-029` 9건을 확정하고 Phase별 신규 Task/Gate 및 review 문서로 추적했다. `rg`, `jq`, `git diff` 계열의 읽기·정적 점검만 사용했으며, 사용자가 현재 compile/test 통과 사실을 제공하고 재실행을 금지했으므로 `./gradlew`, 컴파일, 테스트는 실행하지 않았다. - 실행 계획 동기화 검증(2026-07-28): OpenAPI 23개 고유 operation과 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 분류를 현재 Endpoint Contract Summary 및 `P23-CONTRACT-2`~Phase 7과 대조했다. Goal ID 중복은 없고 PRD Open Questions는 `없음`이며 Task 3.17·3.18에 추가한 production/test/characterization 파일의 존재를 확인했다. - 실행 계획 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 922ms`였다. `git diff --check`, `git diff --cached --check`는 출력이 없었다. - `P23-CONTRACT-1` OpenAPI 검증(2026-07-28): `jq empty`, 23개 고유 operation과 `implemented-contract-alignment-required` 9개/`planned` 14개 assertion, 내부 `$ref` 해석을 실행해 모두 성공했다. `npx --yes @redocly/cli lint --skip-rule info-license ...`는 `valid`, OpenAPI Generator `validate`는 `No validation issues detected`였다. - `P23-CONTRACT-1` 클라이언트 검증(2026-07-28): OpenAPI Generator 7.24.0 `typescript-fetch` 생성을 완료하고, 생성된 23개 Raw operation(Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2)을 확인했다. `tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM .../index.ts`는 TypeScript 5.9.3에서 성공했다. - `P23-CONTRACT-1` 문서·리뷰 Gate(2026-07-28): `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였고, 독립 레거시 DTO/controller/service 대조 및 생성물 재검증 결과 Critical 0, Important 0이었다. - 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`였다. - `P6-R1` RED/GREEN 검증(2026-07-28): `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkQueryTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest"` 최초 실행은 pagination 400과 reply unknown-field 허용으로 4건 실패했다. 공개 v2 query policy 재사용과 reply strict reader 적용 후 동일 명령 재실행은 `BUILD SUCCESSFUL in 3m 14s`였다. - `P6-R1-GATE` fresh 검증(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 36s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 35s`였고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 FanTalk facade/controller/test와 문서에 한정되고 FanTalk/common 회귀가 영향 범위를 포함하므로 생략했다. - `P7-R1` 문서/metadata 검증(2026-07-28): OpenAPI operation/status `jq` assertion은 `true`, 신규 prefix controller method mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 = 23개였다. OpenAPI Generator validate는 `No validation issues detected`, TypeScript client 생성은 성공했고 `npx --yes tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM /tmp/ai-character-admin-typescript-client/index.ts`는 출력 없이 exit 0이었다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다. - `P7-R1-GATE` 최종 검증(2026-07-28): OpenAPI 23개 operation과 23개 `implemented` assertion은 `true`, 신규 prefix controller method mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 = 23개였고, 미처리 `확정` review finding 검색은 출력이 없었다. OpenAPI Generator validate는 `No validation issues detected`, TypeScript client 생성과 compile은 exit 0, `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다. - 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`는 실행하지 않았다. - `P4-T6` focused 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContractTest --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 41s`였고, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --rerun-tasks`는 순차 재실행에서 `BUILD SUCCESSFUL in 3m 3s`였다. 병렬 실행 중 authorization run 1회는 unrelated `DefaultHomeRecommendationQueryRepository` QueryDSL 참조 compile 오류로 실패했으나 같은 명령 순차 재실행으로 영향 범위를 확인했다. - `P4-T6` series 회귀와 lint/diff 검증(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 23s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 25s`였고, `git diff --check`는 출력이 없었다. - `P4-GATE` fresh 검증(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 21s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 24s`였고, `git diff --check`는 출력이 없었다. - `P5-T1` community baseline 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.LegacyCommunityPostCharacterizationTest --rerun-tasks`와 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`는 각각 `BUILD SUCCESSFUL in 2m 22s`였다. `./gradlew ktlintCheck --rerun-tasks`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`였고, `git diff --check`는 출력이 없었다. - 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 안에서 직렬 실행하도록 의존성 표를 갱신했다. - Task TDD 샘플 동기화(2026-07-27): `docs/sample/sample-plan-task.md`에 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 작성 규칙과 read-only Task의 `TDD 예외 사유`·`대체 검증 방법` 형식을 추가하고 기존 샘플 Task 체크박스를 같은 형식으로 통일했다. - 문서 보완 검증(2026-07-27): `REV-001`~`REV-008` 추적 횟수, Phase 2·3 Task/Gate·직렬 Goal 순서, 중복 Goal ID와 미교체 placeholder를 `rg`로 확인했고 `git diff --check`, `git diff --cached --check`가 통과했다. `./gradlew tasks --all`은 exit code 0, `BUILD SUCCESSFUL in 660ms`를 확인했다. 문서 전용 변경이며 사용자가 명시적으로 제외했으므로 전체 `./gradlew test`는 실행하지 않았다. - 리뷰 문서 경로 보완(2026-07-27): 여러 리뷰를 범위별 파일로 누적할 수 있도록 공통 저장 경로를 `docs/[날짜]_구현할내용한글/reviews/`로 정하고, Phase 2·3 리뷰 산출물과 샘플·가이드 경로를 동기화했다. - 전체 회귀 실행 정책 보완(2026-07-27): focused/영향 범위 회귀를 우선하고 공통 경계·여러 Phase 영향 또는 targeted 결과만으로 영향 범위를 판단할 수 없는 경우에만 전체 회귀를 실행하도록 공통 가이드·샘플·`P7-T1`을 동기화했다. 생략 시 근거와 대체 검증 명령을 기록하도록 했다. - Goal 실행형 계획 보완(2026-07-27): `docs/sample/sample-plan-task.md`와 PRD Feature B~F를 대조해 Phase 2·3에는 기존 완료 이력을 보존한 review/세부 보완/Gate Goal을 추가하고, Phase 4~6은 독립 검토 가능한 기능 단위 Task와 Gate로 분할했다. Phase 7에는 targeted·조건부 전체 회귀, 계약·변경 범위와 최종 판정 Goal을 분리했다. - 샘플 참조 가이드 보완(2026-07-27): `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`에 `docs/sample/{sample-prd.md,sample-plan-task.md,sample-review.md}`의 용도, Goal 완료·Gate·리뷰 누적 규칙을 반영했다. - 문서 검증(2026-07-27): Goal/Task/Gate heading과 ID, placeholder·구 샘플 경로, 중복 Goal ID를 `rg`로 확인했고 중복·미교체 placeholder·구 샘플 경로 참조가 없음을 확인했다. `git diff --check`가 통과했다. - 명령 유효성 검증(2026-07-27): sandbox 실행은 Gradle wrapper lock 권한으로 실패해 승인 범위에서 `./gradlew tasks --all`을 재실행했고 `BUILD SUCCESSFUL`을 확인했다. - 계획 작성 단계: 코드 변경 없음. - 문서 규칙 확인: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`, `docs/sample/sample-prd.md`를 확인하고 `docs/20260724_AI캐릭터_관리자_API/{prd.md,plan-task.md}` 형식으로 작성했다. - 금지어 확인: 계획 문서 금지어 검색 명령 실행 결과 없음. - Phase 항목 확인: `rg -n "^### Phase|#### 목표|#### 범위와 비범위|#### 선행 Phase|#### API endpoint|#### entity, repository, service 변경|#### DB migration|#### transaction과 concurrency|#### 보안 및 개인정보|#### acceptance criteria|#### targeted test|#### 전체 회귀 테스트 영향|#### rollback 전략|#### 권장 commit 경계|Final Integration" "docs/20260724_AI캐릭터_관리자_API/plan-task.md"`로 Phase 1~7 전체에 필수 항목이 있음을 확인했다. - 명령 유효성 확인: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`. - Markdown diagnostics: `.md` 확장자용 LSP 서버가 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가. - Endpoint Contract 보강: frontend HTTP 호출 기준으로 query parameter, request body, multipart form fields, response `data` JSON 예시를 추가했다. - Endpoint Contract 보강 검증: 계획 문서 금지어 검색 결과 없음. `.md` 확장자용 LSP 서버가 없어 diagnostics는 실행 불가. - 문서 동기화 보강: source spec 기준 `characterId` 예외, 시리즈 콘텐츠 검색, endpoint별 ADMIN 권한 테스트, parity RED 범위, 의존 방향 검증, PRD 가드레일, pagination 경계값, RED/GREEN/REFACTOR task 규칙을 반영했다. - 문서 동기화 검증: `Read`로 `prd.md`와 `plan-task.md`의 반영 라인을 확인했다. `git status --short` 결과 `docs/20260724_AI캐릭터_관리자_API/`는 현재 untracked 디렉터리로 표시된다. `./gradlew tasks --all`은 이번 세션에서 120초, 300초 제한 모두 초과해 종료 결과를 확인하지 못했다. - 문서 동기화 보강(2차): 기존 legacy/creator-admin 구현을 먼저 통과하는 특성화 baseline과 신규 v2 RED를 분리했고, character/content/series/community/FanTalk 재사용·parity 경계에 반영했다. 시리즈 CRUD·연결·해제·조회·순서 behavior parity와 signed URL 만료 계산식·edge case 특성화를 PRD와 Task에 명시했다. - 문서 동기화 재검증(4번 제외): source spec acceptance criteria 25개 추적 검사 25/25, actor/scope/non-goal/architecture 가드레일 11/11을 확인했고 `git diff --check` 결과 문제가 없었다. plan 상단의 실행 skill 지침은 사용자 요청에 따라 변경·판정 범위에서 제외했다. - 기존 기록은 보존한다. 각 Task의 실행 명령과 결과 요약은 해당 Task 아래에 누적하고, phase/전체 회귀·전체 빌드·포맷·문서 범위 확인만 이 섹션에 누적한다. - Phase 1 RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` 실행 결과 `AiCharacterAdminTargetResolver`와 `ChatCharacterRepository.findByIdWithCreatorMember` 미구현으로 `compileTestKotlin` 실패를 확인했다. - Phase 1 GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` 실행 결과 `BUILD SUCCESSFUL`. - Phase 1 ADMIN 권한 RED/GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`에서 sample route 권한 실패를 확인한 뒤 `/api/v2/admin/ai-characters/**` ADMIN rule을 적용해 `BUILD SUCCESSFUL`을 확인했다. - Phase 1 targeted 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 `BUILD SUCCESSFUL`. - Phase 1 lint 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`. - Phase 1 LSP diagnostics: 현재 도구 목록에 LSP diagnostics tool이 노출되어 있지 않아 실행하지 못했고, 대신 Kotlin compile/test와 `ktlintCheck`로 대체 검증했다. - Phase 1 reviewer gate: 1차 리뷰에서 invalid target 4xx 미충족, production SecurityConfig 미검증, cross-owner fixture 부족을 지적받아 수정했고, 재리뷰 결과 남은 blocking finding 없음으로 승인받았다. - Phase 1 추가 리뷰 반영: mock 기반 resolver 테스트만으로 실제 repository/ownership 동작을 검증하지 못한다는 지적에 따라 `AiCharacterAdminTargetResolverIntegrationTest`를 추가했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest` 실행 결과 `BUILD SUCCESSFUL`. - Phase 1 no-side-effect 근거 보정: 현재 검증은 resolver 자체가 DB row를 변경하지 않는다는 통합 테스트와 resolver가 S3/외부 API/event 의존성을 갖지 않는다는 구조에 한정한다. Phase 2~6 write vertical slice의 S3, 외부 API, 이벤트 no-side-effect는 각 slice 테스트에서 별도로 검증한다. - Phase 1 후속 정책 반영 전 한계: 기존 ADMIN smoke는 JWT authority만 검증했고 현재 DB role/stale claim 및 신규 prefix 오류 envelope/i18n을 검증하지 않았다. 2026-07-24 후속 확정 정책은 Task 1.4~1.5에서 RED/GREEN으로 보완한다. - Phase 1 후속 정책 문서 갱신: JWT ADMIN + 현재 DB ADMIN 이중 인가, stale claim 403, 신규 prefix의 비2xx `ApiResponse.error`/KO·EN·JA 계약, legacy 오류 응답 불변 조건을 PRD와 계획에 반영하고 미완료 Task 1.4~1.5를 추가했다. - Phase 1 후속 정책 문서 자체 검토: 금지어/미확정 문구 검색 결과 없음, 요구사항 추적 검색으로 stale claim·오류 envelope·i18n· legacy fallback·Task 1.4~1.5 반영을 확인했고 `git diff --check` 결과 문제가 없었다. - Phase 1 후속 정책 명령 유효성: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`. - Task 1.4 RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest` 실행 결과 6개 중 stale ADMIN claim과 비`MemberAdapter` principal이 403 기대 대비 200으로 통과해 2개 실패함을 확인했다. - Task 1.4 GREEN: 신규 prefix의 단일 access 식에서 JWT ADMIN, `MemberAdapter` principal, 현재 DB ADMIN을 AND로 검증한 뒤 동일 테스트 실행 결과 `BUILD SUCCESSFUL`. - Task 1.4 reviewer gate: 별도 read-only 리뷰에서 스펙 준수와 코드 품질 모두 승인됐고 Critical/Important/Minor finding이 없음을 확인했다. - Task 1.5 최초 RED: legacy 오류 baseline 2개는 통과했고 신규 401/403/400/500 계약은 15개 중 13개가 status/content type/ message 불일치로 실패함을 확인했다. target resolver의 기존 `ResponseStatusException`을 신규 API 예외로 교체하는 테스트도 새 예외 미구현 상태의 `compileTestKotlin` 실패로 RED를 확인했다. - Task 1.5 확장 RED: handler 선택 전 오류와 filter 내부 장애까지 포함해 오류 계약 39개를 실행한 결과 신규 prefix의 404/405/ 415 및 예상하지 못한 JWT filter 오류 500에 해당하는 12개만 실패했고 legacy baseline은 통과했다. 미등록 message key fallback은 44개 중 해당 KO/EN/JA 3개 실패로 별도 RED를 확인했다. - Task 1.5 GREEN: URI prefix 기반 exception resolver, 낮은 우선순위 404 fallback mapping, prefix 전용 security handler, known 인증 실패 401/그 외 filter 예외 500 분리, 미등록 message key의 localized unknown fallback을 구현했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 44개 invocation 모두 통과해 `BUILD SUCCESSFUL`을 확인했다. - Task 1.5 기존 slice 회귀: production `SecurityConfig`를 import하는 기존 WebMvc test 5개와 신규 authorization/error contract test를 함께 실행한 결과 `BUILD SUCCESSFUL`을 확인했다. 신규 writer/security handler/exception resolver는 `SecurityConfig`의 명시적 bean으로 등록해 slice와 실제 application 구성을 동일하게 유지했다. - Task 1.5 reviewer gate: 최초 read-only 리뷰의 405/415 handler-less 경로, 비인증 filter 예외의 401 오분류, 기존 WebMvc slice 빈 누락, 미매핑 404 지적을 모두 보완했다. 재리뷰 결과 Critical/Important/Minor finding 없이 승인됐다. - Phase 1 후속 정책 최종 targeted/legacy 회귀: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 `BUILD SUCCESSFUL`(28초)을 확인했다. - Phase 1 후속 정책 전체 회귀: 최신 작업 트리에서 `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`(4분 22초)을 확인했다. - Phase 1 후속 정책 lint: 최신 작업 트리에서 `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`(17초)을 확인했다. - Phase 1 후속 정책 diff 무결성: `git diff --check` 통과, conflict marker 없음, build 설정·dependency·DDL 변경 없음을 확인했다. - Task 1.5 최종 리뷰 보완 RED/GREEN: JWT parse 사이 만료 `JwtException` 401과 인증 저장소 장애 500을 추가했을 때 50개 중 해당 6개 실패를 확인한 뒤 known credential failure만 401로 분류해 50개 모두 통과했다. 필수 request header 누락 400은 53개 중 해당 KO/EN/JA 3개 실패를 확인한 뒤 `ServletRequestBindingException`을 400으로 분류해 모두 통과했다. - Task 1.5 CORS RED/GREEN: 허용된 관리자 Origin의 404/405/415와 실제 `authorization,content-type` header를 요청하는 미매핑 prefix preflight를 추가했을 때 57개 중 해당 4개 실패를 확인했다. fallback mapping에 기존 전역 설정과 동일한 CORS 설정을 적용한 뒤 57개 모두 통과했다. - Phase 1 no-side-effect 검증 강화: `AiCharacterAdminTargetResolverIntegrationTest`에서 Hibernate statistics를 초기화한 뒤 invalid target resolver 호출과 flush 후 entity insert/update/delete가 각각 0건임을 직접 검증했다. resolver 단위·통합 및 오류 계약 테스트를 함께 실행한 결과 `BUILD SUCCESSFUL`(27초)을 확인했다. - Phase 1 최종 reviewer gate: 인증 예외 분류, request binding, fallback CORS, DB no-write, 문서/rollback을 독립 read-only로 재검토한 결과 Critical/Important/Minor finding이 없음을 확인했다. - Phase 1 최신 targeted/legacy 회귀: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 `BUILD SUCCESSFUL`(28초)을 확인했다. - Phase 1 최신 전체 회귀: 최종 코드 작업 트리에서 `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`(4분 17초)을 확인했다. - Phase 1 최신 lint: import 순서 1건을 수정한 뒤 `./gradlew ktlintCheck` 재실행 결과 `BUILD SUCCESSFUL`(10초)을 확인했다. - Phase 1 최신 diff 무결성: `git diff --check`와 `git diff --cached --check`가 모두 통과했고 conflict marker가 없으며 build 설정, dependency, DDL 변경이 없음을 확인했다. - Phase 1 코드 리뷰 차단 이슈 보완: 리뷰에서 지적된 staged/untracked 누락을 재확인한 결과 최신 작업 트리는 Phase 1 신규 테스트 2개(`AiCharacterAdminAccessDeniedErrorContractTest`, `AiCharacterAdminLoginJwtIntegrationTest`)와 `AccessDeniedException` 403 수정이 모두 변경 세트에 포함되어 있음을 확인했다. `TokenProvider` subject parsing 보정과 관련 테스트, rollback 범위를 Task 1.4/1.5와 Phase 1 rollback 전략에 반영했다. - Phase 1 코드 리뷰 차단 이슈 재검증: `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest'` 단독 실행 결과 `BUILD SUCCESSFUL`을 확인했다. 이전 QA의 Gradle `TestOutputStore` EOF는 동시/강제 실행 환경에서 발생한 결과 저장소 문제로 보며, 현재 단독 fresh rerun에서는 재현되지 않았다. - Phase 1 코드 리뷰 차단 이슈 최종 검증: 문서 보정 후 `git status --short --untracked-files=all`에서 untracked 파일이 없고 `git diff --name-only` 결과가 비어 있음을 확인했다. `git diff --cached --check`, `git diff --check`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest' --tests 'kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest'`, `./gradlew ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`을 확인했다. - Task 1.6 CORS 계약 재확인: 현재 `WebConfig` 기준 전용 Origin `http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net` 세 개를 PRD/plan에 명시했다. preflight 테스트는 세 Origin 모두 허용하고 기존 범용 관리자/creator Origin 네 개를 거부하는지 고정했다. - Task 1.6 JWT RED/GREEN: `TokenProviderTest` 9개 중 추가한 누락·빈 값·빈 분할 항목·비문자열 `auth` claim 7개 invocation이 실패하는 RED를 확인했다. claim을 authority로 변환하기 전 검증해 `common.error.bad_credentials`로 변환한 뒤 9개 모두 통과했다. - Task 1.6 테스트 fixture 격리: 전용 controller 6개를 각 테스트의 nested `@TestComponent`로 이동하고 `@Import`로만 등록했다. 초기 nested 이동 후 명시 등록이 누락된 WebMvc 요청 37개가 404로 실패한 것을 확인한 뒤 보정했고, 독립 재리뷰의 component scan 지적을 `@TestComponent`로 해소했다. - Task 1.6 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 103개 모두 통과했다. 기존 `AdminAgentReadControllerSecurityTest`, `AdminContentControllerSecurityTest` 보안 회귀도 `BUILD SUCCESSFUL`을 확인했다. - Task 1.6 최종 전체 회귀/lint: 최종 소스 상태에서 `./gradlew test`는 `BUILD SUCCESSFUL`(4분 55초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(19초)를 확인했다. - Task 1.6 최종 reviewer/diff gate: 독립 read-only 재리뷰 결과 CORS, malformed `auth`, fixture scan 격리에 잔여 finding이 없음을 확인했다. `git diff --check`, `git diff --cached --check`가 통과했고 conflict marker와 untracked 파일이 없음을 확인했다. - Task 1.6 2차 리뷰 CORS RED/GREEN: 실제 mapped endpoint, 공유 `/admin/member/login`, `/member/logout`의 캐릭터 관리자 Origin 요청/preflight를 추가했을 때 `AiCharacterAdminLoginJwtIntegrationTest` 6개 중 3개 실패를 확인했다. 두 공유 인증 exact path에만 기존 전역 Origin과 캐릭터 관리자 Origin 합집합을 적용한 뒤, 미등록 Origin 거부까지 포함한 7개가 모두 통과했다. 신규 prefix의 기존 범용 관리자 Origin 거부와 실제 로그인·로그아웃도 함께 검증했다. - Task 1.6 2차 리뷰 HTTP 오류 RED/GREEN: 406, 405 `Allow`, 415 `Accept`/`Accept-Patch`, `MissingPathVariableException` 500 계약을 추가해 신규 13개 실패를 확인했다. `AiCharacterAdminExceptionHandler`에 Spring 기본 HTTP 의미를 보존하는 최소 분기와 header 처리를 추가한 뒤 `AiCharacterAdminErrorContractTest` 80개가 모두 통과했다. - Task 1.6 2차 리뷰 최종 targeted/레거시 회귀: `TokenProviderTest`와 `aicharacter.*` 114개, 기존 `AdminAgentReadControllerSecurityTest`와 `AdminContentControllerSecurityTest` 11개를 함께 실행해 총 125개 모두 통과했고 `BUILD SUCCESSFUL`(42초)을 확인했다. - Task 1.6 2차 리뷰 전체 회귀/lint: 최종 소스 상태에서 `./gradlew test`는 `BUILD SUCCESSFUL`(5분 4초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(20초)을 확인했다. - Task 1.6 2차 리뷰 최종 gate: 공유 인증 exact path가 전역 fallback보다 먼저 적용되고 신규 prefix/fallback은 전용 Origin을 유지하는지 독립 read-only로 재검토했다. `git diff --check`, `git diff --cached --check`가 통과했고 conflict marker와 untracked 파일이 없음을 확인했다. - Task 1.6 후속 리뷰 mapped write preflight 보완: 기존 신규 prefix write preflight 검증이 fallback `/api/v2/admin/ai-characters/unmapped-path`만 타는 한계를 확인했다. 테스트 controller에 실제 mapped `/api/v2/admin/ai-characters/error-contract/write-preflight`의 `POST`/`PUT`/`PATCH`/`DELETE` 매핑을 추가하고, 해당 경로 preflight에서 `Access-Control-Allow-Origin`과 `Access-Control-Allow-Methods`를 함께 검증하도록 보완했다. - Task 1.6 후속 리뷰 fallback write preflight 보완: actual mapping과 fallback이 서로 다른 CORS 설정을 사용하므로, mapped endpoint 검증과 별도로 fallback `/api/v2/admin/ai-characters/unmapped-path`에서도 `POST`/`PUT`/`PATCH`/`DELETE` preflight의 `Access-Control-Allow-Origin`과 `Access-Control-Allow-Methods`를 검증하도록 보완했다. - Task 1.6 후속 리뷰 추가 계약 보완: wrong-role `USER` + `AI_CHARACTER` target은 resolver가 400으로 거부하고 DB insert/update/delete 없이 기존 role/memberKind를 유지하는지 고정했다. 캐릭터 관리자 Origin이 공유 인증 외 legacy/public 경로로 확산되지 않는지 확인했고, 406 Not Acceptable 응답도 허용 Origin에서는 localized `ApiResponse`와 `Access-Control-Allow-Origin`을 함께 반환하는지 검증했다. - Task 1.6 후속 리뷰 계약 테스트 현황: mapped write preflight 보완 후 `AiCharacterAdminErrorContractTest` 85개, `AiCharacterAdminLoginJwtIntegrationTest` 8개, `AiCharacterAdminTargetResolverIntegrationTest` 4개 기준으로 후속 리뷰 항목을 회귀했다. - Task 1.6 후속 리뷰 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 targeted+legacy 132개가 모두 통과했고 `BUILD SUCCESSFUL`(40초)을 확인했다. - Task 1.6 후속 리뷰 전체 회귀/lint: mapped write preflight 보완 후 소스 상태에서 `./gradlew test`는 전체 1,259개 기준 `BUILD SUCCESSFUL`(4분 53초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(37초)을 확인했다. - Task 1.6 후속 리뷰 fallback 보완 후 계약 테스트 현황: fallback write preflight 보완 후 `AiCharacterAdminErrorContractTest` 89개, `AiCharacterAdminLoginJwtIntegrationTest` 8개, `AiCharacterAdminTargetResolverIntegrationTest` 4개 기준으로 후속 리뷰 항목을 회귀했다. - Task 1.6 후속 리뷰 fallback 보완 후 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 targeted+legacy 136개가 모두 통과했고 `BUILD SUCCESSFUL`(1분 38초)을 확인했다. - Task 1.6 후속 리뷰 fallback 보완 후 전체 회귀/lint: fallback write preflight 보완 후 최종 소스 상태에서 `./gradlew test`는 전체 1,263개 기준 `BUILD SUCCESSFUL`(5분 58초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(35초)을 확인했다. - Task 1.6 추가 코드 리뷰 보완: 실제 logout 후 동일 JWT로 신규 prefix 보호 경로를 호출하면 localized 401을 반환하는지 `AiCharacterAdminLoginJwtIntegrationTest`에 통합 테스트로 고정했다. 공유 `/admin/member/login`, `/member/logout` CORS preflight는 `WebConfig`의 기존 전역 Origin과 캐릭터 관리자 Origin 합집합 전체를 허용하는 parameterized test로 확장했다. Phase 1 targeted Run 명령에는 `TokenProviderTest`를 포함하도록 보정했다. - Task 1.6 추가 코드 리뷰 보완 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest` 실행 결과 `BUILD SUCCESSFUL`을 확인했다. - Task 1.6 추가 코드 리뷰 보완 최종 회귀/lint: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 `BUILD SUCCESSFUL`(44초), `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`(25초)을 확인했다. - Phase 1 최신 canonical fresh targeted/legacy 회귀: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 145/145, failure/error 0, `BUILD SUCCESSFUL`(3분 12초)을 확인했다. - Phase 1 최신 전체 fresh 회귀: `./gradlew test --rerun-tasks` 실행 결과 1,272/1,272, failure/error 0, `BUILD SUCCESSFUL`(6분 49초)을 확인했다. - Phase 1 최신 lint: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`(17초)을 확인했다. - Phase 1 최신 명령 유효성: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`이며 `test`, `ktlintCheck` task가 존재함을 확인했다. - Phase 1 최종 보강 후 canonical fresh targeted/legacy 회귀: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 9개 XML class, 154/154, failure/error/skipped 0, `BUILD SUCCESSFUL`(5분)을 확인했다. - Phase 1 최종 보강 후 전체 fresh 회귀: `./gradlew test --rerun-tasks` 실행 결과 243개 XML class, 1,281/1,281, failure/error/skipped 0, `BUILD SUCCESSFUL`(10분 2초)을 확인했다. - Phase 1 최종 보강 후 lint: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL`(29초)을 확인했다. - `P5-T3` RED (2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --rerun-tasks` 실행 결과 테스트 컴파일 후 6개 테스트가 모두 `POST` 미매핑으로 기대 400/200 대비 실제 405를 반환해 실패했다. 테스트 setup 오류가 아닌 생성 endpoint 미구현을 확인했다. - `P5-T3` GREEN (2026-07-28): 동일 focused 명령을 재실행해 정상 무료 생성의 target owner/data null, image/audio legacy media upload, 유료·오디오 이미지 누락 legacy 오류, inactive target 선검증, request part 누락 envelope을 포함한 6개 테스트가 `BUILD SUCCESSFUL in 4m 7s`로 통과했다. 리뷰 보완으로 private publisher reflection과 after-commit mock 검증을 제거하고 실패 케이스를 요청 전후 DB count 불변으로 검증하도록 낮춘 뒤 focused 명령을 다시 실행해 `BUILD SUCCESSFUL in 3m 53s`를 확인했다. - `P5-T3` legacy/community 회귀 (2026-07-28): 병렬 실행 중 legacy 단독 명령 1회가 QueryDSL Q-class 미해결 compile 오류로 실패했으나, 같은 시점의 community package 회귀는 `BUILD SUCCESSFUL in 5m 53s`였다. 이후 legacy 단독 명령을 순차 재실행해 `BUILD SUCCESSFUL in 5m 5s`를 확인했다. - `P5-T3` lint/diff 검증 (2026-07-28): `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 46s`였고, `git diff --check`는 출력이 없었다. 구현 범위는 community controller/facade의 생성 endpoint와 생성 통합 테스트이며 P5-T4/P5-T5 endpoint는 추가하지 않았다. ### `P6-T1` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: `LegacyFanTalkReplyCharacterizationTest`로 legacy `ExplorerService.writeCheers`의 root parent 연결, writer/creator 저장, response mapper, blank 언어 감지 event, missing parent root 전환, nested/inactive parent 허용, 중복 creator 답변과 creator/blocked 오류를 고정했다. - 왜: Phase 6 v2가 legacy 저장·응답·이벤트 의미는 재사용하되 root/active/owner 검증 없는 legacy 허용 범위는 복제하지 않기 때문이다. - 어떻게: production 변경 없이 focused test를 먼저 실행하고, import 정렬 1건을 수정한 뒤 focused test와 `ktlintCheck`를 재실행했다. - 결과: 첫 focused 실행은 `BUILD SUCCESSFUL in 9s`, import 정렬 후 focused 재실행은 `BUILD SUCCESSFUL in 28s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 production 변경이 없고 focused test가 직접 legacy service 경계를 실행하므로 생략했다. - 남은 항목: `P6-T2` FanTalk 관리자 목록 조회 구현. ### `P6-T5` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: 실제 FanTalk list/reply endpoint의 JWT 비ADMIN과 stale ADMIN claim 거부 행렬, 빈/공백 content와 malformed/missing body·content binding의 KO/EN/JA 400 `ApiResponse.error` envelope를 추가했다. - 왜: 공통 prefix 인가만으로는 실제 FanTalk endpoint의 이중 ADMIN 증거가 없었고 빈 content가 저장되는 결함이 있었기 때문이다. - 어떻게: contract RED에서 빈/공백 3개 locale assertion이 200으로 실패함을 확인한 뒤 `createReply` 시작부에 `request.content.isBlank()` 공통 guard만 추가했다. existing `AiCharacterAdminFanTalkReplyCreateTest`의 target creator writer/creator assertion을 재사용해 admin/principal impersonation 없음도 확인했다. - 결과: `AiCharacterAdminFanTalkReplyContractTest`는 `BUILD SUCCESSFUL in 53s`, `AiCharacterAdminAuthorizationTest`는 `BUILD SUCCESSFUL in 46s`, FanTalk package는 `BUILD SUCCESSFUL in 50s`, `ktlintCheck`는 unused import 1건 제거 뒤 `BUILD SUCCESSFUL in 31s`였다. - 남은 항목: `P6-GATE`. ### `P6-GATE` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: Phase 6 FanTalk 목록, root reply 저장, target/root/ownership 거부와 보안·오류 회귀를 최종 판정했다. - 왜: Phase 7로 진행하기 전 P6-T1~P6-T5의 endpoint 계약과 package 회귀 성공 증거를 확정해야 하기 때문이다. - 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와 `./gradlew ktlintCheck`를 실행했다. - 결과: FanTalk package는 `BUILD SUCCESSFUL in 50s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`였다. 전체 `./gradlew test`는 Phase 7 범위이므로 실행하지 않았다. - 문서/변경 범위 검증: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`였고 `test`, `ktlintCheck`, `tasks` task가 존재했다. `git diff --check`는 출력 없이 통과했다. - 남은 항목: `P7-T1` 대기. ### `P7-T1` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: Phase 1~6 targeted test와 전체 회귀 필요성을 판정했다. - 왜: 최종 통합 Gate 전에 신규 AI 캐릭터 관리자 API 전체와 공통 JWT/인가 경계의 최신 회귀 결과가 필요하기 때문이다. - 어떻게: 계획의 targeted 명령인 `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`를 실행했다. - 결과: targeted test는 `BUILD SUCCESSFUL in 3m 50s`였고, 10개 task 중 1 executed, 9 up-to-date였다. 직전 fresh 검증으로 FanTalk package는 `BUILD SUCCESSFUL in 27s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 2s`, `git diff --check`는 출력 없이 통과했다. - 전체 회귀 판정: 이번 Goal은 production 코드를 추가 변경하지 않았고 targeted 범위가 `TokenProviderTest`, 신규 prefix 전체, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, Phase 1~6 신규 slice 회귀를 포함한다. 공통 경계나 legacy/public runtime 변경을 새로 만들지 않았으므로 전체 `./gradlew test`는 생략한다. - 남은 항목: `P7-T2` API contract와 변경 범위 점검. ### `P7-T2` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: API contract, 변경 범위, dependency/DDL, 신규 v2 import 방향, source spec acceptance criteria를 read-only로 점검했다. - 왜: 최종 Gate 전에 신규 기능 추가 없이 계획·PRD·OpenAPI·diff가 같은 상태를 가리키는지 확인하기 위해서다. - 어떻게: `git diff --name-only`, `git diff --check`, `build.gradle.kts` 확인, DDL/migration 경로 검색, 신규 `v2/api/admin/aicharacter` import 검색, `api-contract.openapi.json` operationId 검색, PRD §11 acceptance criteria 대조, `./gradlew ktlintCheck`, `./gradlew tasks --all`을 실행했다. - 결과: `git diff --check`는 출력 없이 통과했다. `build.gradle.kts` diff는 없고, 변경 파일 중 `.sql`/migration/DDL 경로는 0건이다. `api-contract.openapi.json`에는 23개 operationId가 있으며 Phase 2~6 구현 범위와 일치한다. 신규 package import 검색에서 기존 controller 역참조는 없었다. FanTalk facade의 공개 v2 `CreatorChannelFanTalk*Response` import는 계획과 PRD가 명시한 FanTalk 목록 응답 형태 재사용 예외라 유지한다. - Acceptance 대조: PRD §11의 ADMIN 이중 인가, 401/403, API 오류/i18n, 405/406/415/500, CORS/firewall, full-context security, target resolver, binding exception, no-side-effect, cross-owner, character/content/series/community/FanTalk parity, legacy/public 회귀, 신규 dependency/DDL 없음은 Phase 1~6 Gate와 `P7-T1` targeted 결과로 모두 추적됐다. - 검증: `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 11s`였고 `test`, `ktlintCheck`, `tasks` task가 존재했다. - 남은 항목: `P7-GATE` 최종 완료 판정. ### `P7-GATE` 완료 — 2026-07-28 - 상태: 완료 - 무엇을: Phase 1~7 완료 증거, review finding 종결 상태, 최신 targeted/lint/tasks/diff 결과를 대조해 최종 완료로 판정했다. - 왜: 신규 AI 캐릭터 관리자 API 구현을 더 진행하지 않고 인수 가능한 상태인지 확인하기 위해서다. - 어떻게: `REV-001`~`REV-020` 종합 표가 모두 `처리 완료`인지 확인하고, 독립 Oracle read-only 리뷰를 받아 Gate 차단 finding이 없음을 확인했다. 이어 `git status --short --untracked-files=all`, `git diff --check`, `./gradlew ktlintCheck`를 fresh 실행했다. - 결과: Oracle 리뷰는 PASS였고 Blocker/Important finding은 없었다. `git status --short --untracked-files=all`은 Phase 2~6의 의도된 tracked/untracked 변경 범위를 보여줬으며 신규 dependency/DDL 경로는 없었다. `git diff --check`는 출력 없이 통과했고, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 2s`였다. - 최종 판정: `P7-T1` targeted test `BUILD SUCCESSFUL in 3m 50s`, `P7-T2` `ktlintCheck`/`tasks --all`/diff 점검, 전체 회귀 생략 근거, review finding 처리 완료 기록이 모두 충족되어 문서 상태를 `구현 완료`로 갱신했다. - 남은 항목: 없음. 커밋은 수행하지 않았다. ### `P7-GATE` 후속 OpenAPI 불일치 수정 — 2026-07-28 - 상태: 완료 - 무엇을: `removeAiCharacterSeriesContent` 구현을 OpenAPI 원본과 같은 `DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`로 정합화하고 request body DTO를 제거했다. - 왜: OpenAPI는 path `contentId`와 body 없음이 기준인데 기존 구현은 `/contents` + JSON body `{contentId}`를 받아 계약과 달랐기 때문이다. - 어떻게: series content, contract, authorization 테스트를 body 없는 path `contentId` 호출로 먼저 바꿔 RED를 확인한 뒤 controller mapping과 DTO만 최소 변경했다. 신규 API라 dual route나 하위호환 shim은 추가하지 않았다. - 결과: RED focused 실행은 기존 `/contents/{contentId}` 미매핑으로 5건이 실패했다. GREEN focused 실행 후 최신 series package 회귀 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`는 `BUILD SUCCESSFUL in 24s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 2s`, `git diff --check`는 출력 없이 통과했다.