test #443
@@ -6,58 +6,88 @@
|
||||
OpenAPI 3.1 JSON으로 고정한다.
|
||||
|
||||
- 정식 계약: `api-contract.openapi.json`
|
||||
- endpoint: 23개
|
||||
- 현재 route 구현: 9개
|
||||
- 현재 구현 중 계약 정합화 필요: 9개
|
||||
- 구현 예정: 14개
|
||||
- endpoint: 37개
|
||||
- 현재 route 구현: 37개
|
||||
- 현재 계약과 일치하는 구현 완료: 37개
|
||||
- 계약 정합화 필요: 0개
|
||||
- 구현 예정: 0개
|
||||
- 공통 envelope: `ApiResponse<T>`
|
||||
- 인증: JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN` 동시 충족
|
||||
|
||||
`x-implementation-status`의 의미는 다음과 같다.
|
||||
|
||||
- `implemented-contract-alignment-required`: route는 구현되어 있지만 2026-07-28 확정 레거시 JSON 계약과 runtime DTO가
|
||||
아직 달라 `P23-CONTRACT-2`, `P23-CONTRACT-3`을 완료하기 전 호출 호환을 보장하지 않는다.
|
||||
- `planned`: 계약만 고정되었고 route는 아직 구현되지 않았다.
|
||||
- `implemented`: route가 구현되어 있고 확정 계약과 후속 리뷰 Gate를 통과했다.
|
||||
- `alignment-required`: route는 구현되어 있으나 승인된 최신 계약에 맞춘 request/response 정합화가 필요하다.
|
||||
- `planned`: 계약과 구현 Task는 확정됐으나 route가 아직 구현되지 않았다.
|
||||
|
||||
## 2. 계약 결정
|
||||
|
||||
1. 신규 endpoint와 `characterId` 기반 관리자 target 경계는 유지한다.
|
||||
2. JSON 필드명, 타입, optional/nullable, 기본값과 성공 `data` 형태는 레거시 API를 유지한다.
|
||||
3. 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`만 request body에서 중복 제거한다.
|
||||
3. 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`, `replyId`만 request body에서 중복
|
||||
제거한다.
|
||||
4. 레거시 mutation이 `ApiResponse.ok(null)`이면 신규 endpoint도 `data: null`을 반환한다.
|
||||
5. 오디오 콘텐츠 생성은 레거시 `CreateAudioContentResponse(contentId)`를 반환한다.
|
||||
6. FanTalk 답변 작성만 신규 계획의 축약 응답
|
||||
`fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
|
||||
7. FanTalk 목록은 공개 v2 `CreatorChannelFanTalkTabResponse`의 필드 형태를 유지하는 관리자 전용 endpoint로 추가한다.
|
||||
8. 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 response shape가 달라 별도 endpoint로 분리한다.
|
||||
9. 시리즈 상세 `data`는 배열 wrapper 없이 시리즈 목록 `items`의 단일 객체와 동일한 필드·타입을 사용한다.
|
||||
10. 오디오 콘텐츠와 커뮤니티 게시글 댓글의 원댓글/답글 조회는 레거시 응답을 유지하고, 작성은 target AI 캐릭터
|
||||
명의로 수행한다.
|
||||
11. 댓글 수정은 target AI 캐릭터가 작성한 댓글/답글만 허용한다. 삭제는 target 소유 리소스에 달린 댓글/답글이면
|
||||
작성자와 관계없이 해당 row만 soft delete하며, 이미 비활성인 row의 삭제는 성공 no-op이다.
|
||||
12. 댓글 생성의 optional `parentId`가 없으면 원댓글, 있으면 같은 리소스의 활성 원댓글에 대한 답글이다.
|
||||
13. 팬이 작성한 FanTalk 원글 삭제는 target 채널의 root만 soft delete하고 이미 비활성이면 성공 no-op이며 연결된 creator reply row는 유지한다.
|
||||
14. FanTalk 답변 수정은 레거시 `PutWriteCheersRequest`에서 path `replyId`로 이동한 `cheersId`만 제거하고
|
||||
optional/nullable `content`, `isActive`, 빈 객체 no-op과 `CreatorChannelFanTalkResponse` 성공 `data`를 유지한다.
|
||||
15. 캐릭터에 직접 달리는 레거시 댓글 삭제 API는 v2 전환 후 사용하지 않으므로 계약과 구현 범위에서 제외한다.
|
||||
16. 캐릭터 등록용 원작 검색과 시리즈 장르 목록은 기존 관리자 조회 규칙과 전체 response 필드를 재사용한다.
|
||||
17. AI 캐릭터 관리자 오디오·커뮤니티 API는 `timezone` query/body를 받지 않는다. 오디오 생성의 nullable
|
||||
`releaseDate`는 클라이언트가 ISO-8601 UTC(`Z`)로 변환해 보내며, 오디오 상세 `releaseDate`와 오디오·커뮤니티
|
||||
댓글 `date`도 기존 필드명과 null/노출 조건을 유지한 채 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
|
||||
## 3. endpoint와 레거시 근거
|
||||
|
||||
| 상태 | Method | Endpoint | request 근거 | response `data` 근거 |
|
||||
|---|---|---|---|---|
|
||||
| 정합화 필요 | GET | `/api/v2/admin/ai-characters` | `searchTerm?`, `page`, `size` | `ChatCharacterListPageResponse` / `ChatCharacterSearchListPageResponse` |
|
||||
| 정합화 필요 | POST | `/api/v2/admin/ai-characters` | `ChatCharacterRegisterRequest`, 필수 `image` | `null` |
|
||||
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}` | path only | `ChatCharacterDetailResponse` |
|
||||
| 정합화 필요 | PUT | `/api/v2/admin/ai-characters/{characterId}` | `ChatCharacterUpdateRequest`에서 `id` 제외 | `null` |
|
||||
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | body 없음 | `List<GetAudioContentThemeResponse>` |
|
||||
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `search_word?`, `page`, `size` | `GetCreatorAdminContentListResponse` |
|
||||
| 정합화 필요 | POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `CreateAudioContentRequest`, `contentFile`, `coverImage` | `CreateAudioContentResponse` |
|
||||
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | 필수 `timezone` | `GetAudioContentDetailResponse` |
|
||||
| 정합화 필요 | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | `UpdateCreatorAdminContentRequest`에서 `id` 제외 | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series` | `page`, `size` | `GetCreatorAdminContentSeriesListResponse` |
|
||||
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/series` | `CreateSeriesRequest`, 필수 `image` | `null` |
|
||||
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/orders` | `UpdateOrdersRequest` | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | path only | `GetCreatorAdminContentSeriesDetailResponse` |
|
||||
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | `ModifySeriesRequest`에서 `seriesId` 제외 | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `page`, `size` | `GetCreatorAdminContentSeriesContentResponse` |
|
||||
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `AddingContentToTheSeriesRequest`에서 `seriesId` 제외 | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/search` | 필수 `search_word` | `List<SearchContentNotInSeriesResponse>` |
|
||||
| 계획 | DELETE | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}` | body 없음 | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts` | 필수 `timezone`, `page`, `size` | `List<GetCommunityPostListResponse>` |
|
||||
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/community-posts` | `CreateCommunityPostRequest`, `audioFile?`, `postImage?` | `null` |
|
||||
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` | 두 레거시 update request에서 ID 제외 | `null` |
|
||||
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/fan-talks` | 공개 v2 `page?`, `size?` | `CreatorChannelFanTalkTabResponse` |
|
||||
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` | `content` | 신규 축약 응답 |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters` | `searchTerm?`, `page`, `size` | `ChatCharacterListPageResponse` / `ChatCharacterSearchListPageResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters` | `ChatCharacterRegisterRequest`, 필수 `image` | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/original-works/search` | 필수 `searchTerm` | `List<OriginalWorkResponse>` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}` | path only | `ChatCharacterDetailResponse` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}` | `ChatCharacterUpdateRequest`에서 `id` 제외 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | body 없음 | `List<GetAudioContentThemeResponse>` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `search_word?`, `page`, `size` | `GetCreatorAdminContentListResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `timezone`을 제외하고 nullable UTC `releaseDate`를 받는 `AudioContentCreateRequest`, `contentFile`, `coverImage` | `CreateAudioContentResponse` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | path only | `releaseDate`가 nullable UTC인 `GetAudioContentDetailResponse` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | `UpdateCreatorAdminContentRequest`에서 `id` 제외 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments` | `page`, `size` | 댓글 `date`가 UTC인 `GetAudioContentCommentListResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments` | `comment`, `parentId?`, `isSecret?`, `languageCode?` | `null` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}` | `comment` | `null` |
|
||||
| 구현 완료 | DELETE | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}` | body 없음 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}/replies` | `page`, `size` | 댓글 `date`가 UTC인 `GetAudioContentCommentListResponse` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/series-genres` | body 없음 | `List<GetSeriesGenreListResponse>` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/series` | `page`, `size` | `GetCreatorAdminContentSeriesListResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/series` | `CreateSeriesRequest`, 필수 `image` | `null` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/orders` | `UpdateOrdersRequest` | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | path only | 시리즈 목록 `items` 단일 객체 |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | `ModifySeriesRequest`에서 `seriesId` 제외 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `page`, `size` | `GetCreatorAdminContentSeriesContentResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `AddingContentToTheSeriesRequest`에서 `seriesId` 제외 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/search` | 필수 `search_word` | `List<SearchContentNotInSeriesResponse>` |
|
||||
| 구현 완료 | DELETE | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}` | body 없음 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts` | `page`, `size` | `AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/community-posts` | `CreateCommunityPostRequest`, `audioFile?`, `postImage?` | `null` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` | 두 레거시 update request에서 ID 제외 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` | `page`, `size` | 댓글 `date`가 UTC인 `GetCommunityPostCommentListResponse` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` | `comment`, `parentId?`, `isSecret?` | `null` |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}` | `comment` | `null` |
|
||||
| 구현 완료 | DELETE | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}` | body 없음 | `null` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies` | `page`, `size` | 댓글 `date`가 UTC인 `GetCommunityPostCommentListResponse` |
|
||||
| 구현 완료 | GET | `/api/v2/admin/ai-characters/{characterId}/fan-talks` | 공개 v2 `page?`, `size?` | `CreatorChannelFanTalkTabResponse` |
|
||||
| 구현 완료 | DELETE | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}` | body 없음 | `null` |
|
||||
| 구현 완료 | POST | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` | `content` | 신규 축약 응답 |
|
||||
| 구현 완료 | PUT | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` | `PutWriteCheersRequest`에서 `cheersId` 제외: `content?`, `isActive?` | `CreatorChannelFanTalkResponse` |
|
||||
|
||||
## 4. JSON Schema 해석
|
||||
|
||||
@@ -68,7 +98,8 @@ OpenAPI 3.1 JSON으로 고정한다.
|
||||
- request의 optional nullable 필드는 key 생략과 명시적 `null`을 모두 허용한다.
|
||||
- request는 `additionalProperties: false`이므로 정의되지 않은 이름은 계약 위반이다.
|
||||
- 모든 DB ID는 JSON integer, OpenAPI `int64`다.
|
||||
- 레거시 날짜 문자열은 원래 format을 유지한다. UTC ISO-8601이 보장되는 FanTalk `createdAtUtc`만 `date-time`이다.
|
||||
- 승인된 UTC 예외인 오디오 생성·상세 `releaseDate`, 오디오·커뮤니티 댓글 `date`, FanTalk `createdAtUtc`는
|
||||
OpenAPI `date-time`이다. 그 밖의 레거시 날짜 문자열은 기존 format을 유지한다.
|
||||
- enum은 대소문자를 구분하며 Kotlin enum 이름을 그대로 사용한다.
|
||||
- `Accept-Language`는 `ko`, `en`, `ja` 외 문자열도 전송할 수 있고 서버가 KO로 fallback하므로 enum으로 제한하지 않는다.
|
||||
|
||||
@@ -99,17 +130,20 @@ multipart API의 `request` part는 schema상 JSON 객체다.
|
||||
`ChatCharacterListResponse`, `ChatCharacterListPageResponse`,
|
||||
`ChatCharacterSearchListPageResponse`, `ChatCharacterDetailResponse`,
|
||||
`RelationshipResponse`, `PersonalityResponse`, `BackgroundResponse`,
|
||||
`MemoryResponse`, `OriginalWorkBriefResponse`
|
||||
`MemoryResponse`, `OriginalWorkBriefResponse`, `OriginalWorkResponse`
|
||||
- 목록 response는 `totalCount`, `content`이며 `items/page/size/hasNext`로 바꾸지 않는다.
|
||||
- 상세에는 `systemPrompt`, 캐릭터 속성 배열과 `originalWork`를 포함한다.
|
||||
- 생성과 수정의 성공 `data`는 상세가 아니라 `null`이다.
|
||||
- 캐릭터 등록용 원작 검색은 필수 `searchTerm`으로 제목·콘텐츠 타입·카테고리를 부분 검색하고, soft delete 원작을
|
||||
제외한 `OriginalWorkResponse` 전체 필드의 직접 배열을 반환한다. 별도 pagination은 두지 않는다.
|
||||
- 수정은 `isActive=false`와 다른 optional field의 동시 입력도 레거시 request처럼 허용한다. 이 경우 레거시 service 의미대로
|
||||
비활성화만 반영하고 나머지 JSON field는 적용하지 않는다.
|
||||
|
||||
### AudioContent
|
||||
|
||||
- request:
|
||||
`CreateAudioContentRequest`, `UpdateCreatorAdminContentRequest`
|
||||
`AudioContentCreateRequest`, `UpdateCreatorAdminContentRequest`,
|
||||
`RegisterCommentRequest`, `ModifyCommentRequest`
|
||||
- response:
|
||||
`GetAudioContentThemeResponse`, `GetCreatorAdminContentListResponse`,
|
||||
`GetCreatorAdminContentListItem`, `CreateAudioContentResponse`,
|
||||
@@ -120,6 +154,16 @@ multipart API의 `request` part는 schema상 JSON 객체다.
|
||||
`audioContentId`, `contentUrl`, `tags`를 레거시 이름 그대로 사용한다.
|
||||
- 현재 v2 전용 `description`, `releaseDateUtc`, `audioSignedUrl`, `status`,
|
||||
`seriesIds`, `themeName`, `imageUrl` 별칭은 정식 계약에 포함하지 않는다.
|
||||
- 생성 request는 `timezone`을 받지 않으며 nullable `releaseDate`를 ISO-8601 UTC(`Z`)로 받는다. 로컬 시각과
|
||||
timezone을 함께 받는 레거시 생성 형식은 신규 관리자 endpoint에서 지원하지 않는다.
|
||||
- 상세 response의 nullable `releaseDate`는 기존 null/노출 조건을 유지하고, 값이 있으면 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 댓글/답글 목록은 `timezone` 없이 `page`, `size`와 레거시 `totalCount`, `items`를 유지하고, 각 `date`를
|
||||
ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 댓글 생성은 target AI 캐릭터 명의로 수행한다. `parentId`는 optional/nullable, `isSecret` 기본값은 `false`,
|
||||
`languageCode`는 optional/nullable이다.
|
||||
- 댓글 수정 request는 `comment`만 받으며 target AI 캐릭터가 작성한 활성 댓글/답글만 수정한다.
|
||||
- 삭제는 target 소유 활성 오디오 콘텐츠의 댓글/답글 row 하나만 `isActive=false`로 변경한다. 하위 답글을
|
||||
cascade 삭제하지 않으며 mutation 성공 `data`는 `null`이다.
|
||||
|
||||
### Series
|
||||
|
||||
@@ -129,27 +173,41 @@ multipart API의 `request` part는 schema상 JSON 객체다.
|
||||
`UpdateOrdersRequest`
|
||||
- response:
|
||||
`GetCreatorAdminContentSeriesListResponse`,
|
||||
`GetCreatorAdminContentSeriesDetailResponse`,
|
||||
`GetCreatorAdminContentSeriesContentResponse`,
|
||||
`SearchContentNotInSeriesResponse`
|
||||
`SearchContentNotInSeriesResponse`, `GetSeriesGenreListResponse`
|
||||
- `publishedDaysOfWeek` enum은 `SUN..SAT`, `RANDOM`이며 state는
|
||||
`PROCEEDING`, `SUSPEND`, `COMPLETE`다.
|
||||
- 상세의 `publishedDaysOfWeek`, `genre`, `keywords`, `state`는 레거시처럼 문자열이다.
|
||||
- 상세 `state`의 현재 관찰 값은 `연재중`, `휴재중`, `완결`이지만 DTO 타입이 `String`이므로 enum으로 제한하지 않는다.
|
||||
- 상세 `data`는 목록 `items`의 단일 객체와 동일하게
|
||||
`seriesId`, `title`, `introduction`, `coverImageUrl`, `publishedDaysOfWeek`, `genreId`, `isAdult`,
|
||||
`state`, `isActive`, `writer`, `studio`를 반환한다. 별도 `genre`, `keywords`는 반환하지 않는다.
|
||||
- 등록용 장르 목록은 활성 장르를 `orders` 오름차순으로 조회하고 `id`, `genre`, `isAdult`의 직접 배열을 반환한다.
|
||||
- 연결 request는 `contentIdList`, 순서 request는 `ids`다.
|
||||
|
||||
### Community
|
||||
|
||||
- request:
|
||||
`CreateCommunityPostRequest`, `ModifyCommunityPostRequest`,
|
||||
`UpdateCommunityPostFixedRequest`
|
||||
`UpdateCommunityPostFixedRequest`, `CreateCommunityPostCommentRequest`,
|
||||
`ModifyCommunityPostCommentRequest`
|
||||
- response:
|
||||
`GetCommunityPostListResponse`, `GetCommunityPostCommentListItem`
|
||||
- 목록 `data`는 pagination wrapper가 아니라 직접 배열이다.
|
||||
`GetCommunityPostListResponse`, `GetCommunityPostCommentListResponse`,
|
||||
`GetCommunityPostCommentListItem`
|
||||
- 2026-07-29 사용자 확정에 따라 목록은 레거시 직접 배열의 예외다. `timezone` query를 제거하고 `data`를
|
||||
`AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`로 반환한다.
|
||||
- `totalCount`는 target creatorMember 소유 active 게시글 전체 개수이고, `items`는 기존
|
||||
`GetCommunityPostListResponse` 필드·고정 우선 정렬을 유지한다. `hasNext`는 현재 page 뒤에 active owner 게시글이
|
||||
더 있는지를 나타낸다.
|
||||
- 생성 part 이름은 `postImage`, `audioFile`이다.
|
||||
- 수정 endpoint는 기존 본문 수정과 고정/해제를 합치므로 ID를 제외한
|
||||
`content`, `isCommentAvailable`, `isAdult`, `isActive`, `isFixed`를 받는다.
|
||||
- 레거시에 없는 수정 `price`, `audioFile`은 포함하지 않는다.
|
||||
- 댓글/답글 목록은 `timezone` 없이 `page`, `size`와 레거시 `totalCount`, `items`를 유지하고, 각 `date`를
|
||||
ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 댓글 생성은 target AI 캐릭터 명의로 수행하고 optional/nullable `parentId`와 기본값 `false`인 `isSecret`을
|
||||
받는다.
|
||||
- 댓글 수정 request는 `comment`만 받으며 target AI 캐릭터가 작성한 활성 댓글/답글만 수정한다.
|
||||
- 삭제는 target 소유 활성 게시글의 댓글/답글 row 하나만 `isActive=false`로 변경한다. 하위 답글을 cascade
|
||||
삭제하지 않으며 mutation 성공 `data`는 `null`이다.
|
||||
|
||||
### FanTalk
|
||||
|
||||
@@ -159,8 +217,18 @@ multipart API의 `request` part는 schema상 JSON 객체다.
|
||||
- 공개 v2 endpoint를 직접 사용하지 않는다. 공개 v2는 `creatorId`, viewer 인증과 block filter, 다른 CORS 경계를 사용하기
|
||||
때문이다.
|
||||
- 신규 관리자 목록은 `characterId`로 creator를 해석하고 공개 v2 response field만 유지한다.
|
||||
- 답변 request는 path로 이동한 `creatorId`, `parentId`를 제외하고 `content`만 받는다.
|
||||
- 답변 response는 사용자 승인 예외인 신규 축약 형태다.
|
||||
- 답변 작성 request는 path로 이동한 `creatorId`, `parentId`를 제외하고 `content`만 받는다.
|
||||
- 답변 작성 response는 사용자 승인 예외인 신규 축약 형태다.
|
||||
- 답변 수정 request는 레거시 `PutWriteCheersRequest`에서 path로 이동한 `cheersId`만 제외하고 optional/nullable
|
||||
`content`, `isActive`를 받는다. 두 필드를 함께 입력할 수 있고, 모두 생략하거나 `null`이면 성공 no-op이다.
|
||||
- 답변 수정 대상은 target AI가 writer이자 creator이고 path의 활성 root에 직접 연결된 reply로 한정한다. reply 자체가
|
||||
비활성이어도 `isActive=true`로 재활성화할 수 있으며, 다른 target/root·팬 작성 row·nested mismatch는 400이다.
|
||||
- 답변 수정은 non-null field만 반영하고 기존 `languageCode`를 변경하거나 언어 감지·이벤트를 발생시키지 않는다.
|
||||
- 답변 수정 response `data`는 레거시 `CreatorChannelFanTalkResponse` 필드 형태다. `fanTalkId`는 수정한 reply row ID이고
|
||||
`creatorReplies`는 빈 배열이다.
|
||||
- 삭제는 target 채널에 속한 팬 작성 원글만 허용하고 해당 root row만 `isActive=false`로 변경한다. 연결된 creator reply
|
||||
row는 유지되며 목록에서 root가 제외되므로 함께 노출되지 않는다.
|
||||
- 이미 비활성인 원글 삭제는 성공 no-op이고 성공 `data`는 `null`이다.
|
||||
|
||||
## 6. 공통 응답과 오류
|
||||
|
||||
@@ -221,8 +289,7 @@ tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM \
|
||||
```
|
||||
|
||||
`info-license`만 제외하는 이유는 저장소 라이선스 값을 추측해 계약에 추가하지 않기 위해서다.
|
||||
생성 클라이언트에는 23개 operation이 모두 포함된다. `implemented-contract-alignment-required` operation은 해당 runtime
|
||||
정합화 Gate가 끝나기 전 production 호출 대상으로 간주하지 않는다.
|
||||
생성 클라이언트에는 계약의 37개 operation이 모두 포함된다. 37개 모두 실제 route가 구현되어 있고 `implemented` 상태다.
|
||||
|
||||
OpenAPI의 `/` server URL은 현재 host를 의미하지만 OpenAPI Generator 7.24.0의 `typescript-fetch` runtime 기본값은
|
||||
`http://localhost`다. 실제 클라이언트는 배포 환경의 API origin을 `new Configuration({ basePath: "..." })`로 반드시
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "AI 캐릭터 관리자 API",
|
||||
"version": "2.0.0",
|
||||
"description": "클라이언트 개발용 정식 계약. 신규 관리자 endpoint와 path target을 사용하되 JSON 필드명, 타입, optional/nullable, 기본값과 성공 data 형태는 레거시 API를 유지한다. path로 이동한 ID만 body에서 제거한다."
|
||||
"version": "2.3.0",
|
||||
"description": "클라이언트 개발용 정식 계약. 신규 관리자 endpoint와 path target을 사용하되 JSON 필드명, 타입, optional/nullable, 기본값과 성공 data 형태는 승인된 예외 외에는 레거시 API를 유지한다. path로 이동한 ID만 body에서 제거한다. AI 캐릭터 관리자 API의 날짜·시간 예외 계약은 timezone query/body 없이 ISO-8601 UTC(Z)를 사용한다."
|
||||
},
|
||||
"servers": [{"url": "/", "description": "현재 호스트"}],
|
||||
"security": [{"bearerAuth": []}],
|
||||
"tags": [
|
||||
{"name": "Character", "description": "AI 캐릭터 조회·생성·수정"},
|
||||
{"name": "AudioContent", "description": "오디오 콘텐츠 테마·조회·생성·수정"},
|
||||
{"name": "Series", "description": "시리즈와 연결 콘텐츠 관리"},
|
||||
{"name": "Community", "description": "커뮤니티 게시글 관리"},
|
||||
{"name": "FanTalk", "description": "FanTalk 관리자 목록과 creator reply"}
|
||||
{"name": "Character", "description": "AI 캐릭터 조회·생성·수정과 등록용 원작 검색"},
|
||||
{"name": "AudioContent", "description": "오디오 콘텐츠 테마·조회·생성·수정과 댓글 관리"},
|
||||
{"name": "Series", "description": "시리즈·등록용 장르·연결 콘텐츠 관리"},
|
||||
{"name": "Community", "description": "커뮤니티 게시글과 댓글 관리"},
|
||||
{"name": "FanTalk", "description": "FanTalk 관리자 목록·creator reply 작성·수정·팬 원글 삭제"}
|
||||
],
|
||||
"paths": {
|
||||
"/api/v2/admin/ai-characters": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"tags": ["Character"],
|
||||
"summary": "AI 캐릭터 목록/검색",
|
||||
"operationId": "listAiCharacters",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminChatCharacterController.getCharacterList", "AdminChatCharacterController.searchCharacters", "ChatCharacterListPageResponse", "ChatCharacterSearchListPageResponse"],
|
||||
"parameters": [
|
||||
{"name": "searchTerm", "in": "query", "required": false, "description": "생략하면 활성 목록, 지정하면 레거시 검색을 수행한다.", "schema": {"type": "string"}},
|
||||
@@ -43,7 +43,7 @@
|
||||
"tags": ["Character"],
|
||||
"summary": "AI 캐릭터 생성",
|
||||
"operationId": "createAiCharacter",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminChatCharacterController.registerCharacter", "ChatCharacterRegisterRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/CharacterCreateMultipart"},
|
||||
"responses": {
|
||||
@@ -59,13 +59,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/original-works/search": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}],
|
||||
"get": {
|
||||
"tags": ["Character"],
|
||||
"summary": "캐릭터 등록용 원작 검색",
|
||||
"operationId": "searchAiCharacterOriginalWorks",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminOriginalWorkController.search", "AdminOriginalWorkService.searchOriginalWorksAll", "OriginalWorkResponse"],
|
||||
"parameters": [
|
||||
{"name": "searchTerm", "in": "query", "required": true, "description": "제목·콘텐츠 타입·카테고리 부분 검색어", "schema": {"type": "string"}}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/OriginalWorkSearchSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}, {"$ref": "#/components/parameters/CharacterId"}],
|
||||
"get": {
|
||||
"tags": ["Character"],
|
||||
"summary": "AI 캐릭터 상세",
|
||||
"operationId": "getAiCharacter",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminChatCharacterController.getCharacterDetail", "ChatCharacterDetailResponse"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/CharacterDetailSuccess"},
|
||||
@@ -83,7 +106,7 @@
|
||||
"summary": "AI 캐릭터 수정/soft delete",
|
||||
"description": "레거시 ChatCharacterUpdateRequest의 id만 path characterId로 이동한다.",
|
||||
"operationId": "updateAiCharacter",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminChatCharacterController.updateCharacter", "ChatCharacterUpdateRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/CharacterUpdateMultipart"},
|
||||
"responses": {
|
||||
@@ -105,7 +128,7 @@
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "활성 오디오 콘텐츠 테마 목록",
|
||||
"operationId": "listAiCharacterAudioContentThemes",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["GetAudioContentThemeResponse"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/AudioThemeListSuccess"},
|
||||
@@ -125,7 +148,7 @@
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 목록/검색",
|
||||
"operationId": "listAiCharacterAudioContents",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentController.getAudioContentList", "CreatorAdminContentController.searchAudioContent", "GetCreatorAdminContentListResponse"],
|
||||
"parameters": [
|
||||
{"name": "search_word", "in": "query", "required": false, "description": "지정하면 레거시 검색을 수행하며 2자 이상이어야 한다.", "schema": {"type": "string"}},
|
||||
@@ -147,7 +170,7 @@
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 생성",
|
||||
"operationId": "createAiCharacterAudioContent",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentController.createAudioContent", "CreateAudioContentRequest", "CreateAudioContentResponse"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/AudioContentCreateMultipart"},
|
||||
"responses": {
|
||||
@@ -169,9 +192,8 @@
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 상세",
|
||||
"operationId": "getAiCharacterAudioContent",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentController.getDetail", "GetAudioContentDetailResponse"],
|
||||
"parameters": [{"$ref": "#/components/parameters/Timezone"}],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/AudioContentDetailSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
@@ -188,7 +210,7 @@
|
||||
"summary": "오디오 콘텐츠 수정/soft delete",
|
||||
"description": "레거시 UpdateCreatorAdminContentRequest의 id만 path contentId로 이동한다.",
|
||||
"operationId": "updateAiCharacterAudioContent",
|
||||
"x-implementation-status": "implemented-contract-alignment-required",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentController.modifyAudioContent", "UpdateCreatorAdminContentRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/AudioContentUpdateMultipart"},
|
||||
"responses": {
|
||||
@@ -204,13 +226,161 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/ContentId"}
|
||||
],
|
||||
"get": {
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 댓글 목록",
|
||||
"operationId": "listAiCharacterAudioContentComments",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentCommentController.getCommentList", "GetAudioContentCommentListResponse"],
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/Page"},
|
||||
{"$ref": "#/components/parameters/Size"}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/AudioContentCommentListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 댓글 또는 답글 작성",
|
||||
"description": "parentId가 없으면 원댓글, 있으면 같은 콘텐츠의 활성 원댓글에 대한 답글을 target AI 캐릭터 명의로 작성한다.",
|
||||
"operationId": "createAiCharacterAudioContentComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentCommentController.registerComment", "RegisterCommentRequest"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioContentCommentCreateRequest"}}}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"415": {"$ref": "#/components/responses/UnsupportedMediaType"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/ContentId"},
|
||||
{"$ref": "#/components/parameters/CommentId"}
|
||||
],
|
||||
"put": {
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "AI 캐릭터 작성 오디오 콘텐츠 댓글 수정",
|
||||
"operationId": "updateAiCharacterAudioContentComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentCommentController.modifyComment", "ModifyCommentRequest"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentUpdateRequest"}}}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"415": {"$ref": "#/components/responses/UnsupportedMediaType"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 댓글 soft delete",
|
||||
"description": "target 소유 콘텐츠의 댓글 또는 답글을 작성자와 관계없이 해당 row만 isActive=false로 변경한다. 이미 비활성이면 성공 no-op이며 하위 답글은 변경하지 않는다.",
|
||||
"operationId": "deleteAiCharacterAudioContentComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentCommentService.modifyComment", "ModifyCommentRequest.isActive"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}/replies": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/ContentId"},
|
||||
{"$ref": "#/components/parameters/CommentId"}
|
||||
],
|
||||
"get": {
|
||||
"tags": ["AudioContent"],
|
||||
"summary": "오디오 콘텐츠 댓글 답글 목록",
|
||||
"operationId": "listAiCharacterAudioContentCommentReplies",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AudioContentCommentController.getCommentReplyList", "GetAudioContentCommentListResponse"],
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/Page"},
|
||||
{"$ref": "#/components/parameters/Size"}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/AudioContentCommentListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/series-genres": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}],
|
||||
"get": {
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 등록용 활성 장르 목록",
|
||||
"operationId": "listAiCharacterSeriesGenres",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["AdminContentSeriesGenreController.getSeriesGenreList", "GetSeriesGenreListResponse"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/SeriesGenreListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/series": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}, {"$ref": "#/components/parameters/CharacterId"}],
|
||||
"get": {
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 목록",
|
||||
"operationId": "listAiCharacterSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.getSeriesList", "GetCreatorAdminContentSeriesListResponse"],
|
||||
"parameters": [{"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/Size"}],
|
||||
"responses": {
|
||||
@@ -228,7 +398,7 @@
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 생성",
|
||||
"operationId": "createAiCharacterSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.createSeries", "CreateSeriesRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/SeriesCreateMultipart"},
|
||||
"responses": {
|
||||
@@ -250,7 +420,7 @@
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 순서 변경",
|
||||
"operationId": "reorderAiCharacterSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.updateSeriesOrders", "UpdateOrdersRequest"],
|
||||
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesOrderUpdateRequest"}}}},
|
||||
"responses": {
|
||||
@@ -271,9 +441,10 @@
|
||||
"get": {
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 상세",
|
||||
"description": "성공 data는 시리즈 목록 items의 단일 항목과 동일한 schema를 사용한다.",
|
||||
"operationId": "getAiCharacterSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.getDetail", "GetCreatorAdminContentSeriesDetailResponse"],
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.getSeriesList", "GetCreatorAdminContentSeriesListItem"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/SeriesDetailSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
@@ -290,7 +461,7 @@
|
||||
"summary": "시리즈 수정/soft delete",
|
||||
"description": "레거시 ModifySeriesRequest의 seriesId만 path로 이동한다.",
|
||||
"operationId": "updateAiCharacterSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.modifySeries", "ModifySeriesRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/SeriesUpdateMultipart"},
|
||||
"responses": {
|
||||
@@ -312,7 +483,7 @@
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 연결 콘텐츠 목록",
|
||||
"operationId": "listAiCharacterSeriesContents",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.getSeriesContent", "GetCreatorAdminContentSeriesContentResponse"],
|
||||
"parameters": [{"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/Size"}],
|
||||
"responses": {
|
||||
@@ -331,7 +502,7 @@
|
||||
"summary": "시리즈 콘텐츠 연결",
|
||||
"description": "레거시 AddingContentToTheSeriesRequest의 seriesId만 path로 이동한다.",
|
||||
"operationId": "addAiCharacterSeriesContents",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.addingContentToTheSeries", "AddingContentToTheSeriesRequest"],
|
||||
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesContentAddRequest"}}}},
|
||||
"responses": {
|
||||
@@ -353,7 +524,7 @@
|
||||
"tags": ["Series"],
|
||||
"summary": "시리즈 미연결 콘텐츠 검색",
|
||||
"operationId": "searchAiCharacterContentsNotInSeries",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.searchContentNotInSeries", "SearchContentNotInSeriesResponse"],
|
||||
"parameters": [{"name": "search_word", "in": "query", "required": true, "schema": {"type": "string"}}],
|
||||
"responses": {
|
||||
@@ -375,7 +546,7 @@
|
||||
"summary": "시리즈 콘텐츠 연결 해제",
|
||||
"description": "레거시 RemoveContentToTheSeriesRequest의 seriesId와 contentId를 path로 이동해 body가 없다.",
|
||||
"operationId": "removeAiCharacterSeriesContent",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorAdminContentSeriesController.removeContentInTheSeries", "RemoveContentToTheSeriesRequest"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
@@ -395,9 +566,9 @@
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 목록",
|
||||
"operationId": "listAiCharacterCommunityPosts",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.getCommunityPostList", "GetCommunityPostListResponse"],
|
||||
"parameters": [{"$ref": "#/components/parameters/Timezone"}, {"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/Size"}],
|
||||
"parameters": [{"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/Size"}],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/CommunityPostListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
@@ -413,7 +584,7 @@
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 등록",
|
||||
"operationId": "createAiCharacterCommunityPost",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.createCommunityPost", "CreateCommunityPostRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/CommunityPostCreateMultipart"},
|
||||
"responses": {
|
||||
@@ -436,7 +607,7 @@
|
||||
"summary": "커뮤니티 게시글 수정/고정/soft delete",
|
||||
"description": "ModifyCommunityPostRequest의 creatorCommunityId와 UpdateCommunityPostFixedRequest의 postId를 path로 이동하고 나머지 레거시 필드를 하나의 request에 합친다.",
|
||||
"operationId": "updateAiCharacterCommunityPost",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.modifyCommunityPost", "CreatorCommunityController.updateCommunityPostFixed", "ModifyCommunityPostRequest", "UpdateCommunityPostFixedRequest"],
|
||||
"requestBody": {"$ref": "#/components/requestBodies/CommunityPostUpdateMultipart"},
|
||||
"responses": {
|
||||
@@ -452,6 +623,134 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/PostId"}
|
||||
],
|
||||
"get": {
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 댓글 목록",
|
||||
"operationId": "listAiCharacterCommunityPostComments",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.getCommunityPostCommentList", "GetCommunityPostCommentListResponse"],
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/Page"},
|
||||
{"$ref": "#/components/parameters/Size"}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/CommunityCommentListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 댓글 또는 답글 작성",
|
||||
"description": "parentId가 없으면 원댓글, 있으면 같은 게시글의 활성 원댓글에 대한 답글을 target AI 캐릭터 명의로 작성한다.",
|
||||
"operationId": "createAiCharacterCommunityPostComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.createCommunityPostComment", "CreateCommunityPostCommentRequest"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommunityCommentCreateRequest"}}}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"415": {"$ref": "#/components/responses/UnsupportedMediaType"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/PostId"},
|
||||
{"$ref": "#/components/parameters/CommentId"}
|
||||
],
|
||||
"put": {
|
||||
"tags": ["Community"],
|
||||
"summary": "AI 캐릭터 작성 커뮤니티 댓글 수정",
|
||||
"operationId": "updateAiCharacterCommunityPostComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.modifyCommunityPostComment", "ModifyCommunityPostCommentRequest"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentUpdateRequest"}}}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"415": {"$ref": "#/components/responses/UnsupportedMediaType"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 댓글 soft delete",
|
||||
"description": "target 소유 게시글의 댓글 또는 답글을 작성자와 관계없이 해당 row만 isActive=false로 변경한다. 이미 비활성이면 성공 no-op이며 하위 답글은 변경하지 않는다.",
|
||||
"operationId": "deleteAiCharacterCommunityPostComment",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityService.modifyCommunityPostComment", "ModifyCommunityPostCommentRequest.isActive"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/PostId"},
|
||||
{"$ref": "#/components/parameters/CommentId"}
|
||||
],
|
||||
"get": {
|
||||
"tags": ["Community"],
|
||||
"summary": "커뮤니티 게시글 댓글 답글 목록",
|
||||
"operationId": "listAiCharacterCommunityPostCommentReplies",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorCommunityController.getCommentReplyList", "GetCommunityPostCommentListResponse"],
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/Page"},
|
||||
{"$ref": "#/components/parameters/Size"}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/CommunityCommentListSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/fan-talks": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}, {"$ref": "#/components/parameters/CharacterId"}],
|
||||
"get": {
|
||||
@@ -459,7 +758,7 @@
|
||||
"summary": "FanTalk 관리자 목록",
|
||||
"description": "공개 v2 응답 필드 형태를 유지하되 관리자 target/ownership 정책을 사용하고 viewer/block 필터를 적용하지 않는다.",
|
||||
"operationId": "listAiCharacterFanTalks",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["CreatorChannelFanTalkController.getFanTalkTab", "CreatorChannelFanTalkTabResponse"],
|
||||
"parameters": [{"$ref": "#/components/parameters/FanTalkPage"}, {"$ref": "#/components/parameters/FanTalkSize"}],
|
||||
"responses": {
|
||||
@@ -474,6 +773,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}": {
|
||||
"parameters": [
|
||||
{"$ref": "#/components/parameters/AcceptLanguage"},
|
||||
{"$ref": "#/components/parameters/CharacterId"},
|
||||
{"$ref": "#/components/parameters/FanTalkId"}
|
||||
],
|
||||
"delete": {
|
||||
"tags": ["FanTalk"],
|
||||
"summary": "팬 작성 FanTalk 원글 soft delete",
|
||||
"description": "target 채널의 팬 작성 root만 비활성화하며 이미 비활성이면 성공 no-op이다. 연결 creator reply row는 변경하지 않는다.",
|
||||
"operationId": "deleteAiCharacterFanTalk",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["ExplorerService.modifyCheers", "PutWriteCheersRequest.isActive"],
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/NullSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}, {"$ref": "#/components/parameters/CharacterId"}, {"$ref": "#/components/parameters/FanTalkId"}],
|
||||
"post": {
|
||||
@@ -481,7 +805,7 @@
|
||||
"summary": "FanTalk 답변 작성",
|
||||
"description": "사용자가 승인한 예외로 신규 관리자 축약 응답을 반환한다.",
|
||||
"operationId": "createAiCharacterFanTalkReply",
|
||||
"x-implementation-status": "planned",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["ExplorerController.writeCheers", "PostWriteCheersRequest", "CreatorChannelFanTalkReplyResponse"],
|
||||
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FanTalkReplyCreateRequest"}}}},
|
||||
"responses": {
|
||||
@@ -496,6 +820,29 @@
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}": {
|
||||
"parameters": [{"$ref": "#/components/parameters/AcceptLanguage"}, {"$ref": "#/components/parameters/CharacterId"}, {"$ref": "#/components/parameters/FanTalkId"}, {"$ref": "#/components/parameters/ReplyId"}],
|
||||
"put": {
|
||||
"tags": ["FanTalk"],
|
||||
"summary": "FanTalk 답변 수정",
|
||||
"description": "target AI가 작성하고 path의 활성 root에 직접 연결된 reply만 수정한다. 레거시처럼 optional/nullable content와 isActive의 non-null 값만 반영하며 빈 객체는 성공 no-op이다. 비활성 reply는 isActive=true로 재활성화할 수 있고 성공 data는 CreatorChannelFanTalkResponse 필드 형태다.",
|
||||
"operationId": "updateAiCharacterFanTalkReply",
|
||||
"x-implementation-status": "implemented",
|
||||
"x-legacy-sources": ["ExplorerController.modifyCheers", "ExplorerService.modifyCheers", "PutWriteCheersRequest", "CreatorChannelFanTalkResponse"],
|
||||
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FanTalkReplyUpdateRequest"}}}},
|
||||
"responses": {
|
||||
"200": {"$ref": "#/components/responses/FanTalkReplyUpdateSuccess"},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"},
|
||||
"405": {"$ref": "#/components/responses/MethodNotAllowed"},
|
||||
"406": {"$ref": "#/components/responses/NotAcceptable"},
|
||||
"415": {"$ref": "#/components/responses/UnsupportedMediaType"},
|
||||
"500": {"$ref": "#/components/responses/InternalServerError"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -514,12 +861,13 @@
|
||||
"ContentId": {"name": "contentId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"SeriesId": {"name": "seriesId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"PostId": {"name": "postId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"CommentId": {"name": "commentId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"FanTalkId": {"name": "fanTalkId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"ReplyId": {"name": "replyId", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}},
|
||||
"Page": {"name": "page", "in": "query", "required": false, "schema": {"type": "integer", "format": "int32", "default": 0, "minimum": 0}},
|
||||
"Size": {"name": "size", "in": "query", "required": false, "schema": {"type": "integer", "format": "int32", "default": 20, "minimum": 1}},
|
||||
"FanTalkPage": {"name": "page", "in": "query", "required": false, "description": "공개 v2 정책에서 0 이상으로 보정한다.", "schema": {"type": "integer", "format": "int32", "default": 0}},
|
||||
"FanTalkSize": {"name": "size", "in": "query", "required": false, "description": "공개 v2 정책에서 20..50으로 보정한다.", "schema": {"type": "integer", "format": "int32", "default": 20}},
|
||||
"Timezone": {"name": "timezone", "in": "query", "required": true, "example": "Asia/Seoul", "schema": {"type": "string"}}
|
||||
"FanTalkSize": {"name": "size", "in": "query", "required": false, "description": "공개 v2 정책에서 20..50으로 보정한다.", "schema": {"type": "integer", "format": "int32", "default": 20}}
|
||||
},
|
||||
"requestBodies": {
|
||||
"CharacterCreateMultipart": {
|
||||
@@ -558,17 +906,22 @@
|
||||
"responses": {
|
||||
"CharacterListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CharacterListApiResponse"}}}},
|
||||
"CharacterDetailSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CharacterDetailApiResponse"}}}},
|
||||
"OriginalWorkSearchSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OriginalWorkSearchApiResponse"}}}},
|
||||
"AudioThemeListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioThemeListApiResponse"}}}},
|
||||
"AudioContentListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioContentListApiResponse"}}}},
|
||||
"AudioContentCreateSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioContentCreateApiResponse"}}}},
|
||||
"AudioContentDetailSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioContentDetailApiResponse"}}}},
|
||||
"AudioContentCommentListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AudioContentCommentListApiResponse"}}}},
|
||||
"SeriesListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesListApiResponse"}}}},
|
||||
"SeriesDetailSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesDetailApiResponse"}}}},
|
||||
"SeriesGenreListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesGenreListApiResponse"}}}},
|
||||
"SeriesContentListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesContentListApiResponse"}}}},
|
||||
"SeriesContentSearchSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SeriesContentSearchApiResponse"}}}},
|
||||
"CommunityPostListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommunityPostListApiResponse"}}}},
|
||||
"CommunityCommentListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommunityCommentListApiResponse"}}}},
|
||||
"FanTalkListSuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FanTalkListApiResponse"}}}},
|
||||
"FanTalkReplySuccess": {"description": "성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FanTalkReplyApiResponse"}}}},
|
||||
"FanTalkReplyUpdateSuccess": {"description": "레거시 FanTalk 답변 수정 성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FanTalkReplyUpdateApiResponse"}}}},
|
||||
"NullSuccess": {"description": "레거시 mutation 성공", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NullSuccessResponse"}}}},
|
||||
"BadRequest": {"description": "잘못된 요청/target/domain 오류", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiErrorResponse"}}}},
|
||||
"Unauthorized": {"description": "인증 실패", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiErrorResponse"}}}},
|
||||
@@ -796,6 +1149,27 @@
|
||||
},
|
||||
"CharacterListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/CharacterListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"CharacterDetailApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/CharacterDetailResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"OriginalWorkSearchItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "title", "contentType", "category", "isAdult", "description", "originalWork", "originalLink", "writer", "studio", "originalLinks", "tags", "imageUrl"],
|
||||
"properties": {
|
||||
"id": {"type": "integer", "format": "int64"},
|
||||
"title": {"type": "string"},
|
||||
"contentType": {"type": "string"},
|
||||
"category": {"type": "string"},
|
||||
"isAdult": {"type": "boolean"},
|
||||
"description": {"type": "string"},
|
||||
"originalWork": {"$ref": "#/components/schemas/NullableString"},
|
||||
"originalLink": {"$ref": "#/components/schemas/NullableString"},
|
||||
"writer": {"$ref": "#/components/schemas/NullableString"},
|
||||
"studio": {"$ref": "#/components/schemas/NullableString"},
|
||||
"originalLinks": {"type": "array", "items": {"type": "string"}},
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
"imageUrl": {"$ref": "#/components/schemas/NullableString"}
|
||||
}
|
||||
},
|
||||
"OriginalWorkSearchApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/OriginalWorkSearchItem"}}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
|
||||
"AudioContentTheme": {
|
||||
"type": "object",
|
||||
@@ -815,8 +1189,7 @@
|
||||
"price": {"type": "integer", "format": "int32"},
|
||||
"purchaseOption": {"$ref": "#/components/schemas/PurchaseOption", "default": "BOTH"},
|
||||
"limited": {"$ref": "#/components/schemas/NullableInt32"},
|
||||
"timezone": {"type": "string", "default": "Asia/Seoul"},
|
||||
"releaseDate": {"type": ["string", "null"], "description": "yyyy-MM-dd HH:mm"},
|
||||
"releaseDate": {"type": ["string", "null"], "format": "date-time", "pattern": "Z$", "description": "클라이언트가 UTC로 변환해 보내는 ISO-8601 시각. 예: 2026-07-29T09:00:00Z"},
|
||||
"themeId": {"type": "integer", "format": "int64", "default": 0, "description": "0은 binding 기본값이며 domain validation에서 유효하지 않다."},
|
||||
"isAdult": {"type": "boolean", "default": false},
|
||||
"isGeneratePreview": {"type": "boolean", "default": false},
|
||||
@@ -938,10 +1311,37 @@
|
||||
"languageCode": {"$ref": "#/components/schemas/NullableString"},
|
||||
"isSecret": {"type": "boolean"},
|
||||
"donationCan": {"type": "integer", "format": "int32"},
|
||||
"date": {"type": "string"},
|
||||
"date": {"type": "string", "format": "date-time", "pattern": "Z$", "description": "ISO-8601 UTC 시각(Z)"},
|
||||
"replyCount": {"type": "integer", "format": "int32"}
|
||||
}
|
||||
},
|
||||
"AudioContentCommentCreateRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["comment"],
|
||||
"properties": {
|
||||
"comment": {"type": "string"},
|
||||
"parentId": {"$ref": "#/components/schemas/NullableInt64"},
|
||||
"isSecret": {"type": "boolean", "default": false},
|
||||
"languageCode": {"$ref": "#/components/schemas/NullableString"}
|
||||
}
|
||||
},
|
||||
"CommentUpdateRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["comment"],
|
||||
"properties": {"comment": {"type": "string"}}
|
||||
},
|
||||
"AudioContentCommentListResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["totalCount", "items"],
|
||||
"properties": {
|
||||
"totalCount": {"type": "integer", "format": "int32"},
|
||||
"items": {"type": "array", "items": {"$ref": "#/components/schemas/AudioContentComment"}}
|
||||
}
|
||||
},
|
||||
"AudioContentCommentListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/AudioContentCommentListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"TranslatedContent": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -963,7 +1363,7 @@
|
||||
"tag": {"type": "string"},
|
||||
"price": {"type": "integer", "format": "int32"},
|
||||
"duration": {"type": "string"},
|
||||
"releaseDate": {"$ref": "#/components/schemas/NullableString"},
|
||||
"releaseDate": {"type": ["string", "null"], "format": "date-time", "pattern": "Z$", "description": "기존 null/노출 조건을 유지하는 ISO-8601 UTC 시각(Z)"},
|
||||
"totalContentCount": {"$ref": "#/components/schemas/NullableInt32"},
|
||||
"remainingContentCount": {"$ref": "#/components/schemas/NullableInt32"},
|
||||
"orderSequence": {"$ref": "#/components/schemas/NullableInt32"},
|
||||
@@ -1060,22 +1460,14 @@
|
||||
"required": ["totalCount", "items"],
|
||||
"properties": {"totalCount": {"type": "integer", "format": "int32"}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/SeriesListItem"}}}
|
||||
},
|
||||
"SeriesDetailResponse": {
|
||||
"SeriesGenreItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["seriesId", "title", "introduction", "coverImageUrl", "publishedDaysOfWeek", "genre", "keywords", "isAdult", "state", "writer", "studio"],
|
||||
"required": ["id", "genre", "isAdult"],
|
||||
"properties": {
|
||||
"seriesId": {"type": "integer", "format": "int64"},
|
||||
"title": {"type": "string"},
|
||||
"introduction": {"type": "string"},
|
||||
"coverImageUrl": {"type": "string"},
|
||||
"publishedDaysOfWeek": {"type": "string", "description": "예: 월, 수"},
|
||||
"id": {"type": "integer", "format": "int64"},
|
||||
"genre": {"type": "string"},
|
||||
"keywords": {"type": "string"},
|
||||
"isAdult": {"type": "boolean"},
|
||||
"state": {"type": "string", "description": "레거시 String 필드. 현재 관찰 값: 연재중, 휴재중, 완결"},
|
||||
"writer": {"$ref": "#/components/schemas/NullableString"},
|
||||
"studio": {"$ref": "#/components/schemas/NullableString"}
|
||||
"isAdult": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"SeriesContentListItem": {
|
||||
@@ -1109,7 +1501,8 @@
|
||||
"properties": {"ids": {"type": "array", "items": {"type": "integer", "format": "int64"}}}
|
||||
},
|
||||
"SeriesListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/SeriesListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"SeriesDetailApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/SeriesDetailResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"SeriesDetailApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/SeriesListItem"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"SeriesGenreListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/SeriesGenreItem"}}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"SeriesContentListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/SeriesContentListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"SeriesContentSearchApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/SeriesContentSearchItem"}}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
|
||||
@@ -1124,10 +1517,30 @@
|
||||
"profileUrl": {"type": "string"},
|
||||
"comment": {"type": "string"},
|
||||
"isSecret": {"type": "boolean"},
|
||||
"date": {"type": "string"},
|
||||
"date": {"type": "string", "format": "date-time", "pattern": "Z$", "description": "ISO-8601 UTC 시각(Z)"},
|
||||
"replyCount": {"type": "integer", "format": "int32"}
|
||||
}
|
||||
},
|
||||
"CommunityCommentCreateRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["comment"],
|
||||
"properties": {
|
||||
"parentId": {"$ref": "#/components/schemas/NullableInt64"},
|
||||
"comment": {"type": "string"},
|
||||
"isSecret": {"type": "boolean", "default": false}
|
||||
}
|
||||
},
|
||||
"CommunityCommentListResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["totalCount", "items"],
|
||||
"properties": {
|
||||
"totalCount": {"type": "integer", "format": "int32"},
|
||||
"items": {"type": "array", "items": {"$ref": "#/components/schemas/CommunityPostComment"}}
|
||||
}
|
||||
},
|
||||
"CommunityCommentListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/CommunityCommentListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"CommunityPostListItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -1153,6 +1566,18 @@
|
||||
"firstComment": {"oneOf": [{"$ref": "#/components/schemas/CommunityPostComment"}, {"type": "null"}]}
|
||||
}
|
||||
},
|
||||
"CommunityPostListResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["totalCount", "page", "size", "hasNext", "items"],
|
||||
"properties": {
|
||||
"totalCount": {"type": "integer", "format": "int64", "minimum": 0},
|
||||
"page": {"type": "integer", "format": "int32", "minimum": 0},
|
||||
"size": {"type": "integer", "format": "int32", "minimum": 1},
|
||||
"hasNext": {"type": "boolean"},
|
||||
"items": {"type": "array", "items": {"$ref": "#/components/schemas/CommunityPostListItem"}}
|
||||
}
|
||||
},
|
||||
"CommunityPostCreateRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -1194,7 +1619,7 @@
|
||||
"request": {"$ref": "#/components/schemas/CommunityPostUpdateRequest"}
|
||||
}
|
||||
},
|
||||
"CommunityPostListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/CommunityPostListItem"}}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"CommunityPostListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/CommunityPostListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
|
||||
"FanTalkCreatorReply": {
|
||||
"type": "object",
|
||||
@@ -1241,6 +1666,15 @@
|
||||
"required": ["content"],
|
||||
"properties": {"content": {"type": "string"}}
|
||||
},
|
||||
"FanTalkReplyUpdateRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "레거시 PutWriteCheersRequest에서 path로 이동한 cheersId만 제외한다. content와 isActive는 optional/nullable이며 둘 다 생략하거나 null이면 성공 no-op이다.",
|
||||
"properties": {
|
||||
"content": {"$ref": "#/components/schemas/NullableString"},
|
||||
"isActive": {"$ref": "#/components/schemas/NullableBoolean"}
|
||||
}
|
||||
},
|
||||
"FanTalkReplyResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -1254,7 +1688,8 @@
|
||||
}
|
||||
},
|
||||
"FanTalkListApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/FanTalkListResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"FanTalkReplyApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/FanTalkReplyResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]}
|
||||
"FanTalkReplyApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/FanTalkReplyResponse"}, "errorProperty": {"type": ["string", "null"], "const": null}}}]},
|
||||
"FanTalkReplyUpdateApiResponse": {"allOf": [{"$ref": "#/components/schemas/ApiSuccessBase"}, {"type": "object", "required": ["message", "data", "errorProperty"], "properties": {"message": {"type": ["string", "null"]}, "data": {"$ref": "#/components/schemas/FanTalkListItem", "description": "레거시 CreatorChannelFanTalkResponse 필드 형태. fanTalkId는 수정한 reply row ID이며 creatorReplies는 빈 배열이다."}, "errorProperty": {"type": ["string", "null"], "const": null}}}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,18 @@
|
||||
# PRD: AI 캐릭터 관리자 API
|
||||
|
||||
## 1. Overview
|
||||
운영자가 AI 캐릭터용 Member로 직접 로그인하지 않고, `ADMIN` 권한으로 선택한 AI 캐릭터의 크리에이터 채널 자산을 대리 관리하는 신규 v2 관리자 API를 제공한다.
|
||||
운영자가 AI 캐릭터용 Member로 직접 로그인하지 않고, `ADMIN` 권한으로 선택한 AI 캐릭터의 크리에이터 채널 자산과 팬 상호작용을
|
||||
대리 관리하는 신규 v2 관리자 API를 제공한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem
|
||||
- AI 캐릭터용 `Member(memberKind = AI_CHARACTER)`는 직접 로그인할 수 없어야 하지만, 운영자는 캐릭터의 콘텐츠, 시리즈, 커뮤니티, FanTalk 답변을 관리해야 한다.
|
||||
- AI 캐릭터용 `Member(memberKind = AI_CHARACTER)`는 직접 로그인할 수 없어야 하지만, 운영자는 캐릭터의 콘텐츠, 시리즈,
|
||||
커뮤니티, 각 자산의 댓글과 FanTalk를 관리해야 한다.
|
||||
- 기존 기능은 `creatorMember.id` 기반으로 흩어져 있으며, 관리자 frontend가 레거시 endpoint를 조합하면 권한, 소유권, soft delete 의미가 일관되지 않을 수 있다.
|
||||
- 기존 creator/admin service 일부에는 소유권 검증이 약한 경로가 있어, 단순 위임만으로는 다른 캐릭터나 HUMAN creator 자원을 변경할 위험이 있다.
|
||||
- 캐릭터 등록에 필요한 원작 검색과 시리즈 등록에 필요한 장르 목록은 범용 관리자 API에만 있어 캐릭터 관리자 배포 Origin에서
|
||||
호출할 수 없다.
|
||||
- 기존 legacy/public API 계약은 유지해야 하므로 신규 관리자 표면은 별도 v2 경계로 제공되어야 한다.
|
||||
|
||||
---
|
||||
@@ -16,10 +20,13 @@
|
||||
## 3. Goals
|
||||
- 신규 prefix `/api/v2/admin/ai-characters/**`는 JWT `auth` claim의 `ROLE_ADMIN`과 JWT subject로 조회한 현재 DB
|
||||
`Member.role == ADMIN`을 모두 만족하는 요청만 허용한다.
|
||||
- 모든 신규 target endpoint는 외부 대상 식별자로 `characterId`를 받고, 서버가 `ChatCharacter.creatorMember`를 내부 행위자로 해석한다. 단, 캐릭터 목록/검색은 아직 선택된 target이 없어 `characterId`를 받지 않고, 캐릭터 생성은 새 `ChatCharacter`를 만드는 endpoint라 path `characterId`를 받지 않는다.
|
||||
- 모든 신규 target endpoint는 외부 대상 식별자로 `characterId`를 받고, 서버가 `ChatCharacter.creatorMember`를 내부 행위자로
|
||||
해석한다. 단, 캐릭터 목록/검색·생성, 원작 검색과 시리즈 장르 목록은 선택된 target이 필요하지 않아 `characterId`를 받지
|
||||
않는다.
|
||||
- target 해석 시 `ChatCharacter` 존재, `creatorMember` 존재, `creatorMember.role == CREATOR`, `creatorMember.memberKind == AI_CHARACTER`를 모두 검증한다.
|
||||
- 검증 실패 시 4xx로 거부하고 DB, S3, 외부 캐릭터 API, 이벤트 발행 등 후속 부작용을 만들지 않는다.
|
||||
- 캐릭터, 오디오 콘텐츠, 시리즈, 커뮤니티 게시글, FanTalk 목록·답변, 오디오 signed URL을 신규 관리자 API에서 관리한다.
|
||||
- 캐릭터, 오디오 콘텐츠와 댓글, 시리즈, 커뮤니티 게시글과 댓글, FanTalk 목록·답변 작성·수정·팬 원글 삭제, 오디오
|
||||
signed URL을 신규 관리자 API에서 관리한다.
|
||||
- 기존 legacy/public endpoint의 URI, 성공·오류 HTTP status, response body, message/i18n을 포함한 외부 계약은 변경하지 않는다.
|
||||
단, 캐릭터 관리자 frontend가 기존 관리자 인증을 재사용할 수 있도록 `/admin/member/login`, `/member/logout`의 CORS 허용
|
||||
Origin만 path-specific으로 확장한다.
|
||||
@@ -38,14 +45,16 @@
|
||||
- 기존 soft delete 의미 변경은 포함하지 않으며, 변경이 필요하면 재확인한다.
|
||||
- 물리 삭제와 연관 데이터 cascade 삭제는 포함하지 않는다.
|
||||
- 신규 DB schema/DDL 또는 `ChatCharacter`-`Member` 관계 모델 변경은 포함하지 않는다.
|
||||
- 라이브, DM, 후원, 정산, 알림 설정, 랭킹 관리, 콘텐츠 구매/좋아요/댓글, 커뮤니티 구매/좋아요/댓글 관리는 포함하지 않는다.
|
||||
- FanTalk 원글 작성, 일반 사용자 대리 작성, nested reply 작성은 포함하지 않는다.
|
||||
- 라이브, DM, 후원, 정산, 알림 설정, 랭킹 관리, 콘텐츠 구매·좋아요와 커뮤니티 구매·좋아요 관리는 포함하지 않는다.
|
||||
- 사용하지 않는 레거시 캐릭터 직접 댓글 `/api/chat/character/{characterId}/comments`의 v2 전환·조회·삭제는 포함하지 않는다.
|
||||
- FanTalk 원글 작성, creator reply 전용 삭제 endpoint·hard delete, 일반 사용자 대리 작성, nested reply 작성과 FanTalk
|
||||
원글 삭제 시 reply cascade 변경은 포함하지 않는다.
|
||||
- `AudioContentCloudFront` 복사/이동, signed URL 신규 dependency 추가는 포함하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Target Users
|
||||
- 운영자: AI 캐릭터를 대신해 캐릭터 프로필, 콘텐츠, 시리즈, 커뮤니티 게시글, FanTalk 답변을 관리하는 관리자
|
||||
- 운영자: AI 캐릭터를 대신해 캐릭터 프로필, 콘텐츠, 시리즈, 커뮤니티 게시글, 각 자산의 댓글과 FanTalk를 관리하는 관리자
|
||||
- 관리자 frontend: 신규 v2 AI 캐릭터 관리자 API만으로 In-Scope 작업을 수행해야 하는 클라이언트
|
||||
- 서버 개발자: 기존 creator 기능을 회귀시키지 않으면서 AI 캐릭터 대리 관리 경계를 유지해야 하는 개발자
|
||||
|
||||
@@ -53,10 +62,17 @@
|
||||
|
||||
## 6. User Stories
|
||||
- 운영자는 AI 캐릭터 목록을 검색하고 상세 정보를 확인한 뒤 생성, 수정, 비활성화하고 싶다.
|
||||
- 운영자는 캐릭터 등록 시 soft delete되지 않은 원작을 검색해 `originalWorkId`를 선택하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터 소유 오디오 콘텐츠를 조회, 생성, 수정, soft delete하고 관리자 화면에서 재생 가능한 signed URL을 받고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터의 오디오 콘텐츠에 달린 댓글·답글을 조회하고, 캐릭터 명의로 작성·수정하며, 부적절한 댓글·답글을
|
||||
soft delete하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터 소유 시리즈를 조회, 생성, 수정, soft delete하고 콘텐츠 연결/해제/순서를 관리하고 싶다.
|
||||
- 운영자는 시리즈 등록 시 활성 장르 목록에서 `genreId`를 선택하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터 소유 커뮤니티 게시글을 작성, 수정, 고정/해제, soft delete하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터의 FanTalk 목록과 기존 creator reply를 확인한 뒤 활성 root FanTalk에 답변하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터의 커뮤니티 게시글에 달린 댓글·답글을 조회하고, 캐릭터 명의로 작성·수정하며, 부적절한 댓글·답글을
|
||||
soft delete하고 싶다.
|
||||
- 운영자는 선택한 AI 캐릭터의 FanTalk 목록과 기존 creator reply를 확인한 뒤 활성 root FanTalk에 답변하고, 기존 답변의
|
||||
내용·활성 상태를 수정하거나 팬이 작성한 원글을 soft delete하고 싶다.
|
||||
- 서버는 다른 AI 캐릭터나 HUMAN creator의 resource ID가 전달되면 변경 없이 4xx로 거부해야 한다.
|
||||
|
||||
---
|
||||
@@ -70,7 +86,9 @@
|
||||
- JWT가 없거나 잘못됐거나 만료·폐기된 경우는 401, JWT role과 현재 DB role 중 하나라도 ADMIN이 아닌 경우는 403으로
|
||||
처리한다.
|
||||
- JWT에는 `ROLE_ADMIN`이 남아 있지만 현재 DB role이 강등된 stale claim도 403으로 거부한다.
|
||||
- 모든 domain write/read는 `characterId`로 `ChatCharacter`를 조회한 뒤 연결된 `creatorMember`를 사용한다.
|
||||
- 선택한 캐릭터 자원을 다루는 domain write/read는 `characterId`로 `ChatCharacter`를 조회한 뒤 연결된 `creatorMember`를
|
||||
사용한다. 원작 검색과 시리즈 장르 목록은 target 없는 reference endpoint이므로 `characterId`를 받거나 target을 해석하지
|
||||
않는다.
|
||||
- `creatorMember`는 도메인 소유권/작성자 판단에만 사용하고 Spring Security principal로 교체하지 않는다.
|
||||
- `creatorMember` 누락, role 불일치, memberKind 불일치 요청은 4xx로 거부한다.
|
||||
- 요청 중 누락 Member 생성, role/memberKind 자동 보정 같은 lazy repair는 하지 않는다.
|
||||
@@ -84,12 +102,16 @@
|
||||
|
||||
#### Requirements
|
||||
- 목록 조회, 검색, 상세 조회, 생성, 수정, 삭제 의미의 비활성화(`isActive=false`)를 제공한다.
|
||||
- 캐릭터 등록 화면에서 사용할 soft delete되지 않은 원작 검색을 `characterId` 없는 관리자 전용 endpoint로 제공한다.
|
||||
- 원작 검색은 레거시 `AdminOriginalWorkController.search`처럼 필수 `searchTerm`으로 제목·콘텐츠 타입·카테고리를 부분
|
||||
검색하고 soft delete된 원작을 제외하며, 페이징 없는 `OriginalWorkResponse` 목록을 반환한다.
|
||||
- 레거시 플랫폼 관리자와 중복 이름 검증, 외부 캐릭터 API 연동, 대표 이미지 저장, 원작 연결, 언어 감지/번역 이벤트, AI 캐릭터용 `creatorMember` 생성 및 표시 정보 동기화 동작 parity를 유지한다.
|
||||
- 삭제는 soft delete이며 row, 연결 Member, 콘텐츠를 물리 삭제하지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 중복 이름, 외부 캐릭터 API 실패, 이미지 저장 실패는 기존 관리자 동작을 특성화 테스트로 고정한 뒤 유지한다.
|
||||
- 비활성화 실패 시 일부 관계만 변경된 상태로 남기지 않는다.
|
||||
- 원작 검색은 캐릭터 target을 해석하지 않지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 계약을 동일하게 적용한다.
|
||||
|
||||
### Feature C. 오디오 콘텐츠 관리 및 signed URL
|
||||
|
||||
@@ -102,24 +124,48 @@
|
||||
- signed URL은 공통 `AudioContentCloudFront`를 재사용하고 기존 크리에이터 관리자와 같은 만료 정책을 따른다.
|
||||
- 기존 signed URL 구현을 재사용하기 전에 creator admin 만료 계산식과 만료 계산·path 처리에서 실제로 관찰되는 edge case를 통과하는 특성화 테스트로 고정한다.
|
||||
- 응답에 private object path나 서명 키 정보를 노출하지 않는다.
|
||||
- 신규 관리자 오디오 생성 request는 `timezone`을 받지 않고 nullable `releaseDate`를 클라이언트가 변환한 ISO-8601
|
||||
UTC(`Z`)로 받는다. 로컬 시각과 timezone을 함께 받는 레거시 생성 형식은 병행 지원하지 않는다.
|
||||
- 오디오 상세의 nullable `releaseDate`는 기존 필드명과 null/노출 조건을 유지하고, 값이 있으면 ISO-8601 UTC(`Z`)로
|
||||
반환한다. 상세 조회는 `timezone` query를 받지 않는다.
|
||||
- target 소유의 활성 오디오 콘텐츠에 대해 댓글 목록, 답글 목록, 댓글·답글 작성, 캐릭터가 작성한 댓글·답글 수정과 모든
|
||||
댓글·답글 soft delete를 제공한다.
|
||||
- 댓글·답글 작성자는 관리자 principal이 아니라 target `creatorMember`이며, optional `parentId`가 없으면 원댓글, 있으면
|
||||
답글로 저장한다.
|
||||
- 댓글·답글 내용 수정은 작성자가 target `creatorMember`인 활성 row에만 허용한다.
|
||||
- 댓글·답글 삭제는 작성자와 관계없이 target 소유 콘텐츠에 연결된 row의 `isActive=false`만 반영하고 자식 답글을 cascade
|
||||
변경하거나 물리 삭제하지 않는다.
|
||||
- 댓글·답글 조회는 `timezone` 없이 `page`, `size`를 받고 레거시 응답 필드를 유지하되, 각 `date`를 ISO-8601
|
||||
UTC(`Z`)로 반환한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 콘텐츠 소유자가 target `creatorMember`와 다르면 조회/수정/삭제 모두 거부한다.
|
||||
- 댓글 또는 답글이 target 소유 콘텐츠에 연결되지 않았거나, 답글 작성의 `parentId`가 같은 콘텐츠의 활성 원댓글이 아니면
|
||||
mutation 전에 400으로 거부한다.
|
||||
- 댓글 수정은 팬이 작성한 row를 target 캐릭터 명의로 변경하지 않으며, 이미 비활성인 row도 수정하지 않는다.
|
||||
- 이미 비활성인 댓글·답글의 삭제는 성공 no-op으로 처리하고 추가 상태 변경이나 이벤트를 만들지 않는다.
|
||||
- 커뮤니티 오디오의 기존 30분 signed URL 정책은 이 콘텐츠 재생 정책과 임의 통합하지 않는다.
|
||||
|
||||
### Feature D. 시리즈 관리
|
||||
|
||||
#### Requirements
|
||||
- 목록/상세 조회, 생성, 수정, `isActive=false` soft delete를 제공한다.
|
||||
- 시리즈 상세 응답 `data`는 목록 wrapper가 아니라 목록 `items`의 단일 항목과 동일한 schema를 사용한다. 필드는
|
||||
`seriesId`, `title`, `introduction`, `coverImageUrl`, `publishedDaysOfWeek`, `genreId`, `isAdult`, `state`, `isActive`,
|
||||
`writer`, `studio`이며 기존 상세 전용 `genre`, `keywords`는 반환하지 않는다.
|
||||
- 콘텐츠 연결/해제, 시리즈 콘텐츠 조회/검색, 순서 관리를 제공한다.
|
||||
- 기존 creator series 관리의 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 관리 behavior를 먼저 통과하는 특성화 테스트로 고정하고 신규 v2 경로에서 parity를 유지한다.
|
||||
- 시리즈와 연결 콘텐츠는 모두 동일한 `creatorMember` 소유여야 한다.
|
||||
- 기존 `updateSeriesOrders(ids)`처럼 소유권 없는 ID-only 갱신은 신규 v2 경로에서 허용하지 않는다.
|
||||
- 시리즈 콘텐츠 조회는 관리자 연결 작업을 위해 검색어 기반 필터를 제공한다.
|
||||
- 시리즈 등록 화면에서 사용할 활성 장르 목록을 `characterId` 없는 관리자 전용 endpoint로 제공한다.
|
||||
- 장르 목록은 레거시 범용 관리자 API처럼 활성 장르 전체를 `orders` 오름차순으로 반환하며 각 항목에 `id`, `genre`,
|
||||
`isAdult`를 포함한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 순서 변경 요청의 모든 series/content ID는 target character 소유 검증을 통과해야 한다.
|
||||
- inactive series는 일반 활성 조회에서 제외한다.
|
||||
- 장르 목록은 캐릭터 target을 해석하지 않지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 계약을 동일하게 적용한다.
|
||||
|
||||
### Feature E. 커뮤니티 게시글 관리
|
||||
|
||||
@@ -128,12 +174,28 @@
|
||||
- soft delete 시 현재 동작처럼 `isFixed=false`, `fixedAt=null`을 적용한다.
|
||||
- 기존 최대 고정 게시글 수 3개, 이미지/오디오/유료 게시글 검증, 알림/최근 소식 side effect를 유지한다.
|
||||
- 관리자 UI에 필요한 조회는 기존 v2 커뮤니티 조회 로직을 무비판적으로 복제하지 않고 신규 관리자 facade/endpoint에서 안전하게 재사용하거나 최소 query adapter를 둔다.
|
||||
- 관리자 커뮤니티 목록은 `timezone` query를 받지 않고 `page`, `size`만 받는다.
|
||||
- 목록 응답 `data`는 `totalCount`, `page`, `size`, `hasNext`, `items`를 포함해 관리자 UI가 전체 개수와 다음 페이지
|
||||
추가 로딩 필요 여부를 판단할 수 있어야 한다.
|
||||
- target 소유의 활성 커뮤니티 게시글에 대해 댓글 목록, 답글 목록, 댓글·답글 작성, 캐릭터가 작성한 댓글·답글 수정과 모든
|
||||
댓글·답글 soft delete를 제공한다.
|
||||
- 댓글·답글 작성자는 관리자 principal이 아니라 target `creatorMember`이며, optional `parentId`가 없으면 원댓글, 있으면
|
||||
답글로 저장한다.
|
||||
- 댓글·답글 내용 수정은 작성자가 target `creatorMember`인 활성 row에만 허용한다.
|
||||
- 댓글·답글 삭제는 작성자와 관계없이 target 소유 게시글에 연결된 row의 `isActive=false`만 반영하고 자식 답글을 cascade
|
||||
변경하거나 물리 삭제하지 않는다.
|
||||
- 댓글·답글 조회는 `timezone` 없이 `page`, `size`를 받고 레거시 응답 필드를 유지하되, 각 `date`를 ISO-8601
|
||||
UTC(`Z`)로 반환한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 고정 게시글이 이미 3개인 상태에서 추가 고정은 기존 정책대로 실패한다.
|
||||
- soft delete된 고정 게시글은 고정 상태와 시간이 반드시 제거되어야 한다.
|
||||
- 댓글 또는 답글이 target 소유 게시글에 연결되지 않았거나, 답글 작성의 `parentId`가 같은 게시글의 활성 원댓글이 아니면
|
||||
mutation 전에 400으로 거부한다.
|
||||
- 댓글 수정은 팬이 작성한 row를 target 캐릭터 명의로 변경하지 않으며, 이미 비활성인 row도 수정하지 않는다.
|
||||
- 이미 비활성인 댓글·답글의 삭제는 성공 no-op으로 처리하고 추가 상태 변경이나 이벤트를 만들지 않는다.
|
||||
|
||||
### Feature F. FanTalk 답변
|
||||
### Feature F. FanTalk 관리
|
||||
|
||||
#### Requirements
|
||||
- 선택한 AI 캐릭터의 FanTalk 목록과 각 root 글의 creator reply 목록을 관리자 전용 endpoint로 조회한다.
|
||||
@@ -143,10 +205,29 @@
|
||||
- 요청은 `characterId`와 대상 root `fanTalkId`를 포함한다.
|
||||
- 대상 FanTalk가 존재하고 활성 상태이며, 대상 creator가 해석된 `creatorMember`와 일치하는 root 글인지 검증한다.
|
||||
- 언어 감지와 기존 응답 DTO 의미 등 검증 가능한 business behavior를 유지하고, 저장된 답변의 writer/creator는 해석된 `creatorMember`와 일관되어야 한다.
|
||||
- 선택한 AI 캐릭터가 작성한 기존 direct reply의 내용과 활성 상태를 수정하는 관리자 전용 endpoint를 제공한다.
|
||||
- 답변 수정 request는 레거시 `PutWriteCheersRequest`에서 path로 이동한 `cheersId`를 제외한 optional/nullable `content`,
|
||||
`isActive`를 그대로 받는다. 두 필드는 함께 입력할 수 있고 모두 생략하거나 `null`이면 성공 no-op이다.
|
||||
- 답변 수정 대상은 target `creatorMember`가 writer이자 creator인 direct reply여야 하며, `fanTalkId`로 지정한 target
|
||||
채널의 활성 root에 직접 연결되어야 한다. 비활성 reply는 조회 대상에 포함해 `isActive=true`로 재활성화할 수 있다.
|
||||
- 답변 수정은 레거시와 같이 non-null field만 반영하며 저장된 `languageCode`를 변경하거나 언어 감지·이벤트를 발생시키지
|
||||
않는다.
|
||||
- 답변 수정 성공 `data`는 레거시 `CreatorChannelFanTalkResponse` 필드 형태를 유지한다. 응답의 `fanTalkId`는 수정한
|
||||
reply row ID이고 `creatorReplies`는 빈 배열이다.
|
||||
- 팬이 작성한 root FanTalk를 `isActive=false`로 soft delete하는 관리자 전용 endpoint를 제공한다.
|
||||
- FanTalk 삭제는 root row만 비활성화하고 연결된 creator reply row는 변경하지 않는다. 비활성 root가 목록에서 제외되므로
|
||||
연결된 reply도 함께 노출되지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 다른 캐릭터의 FanTalk, reply에 대한 nested reply, 비활성 FanTalk, 미존재 FanTalk에는 답변하지 않는다.
|
||||
- 실패 시 reply 저장과 이벤트 발행이 없어야 한다.
|
||||
- 답변 수정 시 root·reply가 다른 target에 속하거나, reply가 지정한 root의 direct child가 아니거나, root가
|
||||
비활성·미존재이거나, target AI가 작성하지 않은 팬 root/reply이면 400으로 거부하고 변경하지 않는다.
|
||||
- 답변 수정 대상 reply 자체의 비활성 상태는 거부 조건이 아니며, `isActive=true` 재활성화를 허용한다.
|
||||
- FanTalk 삭제 대상은 target `creatorMember` 채널에 연결된 `parent=null`의 fan 작성 root여야 한다. 같은 target의 이미
|
||||
비활성인 fan root는 성공 no-op이고, creator가 작성한 root, reply, 다른 creator의 root, 미존재 root는 변경 없이
|
||||
400으로 거부한다.
|
||||
- FanTalk 원글 삭제는 reply 삭제·수정이나 별도 이벤트를 발생시키지 않는다.
|
||||
|
||||
---
|
||||
|
||||
@@ -188,18 +269,46 @@
|
||||
- request/response의 기계 검증 가능한 단일 기준은 같은 디렉터리의 `api-contract.openapi.json`이다. 설명과 레거시 근거는
|
||||
`api-contract.md`에 기록한다.
|
||||
- 신규 endpoint는 레거시 API의 request/response 필드명, 타입, optional/nullable, 기본값과 성공 `data` 형태를 그대로
|
||||
이관한다. `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`처럼 신규 path로 이동한 ID만 request body에서
|
||||
중복 제거한다.
|
||||
이관한다. `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`, `replyId`처럼 신규 path로 이동한 ID만
|
||||
request body에서 중복 제거한다.
|
||||
- 캐릭터 등록용 원작 검색은 `GET /api/v2/admin/ai-characters/original-works/search?searchTerm=...`, 시리즈 장르 목록은
|
||||
`GET /api/v2/admin/ai-characters/series-genres`로 제공하며 두 endpoint 모두 `characterId`를 받지 않는다.
|
||||
- 오디오 콘텐츠 댓글은
|
||||
`/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments`, 커뮤니티 댓글은
|
||||
`/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` 하위에서 각각 댓글 목록, 답글 목록, 작성,
|
||||
수정, 삭제 5개 operation으로 제공한다. 답글 목록은 `/{commentId}/replies`, 수정·삭제는 `/{commentId}` 하위 path를
|
||||
사용한다.
|
||||
- 오디오 댓글 작성 request는 `comment`, optional `parentId`, `isSecret`, `languageCode`, 커뮤니티 댓글 작성 request는
|
||||
`comment`, optional `parentId`, `isSecret`을 받는다. 수정 request는 두 domain 모두 `comment`만 받으며, 삭제는 request
|
||||
body를 받지 않는다.
|
||||
- 두 댓글 domain의 목록과 답글 목록은 `GetAudioContentCommentListResponse` 또는
|
||||
`GetCommunityPostCommentListResponse`에 해당하는 `totalCount`, `items` 형태를 유지한다. 두 목록은 `timezone`
|
||||
query를 받지 않고 각 댓글 `date`를 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- FanTalk 팬 원글 삭제는
|
||||
`DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`로 제공한다.
|
||||
- FanTalk 답변 수정은
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`로 제공한다.
|
||||
- 위 14개 신규 operation을 기존 23개에 추가해 전체 관리자 계약은 37개 operation으로 관리한다.
|
||||
- `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`의 성공 `data`는
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/series`의 `items` 단일 항목과 동일한 schema를 참조한다.
|
||||
- 캐릭터 수정은 레거시 `ChatCharacterUpdateRequest`처럼 `isActive=false`와 다른 optional field의 동시 입력을 허용하며,
|
||||
이 경우 레거시 service 의미대로 비활성화만 반영한다.
|
||||
- 목록 endpoint의 query와 page 동작은 각 레거시 API를 따른다. FanTalk 관리자 목록만 공개 v2 탭의 `page` 기본값 0,
|
||||
`size` 기본값 20, 최소 20, 최대 50 보정을 따른다.
|
||||
- 오디오 콘텐츠 생성 multipart의 `request`는 `timezone`을 포함하지 않는다. nullable `releaseDate`는 클라이언트가
|
||||
UTC로 변환한 ISO-8601 `date-time`(`Z`)이며 기존 로컬 시각+timezone 형식은 신규 endpoint에서 받지 않는다.
|
||||
- 오디오 콘텐츠 상세, 오디오 댓글·답글 목록, 커뮤니티 댓글·답글 목록은 `timezone` query를 받지 않는다. 상세
|
||||
`releaseDate`와 댓글 `date`는 기존 필드명 및 nullable/노출 조건을 유지한 ISO-8601 UTC(`Z`)다.
|
||||
- 2026-07-29 사용자 확정에 따라 커뮤니티 관리자 목록은 위 레거시 이관 원칙의 예외로 둔다. 사용하지 않는
|
||||
`timezone` query를 제거하고 `data`를 `totalCount`, `page`, `size`, `hasNext`, `items` pagination wrapper로 반환한다.
|
||||
- multipart 생성/수정은 기존 admin/creator-admin 관례대로 파일 part와 `request` JSON string part를 사용한다.
|
||||
- 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 응답 형태가 다르므로 각각
|
||||
`GET .../series/{seriesId}/contents`와 `GET .../series/{seriesId}/contents/search?search_word=...`로 분리한다.
|
||||
- 레거시 mutation이 `ApiResponse.ok(null)`을 반환하면 신규 endpoint도 `data: null`을 반환한다. 오디오 콘텐츠 생성은
|
||||
레거시 `CreateAudioContentResponse(contentId)`를 유지한다. FanTalk 답변 작성만 신규 계획의 축약 응답
|
||||
`fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
|
||||
`fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. FanTalk 답변 수정은 레거시
|
||||
`CreatorChannelFanTalkResponse` 필드 형태를 유지한다.
|
||||
- 신규 댓글 작성·수정·삭제와 FanTalk 원글 삭제의 성공 응답은 모두 `ApiResponse.ok(null)`을 사용한다.
|
||||
- request/response 구현 DTO는 신규 v2 AI character admin API 전용으로 둘 수 있지만, JSON 외부 계약은
|
||||
`api-contract.openapi.json`의 레거시 필드명과 형태를 유지한다.
|
||||
|
||||
@@ -231,7 +340,14 @@
|
||||
- `HttpMessageNotReadableException`, `MethodArgumentNotValidException`, `MissingServletRequestPartException`의 exact exception
|
||||
type과 KO/EN/JA 400 envelope 직접 검증 여부
|
||||
- target/ownership 실패 시 no-side-effect 테스트 존재 여부
|
||||
- character/content/series/community/FanTalk slice별 targeted test 통과 여부
|
||||
- character/original-work/content/content-comment/series/series-genre/community/community-comment/FanTalk slice별 targeted test
|
||||
통과 여부
|
||||
- 오디오 생성 request와 오디오 상세·댓글·답글 및 커뮤니티 댓글·답글 조회에서 `timezone`이 제거되고,
|
||||
`releaseDate`/`date`가 ISO-8601 UTC(`Z`)로 검증되는지 여부
|
||||
- 댓글 작성·수정의 target 작성자 제한, 소유 자산 댓글 삭제, parent 소유권·root 검증과 soft-delete no-cascade 테스트 존재 여부
|
||||
- FanTalk 팬 root 삭제, 비활성 팬 root no-op, creator reply row 보존·비노출 테스트 존재 여부
|
||||
- FanTalk 답변 수정의 optional/nullable `content`·`isActive`, 빈 객체 no-op, 비활성 reply 재활성화,
|
||||
target/root/direct-reply ownership과 레거시 성공 응답 테스트 존재 여부
|
||||
- signed URL TTL 계산식·edge case parity 및 private path 비노출 테스트 통과 여부
|
||||
- 기존 legacy/public endpoint의 성공·오류 status/body/message 회귀 테스트 통과 여부
|
||||
|
||||
@@ -260,17 +376,47 @@
|
||||
- character 미존재, creatorMember 미존재, role 불일치, memberKind 불일치 요청은 4xx이며 아무 side effect도 남기지 않는다.
|
||||
- 다른 character 소유 resource ID를 사용한 조회/수정/삭제/연결/답변은 4xx로 거부된다.
|
||||
- 캐릭터 생성/수정/비활성화는 레거시 관리자 behavior parity를 유지한다.
|
||||
- 캐릭터 등록용 원작 검색은 soft delete된 원작을 제외하고 제목·콘텐츠 타입·카테고리 부분 검색 결과를 반환한다.
|
||||
- 콘텐츠 생성/수정/soft delete와 signed URL 응답은 기존 creator/admin behavior parity를 유지한다.
|
||||
- 신규 관리자 오디오 생성은 `timezone` 없이 nullable `releaseDate`를 ISO-8601 UTC(`Z`)로 받고, 상세 조회도
|
||||
`timezone` 없이 기존 nullable `releaseDate`를 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 오디오 콘텐츠 댓글은 target 소유 활성 콘텐츠 범위에서 조회되고, target 명의 작성·작성자 한정 수정·소유자 moderation
|
||||
soft delete가 적용된다. 댓글·답글 목록은 `timezone` 없이 각 `date`를 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 시리즈 CRUD, 콘텐츠 연결/해제/순서 변경은 target character 소유 범위를 벗어나지 않는다.
|
||||
- 시리즈 상세 `data`는 목록 `items` 한 건과 동일한 필드·타입을 반환하고 상세 전용 `genre`, `keywords`를 반환하지 않는다.
|
||||
- 시리즈 장르 목록은 활성 장르의 `id`, `genre`, `isAdult`를 `orders` 순으로 반환한다.
|
||||
- 커뮤니티 생성/수정/고정/해제/soft delete는 target creatorMember 소유 게시글에만 적용된다.
|
||||
- 커뮤니티 목록은 `timezone` 없이 조회되고 active owner 게시글의 전체 개수, 요청 page/size, 다음 페이지 여부와
|
||||
기존 목록 item 필드를 반환한다.
|
||||
- 커뮤니티 댓글은 target 소유 활성 게시글 범위에서 조회되고, target 명의 작성·작성자 한정 수정·소유자 moderation soft
|
||||
delete가 적용된다. 댓글·답글 목록은 `timezone` 없이 각 `date`를 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- FanTalk 관리자 목록은 target AI character의 root 글과 creator reply를 공개 v2 응답 필드명으로 반환하며, 공개 v2의
|
||||
viewer/block 필터나 creator 관리자 CORS 경계에 의존하지 않는다.
|
||||
- FanTalk 답변은 target AI character 자신의 활성 root FanTalk에만 저장된다.
|
||||
- FanTalk 답변 수정은 target AI character가 작성하고 해당 target의 활성 root에 직접 연결된 reply에만 적용된다.
|
||||
optional/nullable `content`와 `isActive`의 레거시 상태 전이 및 빈 객체 no-op을 유지하며, 비활성 reply를 재활성화할 수
|
||||
있고 성공 `data`는 레거시 `CreatorChannelFanTalkResponse` 필드 형태다.
|
||||
- FanTalk 원글 삭제는 target 채널의 팬 작성 root만 비활성화하고 이미 비활성이면 성공 no-op이며 연결 creator reply row를 변경하지 않는다.
|
||||
- 레거시 캐릭터 직접 댓글 API는 신규 v2 endpoint나 구현 계획에 포함되지 않는다.
|
||||
- 기존 legacy/public endpoint 테스트가 통과하고, 두 공유 인증 경로의 CORS 허용 Origin 확장 외 성공·오류
|
||||
status/body/message를 포함한 request/response contract가 변경되지 않는다.
|
||||
- 신규 dependency, 신규 DDL, 관련 없는 리팩터링이 없다.
|
||||
|
||||
---
|
||||
|
||||
## 12. Open Questions
|
||||
## 12. Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 범위 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-07-29 | `DEC-COMMENT-001` | 확정 | 오디오 콘텐츠·커뮤니티 댓글은 관리자 principal이 아닌 target AI 캐릭터 명의로 작성하고, target 작성 댓글만 내용을 수정하며, target 소유 자산의 댓글은 작성자와 관계없이 soft delete한다. | 사용자 승인과 기존 콘텐츠·게시글 소유자의 댓글 비활성화 동작 | Feature C, Feature E, API Expectations |
|
||||
| 2026-07-29 | `DEC-CHAR-COMMENT-001` | 제외 | 사용하지 않는 레거시 캐릭터 직접 댓글 API는 v2로 전환하거나 관리자 삭제 기능을 추가하지 않는다. | 사용자 확인 결과 v2 전환 후 미사용 | Non-Goals, Acceptance Criteria |
|
||||
| 2026-07-29 | `DEC-FANTALK-DELETE-001` | 확정 | 팬 작성 FanTalk root 삭제는 원글만 soft delete하고 연결 creator reply row는 변경하지 않는다. | 사용자 승인과 기존 `CreatorCheers.isActive` 동작 유지 | Feature F, API Expectations |
|
||||
| 2026-07-29 | `DEC-FANTALK-REPLY-UPDATE-001` | 확정 | FanTalk 답변 수정은 레거시 `PUT /explorer/profile/cheers`의 optional/nullable `content`, `isActive`, 빈 객체 no-op, 비활성 reply 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지한다. 신규 path의 `characterId`, root `fanTalkId`, `replyId`로 target AI 소유 direct reply를 한정한다. | 사용자 요청과 “기존 계약과 동일” 확정, 레거시 `ExplorerService.modifyCheers` 동작 | Feature F, API Expectations |
|
||||
| 2026-07-29 | `DEC-REGISTRATION-REFERENCE-001` | 확정 | 캐릭터 등록용 원작 검색과 시리즈 등록용 장르 목록을 target 없는 신규 v2 관리자 endpoint로 제공한다. | 레거시 API는 캐릭터 관리자 배포 Origin에서 호출할 수 없고 신규 frontend는 v2 경계를 사용해야 함 | Feature B, Feature D, API Expectations |
|
||||
| 2026-07-29 | `DEC-SERIES-DETAIL-001` | 확정 | 시리즈 상세 `data`를 목록 `items`의 단일 항목과 동일한 schema로 변경하고 기존 상세 전용 `genre`, `keywords`를 제거한다. | 사용자 확정과 관리자 목록·상세 DTO 일관성 | Feature D, API Expectations |
|
||||
| 2026-07-29 | `DEC-UTC-DATE-001` | 확정 | 신규 관리자 오디오 생성의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 조회의 `timezone` query를 제거한다. 생성 `releaseDate`는 클라이언트가 UTC로 변환해 보내고, 상세 `releaseDate`와 댓글 `date`는 기존 필드명을 유지한 ISO-8601 UTC(`Z`)로 반환한다. | 클라이언트별 timezone 표시 변환을 제거하고 단일 절대 시각 계약을 유지한다는 사용자 승인 | Feature C, Feature E, API Expectations |
|
||||
|
||||
---
|
||||
|
||||
## 13. Open Questions
|
||||
- 없음.
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
# Phase 1 공통 경계·보안 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 1 / 공통 target resolver, 보안, 오류 경계 |
|
||||
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 2~7 working tree |
|
||||
| 리뷰 일자 | 2026-07-28 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
|
||||
| 리뷰 상태 | 판정 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- PRD Feature A와 공통 API Expectations가 현재 resolver/security/error 구현에 유지되는지 확인한다.
|
||||
- Phase 2~6의 모든 신규 controller가 같은 prefix 경계와 target 불변식을 공유하는지 정적으로 추적한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `AiCharacterAdminTargetResolver`, `SecurityConfig`, 신규 prefix 오류 handler/writer
|
||||
- `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, resolver 관련 테스트
|
||||
- OpenAPI 공통 오류·security 정의와 plan Phase 1 완료 기록
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- 테스트 재실행, Phase 2~6 domain 세부 동작, legacy/public API 변경
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | 인증·인가 우회, cross-owner write, 데이터 손실 위험 |
|
||||
| High | PRD/OpenAPI 공통 보안·오류 계약 위반 |
|
||||
| Medium | 제한된 경로의 오류·현지화·부작용 계약 누락 |
|
||||
| Low | 유지보수성 또는 문서 정합성 문제 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
### 문서와 코드
|
||||
|
||||
- 요구사항: PRD Feature A, API Expectations, Acceptance Criteria
|
||||
- 계획: Phase 1 `Task 1.1`~`Task 1.7`과 완료 증거
|
||||
- 코드: `AiCharacterAdminTargetResolver.kt:17`~`35`
|
||||
- 코드: `SecurityConfig.kt:118`~`130`, `:202`~`:207`
|
||||
- 코드: `AiCharacterAdminExceptionHandler.kt:28`~`97`
|
||||
- 테스트: resolver unit/integration, authorization, error contract 테스트
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| 코드·문서 정적 추적 | 성공 | creator role/kind 불변식, JWT authority + 현재 DB ADMIN 이중 인가, prefix 전용 오류 경계 확인 |
|
||||
| Gradle/컴파일/테스트 | 미실행 | 사용자가 기존 통과 사실을 제공하고 직접 실행하지 말 것을 요청함 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
## 6. 주요 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| target 해석 | 충족 | character와 creatorMember를 함께 조회하고 `CREATOR + AI_CHARACTER`를 검증 |
|
||||
| ADMIN 이중 인가 | 충족 | `ROLE_ADMIN`, `MemberAdapter`, 현재 DB `Member.role == ADMIN`을 모두 요구 |
|
||||
| prefix 오류 경계 | 충족 | security/MVC/fallback 오류가 신규 prefix 전용 handler로 연결됨 |
|
||||
| Phase별 재사용 | 충족 | Character/Content/Series/Community/FanTalk facade가 공통 resolver를 사용 |
|
||||
| 후속 Task 필요성 | 없음 | 정적 근거에서 신규 확정 finding이 발견되지 않음 |
|
||||
|
||||
## 7. plan·goal 전환
|
||||
|
||||
확정 finding이 없어 Phase 1 신규 Task나 Goal을 추가하지 않았다. 기존 Phase 1 완료 이력은 유지한다.
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음
|
||||
|
||||
**검증 제한:** 이번 판정은 정적 리뷰 결과다. 사용자 요청에 따라 테스트·컴파일을 재실행하지 않았으며 기존 plan의 통과
|
||||
기록을 실행 증거로 재사용하지 않고 참고만 했다.
|
||||
|
||||
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature A·API Expectations, plan Phase 1, OpenAPI 공통 security/error
|
||||
- 검토 범위: target resolver, SecurityConfig/WebConfig, JWT와 prefix 전용 security/MVC 오류 handler,
|
||||
Phase 2~6 facade의 resolver 사용
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER`, creatorMember 연결을 공통 resolver에서 검증 |
|
||||
| ADMIN 이중 인가 | 충족 | JWT authority와 현재 DB role을 모두 확인 |
|
||||
| 오류/CORS 경계 | 충족 | 신규 prefix 전용 handler와 허용 Origin 분리 유지 |
|
||||
| Phase별 적용 | 충족 | Character/AudioContent/Series/Community/FanTalk facade가 resolver 사용 |
|
||||
| plan 전환 | 해당 없음 | Phase 1 신규 확정 finding 없음 |
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음. `REV-030`~`REV-033`의 소유 Phase 보완 뒤 `P7-R2` 통합 재판정에 참여한다.
|
||||
|
||||
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature A·공통 오류, plan Phase 1, OpenAPI security/error
|
||||
- 검토 범위: target resolver, ADMIN 이중 인가, 신규 prefix 오류·CORS, Phase 2~6 resolver 적용
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| target 불변식 | 충족 | creatorMember fetch와 `CREATOR + AI_CHARACTER` 검증 유지 |
|
||||
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role 이중 확인 유지 |
|
||||
| 오류/CORS | 충족 | 신규 prefix 전용 handler와 전용 Origin 정책 유지 |
|
||||
| Phase 적용 | 충족 | 각 domain facade가 공통 resolver를 통해 target을 해석 |
|
||||
| plan 전환 | 해당 없음 | Phase 1 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음
|
||||
|
||||
**남은 항목:** `P7-R3`에서 공통 인가·오류 회귀를 통합 재검증한다.
|
||||
|
||||
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature A·공통 오류, plan Phase 1, OpenAPI 공통 security/error
|
||||
- 검토 범위: target resolver, ADMIN 이중 인가, 오류·CORS·firewall, 각 domain의 resolver 적용
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| target 불변식 | 충족 | fetch join과 `CREATOR + AI_CHARACTER` 검증 유지 |
|
||||
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role을 독립 확인 |
|
||||
| 오류/CORS/firewall | 충족 | prefix 전용 handler와 legacy fallback 유지 |
|
||||
| Phase 적용 | 충족 | Character~FanTalk facade가 공통 resolver 사용 |
|
||||
| plan 전환 | 해당 없음 | Phase 1 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위
|
||||
|
||||
- PRD·OpenAPI 공통 ADMIN 인가, target resolver, 오류·CORS·firewall 계약
|
||||
- Phase 2~6 facade의 공통 resolver 사용과 JSON mapping 오류 변환 경계
|
||||
- primitive nullability 보완을 전역 설정이 아닌 각 v2 request 경계에 둘 수 있는지
|
||||
|
||||
### 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
|
||||
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role의 이중 확인 유지 |
|
||||
| 오류/CORS/firewall | 충족 | prefix 전용 error writer/handler와 허용 origin 정책 유지 |
|
||||
| primitive finding 소유 | Phase 2~5 | 공통 mapper가 아니라 domain별 수동 `ObjectMapper` reader와 DTO에서 발생 |
|
||||
| plan 전환 | 해당 없음 | 전역 Jackson·공통 계층 변경 없이 각 Phase Task로 분리 |
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않고 코드·계약만 정적으로 대조했다.
|
||||
|
||||
**최종 결론:** Phase 1 신규 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 13. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 response/security 계약
|
||||
- 검토 범위: 신규 prefix의 security matcher, JWT authority와 DB role 이중 인가, target resolver,
|
||||
공통 exception handler와 CORS 설정
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### 확인 결과
|
||||
|
||||
| 항목 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| ADMIN 인가 | 충족 | 신규 prefix는 JWT `ROLE_ADMIN`과 현재 principal Member의 DB `ADMIN` role을 모두 확인 |
|
||||
| target 불변식 | 충족 | `characterId`가 가리키는 creatorMember의 `CREATOR + AI_CHARACTER`를 공통 resolver에서 검증 |
|
||||
| 오류/CORS | 충족 | prefix 전용 handler/writer와 승인된 Origin 범위 유지 |
|
||||
| 6차 finding 영향 | 없음 | `REV-052`~`REV-058`은 domain controller의 query/media type 경계에 한정 |
|
||||
|
||||
### finding 및 plan 전환
|
||||
|
||||
- 신규 Phase 1 finding 없음.
|
||||
- Phase 1 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 1 공통 보안·resolver·오류 경계 유지
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 14. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 security/error 계약
|
||||
- 검토 범위: security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 공통 exception/CORS 경계
|
||||
- 검증 방식: 현재 working tree의 문서·코드·관련 테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 principal Member의 DB `ADMIN` role을 독립 확인 |
|
||||
| target 불변식 | 충족 | `characterId` 대상의 `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
|
||||
| 공통 오류/CORS | 충족 | 신규 prefix 전용 handler와 승인 Origin 정책 유지 |
|
||||
| 7차 finding 소유 | Phase 2~5·7 | multipart 이름 검증은 domain controller, 구현 상태 문서는 통합 Phase 소유 |
|
||||
|
||||
### finding 및 plan 전환
|
||||
|
||||
- 신규 Phase 1 finding 없음.
|
||||
- Phase 1 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음
|
||||
|
||||
**남은 항목:** `P7-R9` 통합 재판정에 공통 오류·인가 회귀 근거로 참여한다.
|
||||
|
||||
## 15. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 security/error 계약
|
||||
- 검토 범위: security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 공통 exception/CORS 경계
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·관련 테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`을 독립 확인하는 경계 유지 |
|
||||
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
|
||||
| 공통 오류/CORS | 충족 | 신규 prefix 전용 오류 envelope/i18n과 승인 Origin 정책 유지 |
|
||||
| 8차 finding 소유 | Phase 2~7 | multipart 전체 part, Series 장르 ID, FanTalk 설명, 문서 상태 문제로 공통 경계 변경 불필요 |
|
||||
|
||||
### finding 및 plan 전환
|
||||
|
||||
- 신규 Phase 1 finding 없음.
|
||||
- Phase 1 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 1은 요구사항과 일치하며 추가 수정이 없다.
|
||||
|
||||
**남은 항목:** Phase 2~7 후속 Goal 완료 뒤 `P7-R10` 통합 재판정에 공통 오류·인가 근거로 참여한다.
|
||||
|
||||
## 16. 9차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD 공통 인가·target resolver·오류·CORS 요구사항, OpenAPI 공통 security
|
||||
- 검토 범위: security matcher, JWT/현재 DB role 이중 인가, target resolver, 신규 prefix 오류·CORS 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN` 검증 경계 유지 |
|
||||
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 creator member 해석 경로 유지 |
|
||||
| 오류·CORS | 충족 | 신규 prefix 전용 오류 envelope/i18n과 승인 Origin 경계 유지 |
|
||||
| 신규 finding | 없음 | Phase 3 preview 검증 회귀는 공통 security/target 경계 변경 없이 소유 Phase에서 수정 가능 |
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Phase 1 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 1 추가 수정 없음.
|
||||
|
||||
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
|
||||
|
||||
## 17. 10차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature A, OpenAPI 공통 security/error 계약
|
||||
- 검토 범위: 신규 prefix security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 오류·CORS 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`의 이중 인가가 신규 prefix보다 먼저 적용된다.
|
||||
- target resolver는 `ChatCharacter.creatorMember`의 `CREATOR + AI_CHARACTER` 불변식을 유지한다.
|
||||
- prefix 전용 오류 envelope/i18n, 405 `Allow`, 415 `Accept`, 승인 Origin과 공유 로그인·로그아웃 CORS 경계가 유지된다.
|
||||
- 신규 확정 finding이 없어 Phase 1 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 1 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
@@ -9,7 +9,7 @@
|
||||
| 리뷰 일자 | 2026-07-27 |
|
||||
| 리뷰어 | Sisyphus |
|
||||
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
|
||||
| 리뷰 상태 | 판정 완료 |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
@@ -581,3 +581,433 @@ Endpoint Contract Summary를 source of truth로 사용하는 client는 제공
|
||||
**최종 결론:** Phase 2 6차 리뷰 종결
|
||||
|
||||
**남은 항목:** `P3-R7` → `P3-R8` → `P3-R5-GATE`. Phase 4는 진행하지 않는다.
|
||||
|
||||
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 검증 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
|
||||
- 기준 문서: PRD Feature B, `plan-task.md`, `api-contract.openapi.json`
|
||||
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
|
||||
- 검증 방식: controller/facade/mapper/test 호출을 정적으로 추적했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### 추가 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-021` | Low | 처리 완료 | mutation에서 사용하지 않는 전체 response mapping 수행 | `Task 2.12` | `P2-R6` |
|
||||
|
||||
### REV-021 — mutation의 미사용 response mapping
|
||||
|
||||
- **심각도:** Low
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature B, 레거시 mutation 성공 응답 유지
|
||||
- **관련 계약:** Character POST/PUT의 성공 `data`는 `null`
|
||||
- **소유 Task:** `Task 2.12`, `P2-R6`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
controller는 create/update facade 반환값을 사용하지 않고 항상 `ApiResponse.ok(null)`을 반환한다. 그러나 facade는 두
|
||||
mutation 마지막에 전체 `AiCharacterAdminCharacterResponse`를 생성한다. 외부 계약에 필요 없는 객체 그래프 mapping이
|
||||
write 성공 뒤 추가 실패 지점과 유지보수 비용을 만든다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `AiCharacterAdminCharacterController.kt:34`~`50`은 facade 호출 뒤 exact `data: null`을 반환한다.
|
||||
- 코드: `AiCharacterAdminCharacterFacade.kt:62`, `:116`은 response DTO 반환형을 선언한다.
|
||||
- 코드: 같은 파일 `:112`, `:163`은 controller가 버리는 `characterMapper.toResponse(...)`를 수행한다.
|
||||
- 계약: OpenAPI Character mutation은 `NullSuccess`를 사용한다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
`P2-R6`에서 create/update facade 반환형을 `Unit`으로 축소하고 두 mapper 호출만 제거한다. controller와 공개 schema,
|
||||
business pipeline은 변경하지 않고 기존 mutation exact `data: null` 회귀로 동작 불변을 확인한다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 2에 `Task 2.12` / `P2-R6`과 `P2-R6-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
|
||||
|
||||
### 7차 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 문서·구현 대조 | 충족 | Character 4개 operation과 controller/facade 정적 추적 |
|
||||
| 후보 판정 | 충족 | `REV-021` 확정 |
|
||||
| plan 반영 | 충족 | `Task 2.12`, `P2-R6`, `P2-R6-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
|
||||
|
||||
**최종 결론:** 수정 goal 필요
|
||||
|
||||
**남은 항목:** `P2-R6` 실행 후 `P2-R6-GATE`에서 Phase 2를 재판정한다.
|
||||
|
||||
### P2-R6-GATE 종료 판정 — 2026-07-28
|
||||
|
||||
- 무엇을: `REV-021`의 캐릭터 POST/PUT 미사용 response mapping 제거를 최종 판정했다.
|
||||
- 왜: mutation 성공 응답은 `data: null`인데 facade가 controller가 사용하지 않는 상세 DTO를 만들고 있었기 때문이다.
|
||||
- 어떻게: `AiCharacterAdminCharacterFacade.create/update` 반환형을 `Unit`으로 축소하고 마지막 `characterMapper.toResponse(...)` 호출만 제거했다. 이후 mutation focused, character/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
|
||||
- 결과: mutation focused는 `BUILD SUCCESSFUL in 1m 54s`, character/common 회귀는 `BUILD SUCCESSFUL in 1m 57s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력 없음이었다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| `REV-021` 처리 | 충족 | facade 반환형 축소와 미사용 mapper 호출 제거 |
|
||||
| actual endpoint 회귀 | 충족 | POST/PUT mutation `data: null` focused test 성공 |
|
||||
| 영향 범위 회귀 | 충족 | character/common 회귀, lint, diff check 성공 |
|
||||
| 범위 준수 | 충족 | business pipeline, controller response, schema 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 2 7차 리뷰 종결
|
||||
|
||||
**남은 항목:** `P3-R9` 실행 후 `P3-R9-GATE`에서 Phase 3을 재판정한다.
|
||||
|
||||
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
|
||||
- 검토 범위: controller/facade/DTO/mapper, 외부 API·S3·DB/event 순서와 관련 actual endpoint 테스트
|
||||
- 검증 방식: 정적 호출·schema 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | 충족 | Character 4개 mapping과 OpenAPI request/response 형태 일치 |
|
||||
| target/ownership | 충족 | detail/update가 공통 resolver와 character target을 사용 |
|
||||
| mutation 응답 | 충족 | POST/PUT `data: null`, 미사용 상세 mapping 제거 상태 유지 |
|
||||
| 외부 부작용 경계 | 신규 finding 없음 | 기존 선검증·rollback/비보상 결정과 테스트 존재 |
|
||||
| plan 전환 | 해당 없음 | Phase 2 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 2 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음. `P7-R2` 통합 재판정에서 기존 Character/common 회귀만 확인한다.
|
||||
|
||||
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
|
||||
- 검토 범위: controller/facade/mapper, multipart 생성, optional update field와 레거시 controller/service parity
|
||||
- 검증 방식: 정적 호출·schema·테스트 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항
|
||||
|
||||
#### `REV-034` — Medium — 캐릭터 생성의 빈 필수 image 허용
|
||||
|
||||
- OpenAPI `CharacterCreateMultipart`는 `image`를 required로 선언한다.
|
||||
- controller는 non-null part 존재까지만 강제하고, facade는 `image?.isEmpty == false`일 때만 upload한다.
|
||||
- 빈 part는 외부 API 생성과 DB 저장을 먼저 수행한 뒤 image upload를 건너뛰므로 이미지 없는 캐릭터와 관련 부작용을 남긴다.
|
||||
- 기존 mutation 테스트는 image part 누락은 다루지만 빈 part를 다루지 않는다.
|
||||
|
||||
**권장 조치:** facade 진입 직후 빈 image를 400 `common.error.invalid_request`로 거부하고 외부 API, DB, S3,
|
||||
event 0회를 actual endpoint로 고정한다.
|
||||
|
||||
#### `REV-035` — Medium — `isActive=true` 단독 수정의 레거시 parity 위반
|
||||
|
||||
- OpenAPI `CharacterUpdateRequest.isActive`는 nullable boolean이며 `true`도 유효하다.
|
||||
- 레거시 `AdminChatCharacterController.hasChanges`는 `isActive != null`을 변경 요청으로 인정해 200
|
||||
`data: null` pipeline을 수행한다.
|
||||
- v2 mapper는 `request.isActive == false`만 external change로 인정하므로 `{"isActive":true}` 단독 요청이
|
||||
facade no-change guard에서 400이 된다.
|
||||
|
||||
**권장 조치:** non-null `isActive`를 변경 요청으로 인정하되 기존 `false` 비활성화 의미와 일반 update pipeline은
|
||||
변경하지 않는다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 2에 `Task 2.13` / `P2-R7`과 `P2-R7-GATE`를 추가했다. 두 finding은 같은 facade/mapper와
|
||||
mutation test 범위이므로 하나의 최소 보완 Task로 묶었다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | route 유지 | Character 4개 mapping은 유지되나 두 runtime 의미 불일치 존재 |
|
||||
| multipart create | 수정 필요 | 빈 required image가 외부·DB 부작용 뒤 무시됨 |
|
||||
| update parity | 수정 필요 | `isActive=true` 단독 요청이 legacy와 다른 400 |
|
||||
| plan 반영 | 충족 | `Task 2.13`, `P2-R7`, `P2-R7-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
|
||||
|
||||
**최종 결론:** Phase 2 후속 수정 필요
|
||||
|
||||
**남은 항목:** `P2-R7` → `P2-R7-GATE`.
|
||||
|
||||
## 18. 9차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-034`, `REV-035`를 처리했다.
|
||||
- 왜: 빈 필수 `image`가 외부 생성·DB 저장 뒤 무시되고, `isActive=true` 단독 수정이 레거시와 달리 400으로 거부됐기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminCharacterControllerMutationTest`에 빈 image POST 400/no-side-effect와 `isActive=true` PUT 200 `data:null` actual endpoint 테스트를 추가했다. focused 실행에서 신규 2건이 실패했다.
|
||||
- GREEN: create facade가 empty image를 400으로 거부하고, mapper가 non-null `isActive`를 변경 요청으로 인정하도록 최소 수정했다.
|
||||
- 검증: focused mutation test, `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, 전체 `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
|
||||
- 결과: `REV-034`, `REV-035` 처리 완료. 빈 image는 외부 API·DB·S3·event 전에 400으로 종료되고, `isActive=true` 단독 PUT은 200 `data:null`로 통과한다.
|
||||
|
||||
**최종 결론:** Phase 2 9차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
|
||||
- 검토 범위: 목록·상세·생성·수정 controller/facade/mapper, 레거시 parity와 최신 `REV-034`~`REV-035` 보완
|
||||
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
Character runtime의 신규 확정 finding은 없다. 빈 필수 image는 외부·DB·S3·event 전에 거부되고,
|
||||
`isActive=true` 단독 수정은 레거시 200 `data: null` 의미를 유지한다.
|
||||
|
||||
`Task 2.13` 헤더가 `[ ]`로 남은 문제는 기능 문제가 아닌 전체 완료 상태 기록 불일치였고,
|
||||
Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | 충족 | Character 4개 route와 OpenAPI field·multipart 경계 일치 |
|
||||
| ownership/인가 | 충족 | 목록 외 target route와 공통 ADMIN 이중 인가 유지 |
|
||||
| mutation parity | 충족 | create/update/soft delete와 최신 empty/no-op 보완 유지 |
|
||||
| Phase 2 기능 Task | 해당 없음 | 신규 production 수정 불필요 |
|
||||
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
|
||||
|
||||
**최종 결론:** Phase 2 기능 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 확인된 문제
|
||||
|
||||
#### `REV-040` — 필수 관계 정수의 누락·null이 기본값으로 보정될 수 있음
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **계약:** OpenAPI `CharacterRelationship`은 `importance`를 required non-null integer로 정의한다.
|
||||
- **구현:** `ChatCharacterRelationshipRequest.importance`는 Kotlin `Int`이고, v2 character facade의 strict reader는
|
||||
`FAIL_ON_UNKNOWN_PROPERTIES`와 `FAIL_ON_NULL_FOR_PRIMITIVES`를 활성화한다.
|
||||
- **근거:** 사용 중인 Jackson Kotlin/databind 2.13.5에서 `FAIL_ON_NULL_FOR_PRIMITIVES` 기본값은 비활성화되어
|
||||
Kotlin/JVM primitive의 누락·null이 `0`으로 역직렬화될 수 있다.
|
||||
- **영향:** 잘못된 관계 입력이 400으로 거부되지 않고 외부 캐릭터 생성·DB mutation으로 이어질 수 있다.
|
||||
|
||||
### 보완 계획
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 2.14` / `P2-R8` |
|
||||
| Gate | `P2-R8-GATE` |
|
||||
| RED | `importance` 누락·null actual POST와 외부 API·DB·S3·event 결과 |
|
||||
| GREEN | v2 생성 경계의 primitive null/누락 400 변환 완료 |
|
||||
| 범위 제한 | 전역 mapper·레거시 DTO·OpenAPI 변경 없음 |
|
||||
|
||||
### 처리 결과
|
||||
|
||||
- `AiCharacterAdminCharacterControllerMutationTest`에 관계 `importance` 누락·null actual POST와 외부 API·DB·S3·event no-side-effect 회귀를 추가했다.
|
||||
- `AiCharacterAdminCharacterFacade.readRequest()`에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가해 전역 mapper·레거시 DTO·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
|
||||
- RED: 신규 2건은 보완 전 `status().isBadRequest` 기대에서 실패했다.
|
||||
- GREEN/GATE: 보완 후 focused, character/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
|
||||
**최종 결론:** Phase 2는 `REV-040` 처리 완료
|
||||
|
||||
**다음 Goal:** `P3-R12`.
|
||||
|
||||
## 21. 등록 참조 API 후속 검토 — 2026-07-29
|
||||
|
||||
### 확인 결과
|
||||
|
||||
- **`REV-044` / High / 구현 대기:** 캐릭터 등록용 원작 검색은 레거시
|
||||
`AdminOriginalWorkController.search`와 `AdminOriginalWorkService.searchOriginalWorksAll`에 존재하지만 신규 v2
|
||||
캐릭터 관리자 route에는 없다.
|
||||
- 검색 계약은 필수 `searchTerm`, 제목·콘텐츠 타입·카테고리 부분 검색, soft delete 제외, 무페이징
|
||||
`OriginalWorkResponse` 직접 배열로 확정됐다.
|
||||
- target 없는 reference endpoint지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 경계는 동일하게 적용한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 2.15` / `P2-R9`
|
||||
- Gate: `P2-R9-GATE`
|
||||
- 범위 밖: 원작 CRUD, pagination·정렬 추가, 레거시 endpoint 변경
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 2 원작 검색 구현 필요
|
||||
|
||||
**다음 Goal:** `P2-R9`.
|
||||
|
||||
## 22. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature B, OpenAPI Character 5개 operation
|
||||
- 검토 범위: Character controller/facade/strict request reader, 원작 검색, 외부 API·S3·DB 선검증 경계
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### 확인 결과
|
||||
|
||||
| 항목 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| route/operation | 충족 | Character 5개 OpenAPI operation과 실제 mapping 유지 |
|
||||
| request 전체 media type | 충족 | 생성·수정 mapping은 multipart/form-data로 제한 |
|
||||
| pagination/reference query | 충족 | 목록 기본값과 원작 검색 필수 `searchTerm` 계약 일치 |
|
||||
| mutation 경계 | 충족 | strict JSON, 필수 image, target/원작 검증과 기존 외부/S3/DB 순서 유지 |
|
||||
|
||||
### `REV-055` — High — 처리 완료
|
||||
|
||||
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
|
||||
- controller는 기존 `@RequestPart("request") request: String` strict reader 전달을 유지하면서 multipart part header에서
|
||||
`application/json` 호환 여부를 확인한다.
|
||||
- `text/plain`과 content type 누락은 `HttpMediaTypeNotSupportedException`으로 공통 415 오류 계약에 연결해
|
||||
localized `ApiResponse.error`와 `Accept: application/json`을 반환한다.
|
||||
- POST·PUT actual endpoint의 KO/EN/JA 12개 case는 external API·S3·DB·event 부작용 없이 415를 반환하고,
|
||||
request part 누락은 기존 400으로 유지한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 2.16` / `P2-R10`
|
||||
- Gate: `P2-R10-GATE`
|
||||
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
|
||||
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
|
||||
external/S3/DB/event no-side-effect
|
||||
|
||||
### `P2-R10` / `P2-R10-GATE` 완료 판정 — 2026-07-29
|
||||
|
||||
- RED: focused mutation test에서 새 `text/plain`·content type 누락 POST·PUT 12개 415 기대 case가 수정 전 실패했다.
|
||||
- GREEN: controller의 part-level JSON compatibility 확인 뒤 기존 String payload를 facade strict reader에 그대로 전달했다.
|
||||
- GATE: focused mutation test는 `BUILD SUCCESSFUL in 31s`, Character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 18s`, `git diff --check`는 출력 없이 성공했다.
|
||||
- 전체 `./gradlew test`는 controller part 경계와 직접 영향 Character/common 회귀를 실행했으므로 생략했다.
|
||||
|
||||
**최종 결론:** `REV-055` 처리 완료, Phase 2 완료.
|
||||
|
||||
**다음 Goal:** `P3-R15`.
|
||||
|
||||
## 23. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature B, OpenAPI `CharacterCreateMultipart`·`CharacterUpdateMultipart`
|
||||
- 검토 범위: Character POST·PUT controller의 multipart binding·part media type 검사와 mutation 테스트
|
||||
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### `REV-060` — Medium — 미정의 multipart part를 무시하고 mutation 진행
|
||||
|
||||
- OpenAPI의 두 Character multipart schema는 `additionalProperties: false`이고 허용 이름을 `image`, `request`로
|
||||
한정한다.
|
||||
- controller는 선언된 `@RequestPart`를 binding하고 `request`의 `application/json` 여부만 확인한다.
|
||||
`MultipartHttpServletRequest`의 전체 part 이름 집합은 검사하지 않는다.
|
||||
- 따라서 정상 `image`·`request`와 `unexpected` part를 함께 보내도 추가 part는 무시되고 create/update facade가
|
||||
실행될 수 있다.
|
||||
- 잘못된 입력이 성공 mutation으로 이어지므로 계약 정합성 문제로 확정하되, 추가 part 자체를 사용하거나 저장하지는
|
||||
않으므로 심각도는 Medium으로 판정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 2.17` / `P2-R11` |
|
||||
| Gate | `P2-R11-GATE` |
|
||||
| RED | POST·PUT의 미정의 file/text part와 외부 API·S3·DB·event 결과 |
|
||||
| GREEN | 실제 part 이름을 `{image, request}`와 비교해 초과 이름을 공통 400으로 거부 |
|
||||
| 범위 제한 | OpenAPI·legacy/public·전역 multipart resolver 변경 없음 |
|
||||
|
||||
### `P2-R11` / `P2-R11-GATE` 처리 결과 — 2026-07-29
|
||||
|
||||
- RED: `AiCharacterAdminCharacterControllerMutationTest`에 Character POST·PUT의 `unexpected` multipart part KO/EN/JA 400/no-side-effect actual endpoint test를 추가했고, 기존 구현은 6개 invocation 모두 400 기대 대비 200으로 실패했다.
|
||||
- GREEN: `AiCharacterAdminCharacterController`에서 `MultipartHttpServletRequest.fileMap.keys`가 `{image, request}`의 부분집합인지 확인하고 초과 part를 400 `common.error.invalid_request`로 거부했다.
|
||||
- Gate: undefined part, 기존 request part 415, request part 누락 400 focused 회귀는 `BUILD SUCCESSFUL in 1m 41s`, Character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 36s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 30s`, `git diff --check`는 출력 없이 성공했다.
|
||||
|
||||
**최종 결론:** `REV-060` 처리 완료, Phase 2 13차 리뷰 종결.
|
||||
|
||||
**다음 Goal:** `P3-R17`.
|
||||
|
||||
## 24. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: OpenAPI `CharacterCreateMultipart`·`CharacterUpdateMultipart`의
|
||||
`additionalProperties: false`, 허용 part `{image, request}`
|
||||
- 검토 범위: Character POST·PUT controller의 part allow-list와 미정의 part 회귀 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-065` — Medium — 일반 form-field multipart part가 allow-list 우회
|
||||
|
||||
- `AiCharacterAdminCharacterController.kt:71-74`는 `MultipartHttpServletRequest.fileMap.keys`만 검사한다.
|
||||
filename 없는 일반 form-field part는 file map 대상이 아니므로 `{image, request}` 외 이름을 검출하지 못한다.
|
||||
- 로컬 의존성 Spring Web 5.3.29의 `StandardMultipartHttpServletRequest.parseRequest` bytecode도 filename이 있는
|
||||
part만 multipart file map에 넣고, filename이 없는 part 이름은 별도 parameter 집합에 넣음을 확인했다.
|
||||
- 기존 `AiCharacterAdminCharacterControllerMutationTest.kt:424-469`는 filename이 있는
|
||||
`MockMultipartFile("unexpected", ...)`만 사용해 이 경계를 고정하지 않는다.
|
||||
- 따라서 `additionalProperties: false` 계약을 일반 form-field part가 우회해 facade mutation으로 진행할 수 있다.
|
||||
입력 자체를 저장하지는 않지만 계약 위반과 부작용 가능성이 있어 Medium으로 판정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 2.18` / `P2-R12` |
|
||||
| Gate | `P2-R12-GATE` |
|
||||
| RED | filename 없는 `unexpected` part의 POST·PUT 400/no-side-effect |
|
||||
| GREEN | servlet 전체 part 이름을 `{image, request}`와 비교 |
|
||||
| 범위 제한 | Character controller/test만 최소 변경, OpenAPI·전역 resolver·legacy/public 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 2 보완 필요 — `REV-065` 확정
|
||||
|
||||
**다음 Goal:** `P2-R12`.
|
||||
|
||||
## 24. 8차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-065`의 Character POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
|
||||
- 왜: 기존 `fileMap.keys` 검사만으로는 `{image, request}` 외 일반 form-field part를 mutation 전 거부하지 못했기 때문이다.
|
||||
- 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 `shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects` KO/EN/JA POST·PUT actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하게 했다.
|
||||
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
|
||||
|
||||
**최종 결론:** `REV-065` 처리 완료. Phase 2 후속 Gate 완료.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 25. 15차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature C, OpenAPI Character 5개 operation
|
||||
- 검토 범위: 목록·원작 검색·상세·생성·수정, target/owner, multipart·strict JSON, external/S3/event 순서
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- Character 5개 OpenAPI operation과 controller mapping, target/owner 선검증, strict multipart/JSON 경계를 대조했다.
|
||||
- 기존 완료 finding 이후 새 계약 불일치나 확정 가능한 production 결함은 확인되지 않았다.
|
||||
- Phase 3의 `REV-072`는 Character 경로에 영향을 주지 않는다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Phase 2 finding 및 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 2 추가 수정 없음.
|
||||
|
||||
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
|
||||
|
||||
## 26. 16차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature B, OpenAPI Character 5개 operation
|
||||
- 검토 범위: 목록·원작 검색·상세·생성·수정, AI target, strict multipart/JSON, external API·S3·DB·event 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- Character 5개 operation과 controller mapping, active AI 목록 필터와 target 없는 원작 검색 계약이 일치한다.
|
||||
- 생성·수정은 exact multipart part와 JSON media type, 필수/nullable·미지 필드, 원작·타입·중복 선검증을 유지한다.
|
||||
- external API, S3, DB와 event의 기존 호출·rollback/비보상 경계가 계획의 특성화 결과와 일치한다.
|
||||
- 신규 확정 finding이 없어 Phase 2 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 2 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
| 리뷰 일자 | 2026-07-27 |
|
||||
| 리뷰어 | Sisyphus |
|
||||
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
|
||||
| 리뷰 상태 | 판정 완료 |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
@@ -726,3 +726,622 @@ production code는 변경하지 않는다.
|
||||
**최종 결론:** Phase 3 6차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음. 다음은 사용자 진행 지시 후 `P4-T1`이다.
|
||||
|
||||
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 검증 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
|
||||
- 기준 문서: PRD Feature C, `plan-task.md`, `api-contract.openapi.json`
|
||||
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
|
||||
- 검증 방식: production/test 전체 호출 검색과 repository/DTO 정적 추적을 수행했다. 사용자 요청에 따라 Gradle,
|
||||
컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### 추가 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-022` | Low | 처리 완료 | 관리자 content repository에 호출되지 않는 확장 코드 잔존 | `Task 3.19` | `P3-R9` |
|
||||
|
||||
### REV-022 — 호출되지 않는 repository 확장과 전용 enum
|
||||
|
||||
- **심각도:** Low
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature C
|
||||
- **관련 계약:** 현재 5개 AudioContent operation의 실제 조회/생성/수정 경계
|
||||
- **소유 Task:** `Task 3.19`, `P3-R9`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
현재 facade가 사용하는 관리자 전용 repository method는 owner-scoped 상세 조회
|
||||
`findByIdAndCreatorMemberId` 하나다. 동일 repository의 page 조회, series ID 조회/교체, 활성 series 검사와 private helper는
|
||||
production/test 호출자가 없고, `AiCharacterAdminAudioContentStatus`도 이 미사용 코드에서만 참조된다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `AiCharacterAdminAudioContentRepository.kt:50`~`56`의 owner-scoped 상세 조회는 실제 facade 호출 대상이다.
|
||||
- 코드: 같은 파일 `:25`~`:48`, `:58`~`:142`의 나머지 public/private method는 전체 호출 검색 결과 외부 참조가 없다.
|
||||
- 코드: `AiCharacterAdminAudioContentDto.kt:28`~`31`의 status enum은 위 미사용 page query에서만 참조된다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
`P3-R9`에서 실제 사용 중인 상세 조회만 보존하고 호출 0건 method, helper, enum 및 그로 인해 unused가 된 import만 제거한다.
|
||||
legacy repository나 콘텐츠/series 동작은 변경하지 않고 상세·ownership 회귀로 동작 불변을 확인한다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 3에 `Task 3.19` / `P3-R9`과 `P3-R9-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
|
||||
|
||||
### 7차 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 문서·구현 대조 | 충족 | AudioContent 5개 operation과 facade/repository 호출 정적 추적 |
|
||||
| 후보 판정 | 충족 | `REV-022` 확정 |
|
||||
| plan 반영 | 충족 | `Task 3.19`, `P3-R9`, `P3-R9-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
|
||||
|
||||
**최종 결론:** 수정 goal 필요
|
||||
|
||||
**남은 항목:** `P2-R6-GATE` 후 `P3-R9`를 실행하고 `P3-R9-GATE`에서 Phase 3을 재판정한다.
|
||||
|
||||
### P3-R9-GATE 종료 판정 — 2026-07-28
|
||||
|
||||
- 무엇을: `REV-022`의 관리자 오디오 repository 미사용 확장 제거를 최종 판정했다.
|
||||
- 왜: 실제 facade 호출 대상은 owner-scoped 상세 조회 하나뿐이고, 나머지 page/series helper와 전용 status enum은 현재 5개 AudioContent operation에 쓰이지 않기 때문이다.
|
||||
- 어떻게: `AiCharacterAdminAudioContentRepository`에서 상세 조회 외 method와 helper를 제거하고 `AiCharacterAdminAudioContentStatus`를 삭제했다. package-scoped 호출 검색, 상세·ownership focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
|
||||
- 결과: 대상 package 호출 검색은 출력이 없었다. 상세·ownership focused는 `BUILD SUCCESSFUL in 3m 39s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력 없음이었다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| `REV-022` 처리 | 충족 | 미사용 repository method/helper/status enum 제거 |
|
||||
| static 호출 검색 | 충족 | 대상 admin content package에서 제거 대상 호출 0건 |
|
||||
| owner-scoped 상세 회귀 | 충족 | detail/ownership focused test 성공 |
|
||||
| 영향 범위 회귀 | 충족 | content/common 회귀, lint, diff check 성공 |
|
||||
| 범위 준수 | 충족 | legacy repository/service, 콘텐츠·시리즈 동작 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 3 7차 리뷰 종결
|
||||
|
||||
**남은 항목:** `P4-R1` 실행 후 `P4-R1-GATE`에서 Phase 4를 재판정한다.
|
||||
|
||||
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 검증 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature C·API Expectations, plan Phase 3, OpenAPI AudioContent 5개 operation
|
||||
- 검토 범위: 생성 request strict parse와 legacy `AudioContentService` 호출, Java time 변환, prefix 예외 handler,
|
||||
생성 actual endpoint 테스트
|
||||
- 검증 방식: 요청값 → facade → legacy service → 예외 handler와 side-effect 순서를 정적으로 역추적했다.
|
||||
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 추가 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-030` | Medium | 처리 완료 | 잘못된 생성 날짜·시간대가 client 오류가 아닌 500으로 반환됨 | `Task 3.20` | `P3-R10` |
|
||||
|
||||
### REV-030 — 생성 날짜·시간대 의미 오류가 500으로 분류됨
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature C, API Expectations의 request/domain 오류 400
|
||||
- **관련 계약:** `AudioContentCreateRequest.releaseDate`는 `yyyy-MM-dd HH:mm`, `timezone`은 시간대 ID
|
||||
- **소유 Task:** `Task 3.20`, `P3-R10`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
facade의 strict reader는 JSON 문법·타입·미지 필드만 검증하고 파싱한 request를 버린다. 이후 legacy service가
|
||||
`releaseDate`를 `LocalDateTime`으로 변환하고 `ZoneId.of(timezone)`을 호출한다. 잘못된 값은
|
||||
`DateTimeParseException`/`ZoneRulesException` 등 `DateTimeException`으로 빠지며, prefix handler는 이를 client 오류로
|
||||
분류하지 않아 500 `common.error.unknown`을 반환한다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `AiCharacterAdminAudioContentFacade.kt:65`~`:80`은 strict parse 뒤 같은 raw JSON을 legacy service에 전달한다.
|
||||
- 코드: `AudioContentService.kt:226`~`:230`에서 날짜 형식과 `ZoneId`를 변환한다.
|
||||
- 코드: `AiCharacterAdminExceptionHandler.kt:55`~`:71`은 `DateTimeException`을 400 분기에 포함하지 않는다.
|
||||
- 계약: OpenAPI `AudioContentCreateRequest`의 `releaseDate` 설명과 기본 `timezone`.
|
||||
- 계획: 기존 `Task 3.13`은 invalid date 400 증거를 완료 조건으로 적었지만 실제 테스트는 현재 계약 밖
|
||||
`releaseDateUtc` 미지 필드만 검증한다.
|
||||
- 테스트: 현재 생성 테스트에는 유효한 날짜·시간대와 제거된 `releaseDateUtc`만 있고 실제 `releaseDate` 형식·`timezone`
|
||||
의미 오류가 없다.
|
||||
|
||||
**정적 재현 절차**
|
||||
|
||||
1. 유효한 `coverImage`, `contentFile`, theme과 필수 JSON field를 준비한다.
|
||||
2. `releaseDate="not-a-date"` 또는 `timezone="Invalid/Zone"`으로 생성 요청을 보낸다.
|
||||
3. strict JSON parse는 통과하고 legacy Java time 변환이 예외를 던진다.
|
||||
4. 현재 handler 분류는 500이며 요구 결과는 side effect 없는 400 `common.error.invalid_request`다.
|
||||
|
||||
**영향**
|
||||
|
||||
형식상 JSON은 맞지만 의미가 잘못된 client 입력이 서버 장애로 기록·응답된다. 실제 업로드 이전에 실패하므로 현재 경로의
|
||||
DB/S3/event 변경 가능성은 낮지만, 오류 계약과 운영 장애 지표가 왜곡된다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
공통 handler를 넓히지 말고 facade가 strict parse한 생성 DTO를 재사용해 날짜 형식과 `ZoneId`만 legacy 호출 전에 검증한다.
|
||||
두 입력의 KO/EN/JA 400과 DB/S3/event 0건을 actual endpoint로 고정한다.
|
||||
|
||||
**판정 기록**
|
||||
|
||||
- 2026-07-28 — 코드·OpenAPI·handler·테스트 정적 추적으로 확정. 테스트는 사용자 요청에 따라 미실행.
|
||||
- 2026-07-28 — `P3-R10`에서 `AiCharacterAdminAudioContentCreateTest`에 `releaseDate="not-a-date"`와
|
||||
`timezone="Invalid/Zone"`의 KO/EN/JA actual endpoint matrix를 추가했다. RED는 신규 6건이 400 기대 assertion에서 실패했고,
|
||||
facade가 strict parse 결과의 `releaseDate`/`timezone`을 legacy service 호출 전에 Java time API로 검증하도록 수정한 뒤
|
||||
create focused, create+controller focused, content/common 영향 범위 회귀와 `ktlintCheck`가 모두 성공했다. production 변경은
|
||||
v2 audio content facade 경계에 한정했고 OpenAPI schema와 legacy `AudioContentService`는 변경하지 않았다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 3에 `Task 3.20` / `P3-R10`과 `P3-R10-GATE`를 추가했다. 기존 완료 Task/Gate는 유지한다.
|
||||
|
||||
### 8차 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema 대조 | 충족 | AudioContent 5개 mapping과 OpenAPI schema 유지 |
|
||||
| 후보 판정 | 충족 | `REV-030` 원인·오류 분류·테스트 공백 확인 및 처리 완료 |
|
||||
| plan 반영 | 충족 | `Task 3.20`, `P3-R10`, `P3-R10-GATE` |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
|
||||
|
||||
**최종 결론:** `REV-030` 처리 완료, Phase 3 8차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음. 다음은 `P4-R2` 실행 후 Phase 4를 재판정한다.
|
||||
|
||||
### P3-R10-GATE 종료 판정 — 2026-07-28
|
||||
|
||||
- 무엇을: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 400 `common.error.invalid_request`로 복구했다.
|
||||
- 왜: 형식상 JSON은 유효하지만 의미가 잘못된 client 입력이 legacy Java time 변환까지 내려가 500으로 반환되는 계약 위반을 막기 위해서다.
|
||||
- 어떻게: v2 facade에서 strict parse 결과를 재사용해 `yyyy-MM-dd HH:mm`과 `ZoneId`를 legacy service 호출 전에 검증했다.
|
||||
- 결과: RED는 신규 6건 실패로 재현됐고, GREEN 후 create focused는 `BUILD SUCCESSFUL in 1m 4s`, create+controller focused는
|
||||
`BUILD SUCCESSFUL in 1m 16s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`는
|
||||
`BUILD SUCCESSFUL in 38s`였다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| `REV-030` 처리 | 충족 | invalid `releaseDate`/`timezone` actual endpoint KO/EN/JA 400 추가 |
|
||||
| side-effect 차단 | 충족 | DB count, S3 putObject 0회, event no-interaction 단언 |
|
||||
| 영향 범위 회귀 | 충족 | content/common 회귀와 lint 성공 |
|
||||
| 범위 준수 | 충족 | OpenAPI schema, legacy service, upload/processing pipeline 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 3 8차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음. 다음은 `P4-R2`다.
|
||||
|
||||
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
|
||||
- 검토 범위: 상세 facade/mapper, 레거시 상세 response 파생 규칙, 예약일·locale·signed URL 테스트
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항
|
||||
|
||||
#### `REV-036` — High — 미래 예약 콘텐츠 상세의 releaseDate 소실
|
||||
|
||||
- OpenAPI 상세 response는 nullable `releaseDate`를 레거시 `GetAudioContentDetailResponse` 필드로 유지한다.
|
||||
- 레거시 `AudioContentService`는 미래 예약일을 UTC에서 Asia/Seoul로 변환하고
|
||||
`content.release_date.format`의 KO/EN/JA 형식 문자열을 반환하며, 공개 시각이 지나면 null을 반환한다.
|
||||
- v2 `AiCharacterAdminAudioContentMapper.toResponse`는 콘텐츠 상태와 locale에 관계없이 `releaseDate = null`로
|
||||
고정한다.
|
||||
- 현재 상세 테스트의 예약일은 점검일보다 과거라 null 분기만 검증해 미래 분기 누락을 발견하지 못한다.
|
||||
|
||||
**영향:** 예약 공개 전 관리자 상세에서 공개 예정 시각이 숨겨지고 레거시 response 의미와 OpenAPI 이관 원칙을 위반한다.
|
||||
|
||||
**권장 조치:** 기존 `SodaMessageSource`와 `LangContext`를 사용해 미래 여부, UTC→Asia/Seoul 변환, locale별 포맷을
|
||||
mapper에 최소 이관하고 미래·과거 KO/EN/JA actual endpoint를 고정한다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 3에 `Task 3.21` / `P3-R11`과 `P3-R11-GATE`를 추가했다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | route 유지 | AudioContent 5개 mapping과 response 필드는 존재 |
|
||||
| 미래 예약일 | 수정 필요 | mapper가 `releaseDate`를 무조건 null로 설정 |
|
||||
| 과거 예약일 | 충족 | null 반환은 레거시 의미와 일치 |
|
||||
| plan 반영 | 충족 | `Task 3.21`, `P3-R11`, `P3-R11-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
|
||||
|
||||
**최종 결론:** Phase 3 후속 수정 필요
|
||||
|
||||
**남은 항목:** `P2-R7-GATE` 후 `P3-R11` → `P3-R11-GATE`.
|
||||
|
||||
## 18. 9차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-036`을 처리했다.
|
||||
- 왜: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null이라 레거시 locale별 공개 예정 시각을 숨겼기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminAudioContentQueryTest`에 미래 예약일 KO/EN/JA와 과거 null actual endpoint 테스트를 추가했다. focused 실행에서 미래 3개 locale이 null 반환으로 실패했다.
|
||||
- GREEN: `AiCharacterAdminAudioContentMapper`가 `SodaMessageSource`, `LangContext`를 사용해 레거시 `content.release_date.format`과 UTC→Asia/Seoul 변환을 적용하도록 최소 수정했다.
|
||||
- 검증: focused query test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
|
||||
- 결과: `REV-036` 처리 완료. 미래 예약일은 KO/EN/JA 형식 문자열로 반환하고 과거 예약일은 null을 유지한다.
|
||||
|
||||
**최종 결론:** Phase 3 9차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
|
||||
- 검토 범위: 테마·목록·상세·생성·수정 facade/mapper/repository, signed URL과 최신 예약일 보완
|
||||
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
AudioContent runtime의 신규 확정 finding은 없다. owner-scoped 상세, 레거시 목록/생성/수정 DTO,
|
||||
빈 파일 경계, signed URL과 미래·과거 KO/EN/JA 예약일 의미가 유지된다.
|
||||
|
||||
`Task 3.21` 헤더가 `[ ]`로 남은 문제는 Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | 충족 | AudioContent 5개 route와 OpenAPI field·multipart 경계 일치 |
|
||||
| ownership | 충족 | target owner content만 상세·수정 가능 |
|
||||
| signed URL/private path | 충족 | 공통 CloudFront 정책 재사용과 private path 비노출 |
|
||||
| 예약 공개일 | 충족 | 미래 locale별 표시, 과거 null 유지 |
|
||||
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
|
||||
|
||||
**최종 결론:** Phase 3 기능 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 확인된 문제
|
||||
|
||||
#### `REV-041` — 오디오 생성 primitive의 required·null 계약 미강제
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **계약:** OpenAPI `AudioContentCreateRequest`는 `price`를 required non-null integer로 정의하고,
|
||||
`themeId`, 각 boolean primitive도 nullable로 선언하지 않는다.
|
||||
- **구현:** `CreateAudioContentRequest`의 해당 값은 Kotlin primitive이며, v2 content facade의 strict reader는
|
||||
`FAIL_ON_UNKNOWN_PROPERTIES`, `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 활성화한 뒤 원본 JSON을 legacy service로 전달한다.
|
||||
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 primitive 누락·null은 `0`/`false`로 보정될 수 있다.
|
||||
- **영향:** 필수 `price` 누락·null이 400 없이 생성으로 이어질 수 있고, `isFullDetailVisible` 같은 필드의 explicit null은
|
||||
문서·DTO의 생략 기본값과 다른 값으로 처리될 수 있다.
|
||||
|
||||
### 보완 계획
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 3.22` / `P3-R12` |
|
||||
| 시작 조건 | `P2-R8-GATE` |
|
||||
| Gate | `P3-R12-GATE` |
|
||||
| RED | required `price` 누락·null, non-null primitive null과 S3·DB·event 무변경 |
|
||||
| GREEN | v2 생성 경계의 primitive null/누락 400 변환과 optional 생략 기본값 유지 완료 |
|
||||
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
|
||||
|
||||
### 처리 결과
|
||||
|
||||
- `AiCharacterAdminAudioContentCreateTest`에 `price` 누락·null, primitive field explicit null actual POST와 파일 업로드·DB·event no-side-effect 회귀를 추가했다.
|
||||
- `AiCharacterAdminAudioContentFacade.readRequest()`에 `FAIL_ON_NULL_FOR_PRIMITIVES`와 `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가해 전역 mapper·레거시 service·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
|
||||
- RED: 신규 8개 invocation은 보완 전 `status().isBadRequest` 기대에서 실패했다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다.
|
||||
- GREEN/GATE: 보완 후 focused, content/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
|
||||
**최종 결론:** Phase 3는 `REV-041` 처리 완료
|
||||
|
||||
**다음 Goal:** `P4-R4`.
|
||||
|
||||
## 21. 오디오 콘텐츠 댓글 후속 검토 — 2026-07-29
|
||||
|
||||
### 확인 결과
|
||||
|
||||
- **`REV-045` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글
|
||||
조회/작성/수정/삭제 5개 operation이 없다.
|
||||
- 조회는 필수 `timezone`과 `page`, `size`, 레거시 `totalCount/items`를 유지한다.
|
||||
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 콘텐츠의 row를
|
||||
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
|
||||
- 답글 `parentId`는 같은 콘텐츠의 활성 원댓글이어야 한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 3.23` / `P3-R13`
|
||||
- Gate: `P3-R13-GATE`
|
||||
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 CRUD 구현 필요
|
||||
|
||||
**다음 Goal:** `P3-R13`.
|
||||
|
||||
## 22. 오디오 콘텐츠 댓글 구현 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-045`를 처리했다.
|
||||
- 왜: 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 5개 operation이 없었기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminAudioContentCommentTest`에 root/reply 조회, target AI 작성, target 작성 row 수정, owner 범위 row soft delete, 잘못된 parent/timezone/page/size/unknown field/cross-resource 계약 7건을 추가했고 미구현 route의 404/405로 실패했다.
|
||||
- GREEN: `AiCharacterAdminAudioContentController`에 5개 route를 추가하고, facade에서 target active owner content, 같은 콘텐츠의 활성 root parent, target 작성 수정 권한을 선검증한 뒤 기존 `AudioContentCommentService`를 재사용했다.
|
||||
- Gate: focused 댓글 테스트, content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
- 결과: `REV-045` 처리 완료. 삭제는 작성자와 관계없이 target 소유 콘텐츠의 해당 row만 soft delete하고 cascade하지 않으며, 이미 비활성인 row는 200 no-op을 유지한다.
|
||||
|
||||
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 후속 기능 종결
|
||||
|
||||
**다음 Goal:** `P4-R5`.
|
||||
|
||||
## 23. UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature C, OpenAPI 2.2.0 AudioContent 10개 operation, `DEC-UTC-DATE-001`
|
||||
- 검토 범위: 오디오 생성 request, 상세 GET, 댓글·답글 GET의 controller/facade/DTO/mapper/repository
|
||||
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-050` — High — 오디오 4개 operation의 timezone/UTC 계약 불일치
|
||||
|
||||
- 생성은 현재 레거시 `CreateAudioContentRequest`를 typealias로 사용해 `timezone` body와
|
||||
`yyyy-MM-dd HH:mm` 로컬 `releaseDate`를 받는다.
|
||||
- 상세 GET은 필수 `timezone` query를 받지만 facade에서 사용하지 않는다. 상세 `releaseDate`는 미래 예약일에
|
||||
UTC를 Asia/Seoul로 바꾼 locale 문자열이며 현재/과거는 null이다.
|
||||
- 댓글·답글 GET도 필수 `timezone` query를 받고 레거시 목록 service/repository의 표시 문자열을 반환한다.
|
||||
- 승인된 최신 계약은 생성 body와 세 GET에서 `timezone`을 제거하고, 생성의 nullable `releaseDate`, 상세의
|
||||
기존 nullable `releaseDate`, 댓글의 기존 `date`를 ISO-8601 UTC(`Z`)로 사용한다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| route 수 | 유지 | AudioContent 10개 operation 자체는 변경 없음 |
|
||||
| 생성 request | 처리 완료 | v2 전용 DTO가 `timezone`을 거부하고 UTC `releaseDate`만 내부 경계에 전달 |
|
||||
| 상세 response | 처리 완료 | query를 제거하고 미래 예약일만 UTC `Z`로 반환 |
|
||||
| 댓글·답글 response | 처리 완료 | `page`/`size`만 받고 기존 `date`를 UTC `Z`로 mapping |
|
||||
| legacy/public 격리 | 충족 | 기존 controller/service의 timezone 계약을 유지 |
|
||||
| OpenAPI 상태 | 처리 완료 | 영향 4개 operation을 `implemented`로 동기화 |
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
- 신규 Task: `Task 3.24` / `P3-R14`
|
||||
- Gate: `P3-R14-GATE`
|
||||
- 완료 조건: 생성·상세·댓글·답글 actual endpoint UTC exact JSON, 상세 기존 null/노출 조건과 댓글
|
||||
pagination/ownership 보존, legacy/public 회귀
|
||||
- 범위 밖: 오디오 목록 날짜, 로컬 시각+timezone 병행 지원, 신규 dependency·DDL
|
||||
|
||||
### `P3-R14` / `P3-R14-GATE` 처리 결과
|
||||
|
||||
- RED: create/query/comment focused actual endpoint 테스트는 48개 중 9개가 기존 `timezone` 필수와 legacy 날짜 포맷으로
|
||||
실패했다.
|
||||
- GREEN: v2 생성 DTO가 `timezone`을 미지 필드로 거부하고 UTC instant를 `LocalDateTime`으로 한 번 변환해 내부 생성
|
||||
overload로 전달한다. 상세와 root/reply 목록은 기존 null·pagination·ownership·filter 의미를 유지하면서 `releaseDate`와
|
||||
`date`만 `toUtcIso()`로 반환한다.
|
||||
- Gate: focused는 `BUILD SUCCESSFUL in 2m 13s`, parent 재실행은 `BUILD SUCCESSFUL in 52s`, content/common·legacy 영향
|
||||
범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 23s`였다. 당시 OpenAPI는 34개
|
||||
`implemented`, 2개 `alignment-required`였고, 후속 `P5-R6` 뒤 36개 모두 `implemented`로 통합됐다. `git diff --check`는
|
||||
출력이 없었다.
|
||||
|
||||
**최종 결론:** `REV-050` 처리 완료, Phase 3 UTC 계약 정합화 완료
|
||||
|
||||
**다음 Goal:** `P5-R6`.
|
||||
|
||||
## 24. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation과 공통 `Page`/`Size`
|
||||
- 검토 범위: audio controller/facade의 query binding·검증, 관련 댓글 actual endpoint 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-052` — High — 오디오 댓글·답글 목록의 optional pagination 계약 위반
|
||||
|
||||
- OpenAPI의 `Page`, `Size`는 `required: false`이고 각각 기본값 `0`, `20`이다.
|
||||
- 댓글 목록과 답글 목록 controller의 `page`, `size`에는 `defaultValue`가 없어 두 query를 생략하면 MVC binding
|
||||
단계에서 400이 된다.
|
||||
- facade의 `validateCommentQuery()`도 실제 query 이름 집합이 정확히 `page`, `size` 두 개일 때만 허용하므로,
|
||||
controller 기본값만 추가해도 전체 또는 부분 생략 요청을 거부한다.
|
||||
- 현재 댓글 테스트는 query 전체 생략을 400으로 기대해 계약 불일치를 회귀로 고정하고 있다.
|
||||
- 영향은 두 GET의 정상 요청 가용성에 직접 미치므로 High로 판정한다.
|
||||
|
||||
### 검증 근거
|
||||
|
||||
| 근거 | 확인 내용 |
|
||||
|---|---|
|
||||
| OpenAPI | 두 GET이 공통 optional `Page`/`Size`를 참조 |
|
||||
| controller | 댓글·답글 모두 기본값 없는 `@RequestParam page`, `size` |
|
||||
| facade | query 이름의 부분집합이 아니라 정확한 집합 일치 요구 |
|
||||
| test | query 전체 생략 요청을 400으로 기대 |
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 3.25` / `P3-R15`
|
||||
- Gate: `P3-R15-GATE`
|
||||
- 최소 수정: 두 controller query 기본값과 facade의 미지 query 거부 조건만 정합화
|
||||
- 완료 조건: 전체·부분 생략 200/default, 범위 오류·미지 query 400, 기존 UTC/ownership/pagination 회귀
|
||||
- 범위 밖: OpenAPI·legacy/public API·FanTalk query policy 변경
|
||||
|
||||
### `P3-R15` / `P3-R15-GATE` 처리 결과 — 2026-07-29
|
||||
|
||||
- RED: `AiCharacterAdminAudioContentCommentTest` 9건 중 댓글·답글 전체 생략 테스트 2건이 `isOk` 기대에서 실패해 `BUILD FAILED in 45s`였다.
|
||||
- GREEN: 두 controller 목록의 `page`, `size`에 각각 `0`, `20` 기본값을 적용하고, facade는 `page`, `size`의 부분집합만 허용해 미지 query·음수 page·1 미만 size의 기존 400 `ApiResponse.error` 경계를 유지했다.
|
||||
- Gate: focused는 `BUILD SUCCESSFUL in 40s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 controller/facade와 actual endpoint 변경을 포함하므로 실행하지 않았다.
|
||||
|
||||
**현재 결론:** `REV-052` 처리 완료. Phase 3 완료 판정에는 별도 `REV-056` / `P3-R16` 보완이 남아 있다.
|
||||
|
||||
### `REV-056` — High — AudioContent multipart request part의 JSON media type 미강제
|
||||
|
||||
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
|
||||
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
|
||||
- 실제 `AiCharacterAdminAudioContentControllerTest`와 update 테스트는 유효 JSON을 `text/plain` request part로
|
||||
보내 200을 기대하므로 계약 불일치가 실행 테스트에도 고정돼 있다.
|
||||
- 기존 정상 strict parsing·file/series/UTC 의미를 유지하면서 part-level media type만 415로 차단해야 한다.
|
||||
|
||||
### 추가 plan 전환
|
||||
|
||||
- 신규 Task: `Task 3.26` / `P3-R16`
|
||||
- Gate: `P3-R16-GATE`
|
||||
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
|
||||
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
|
||||
S3/DB/processing/event no-side-effect
|
||||
|
||||
**최종 결론:** Phase 3은 `REV-052`, `REV-056` 수정 전 완료 판정 불가
|
||||
|
||||
**다음 Goal:** `P3-R15` (`P2-R10-GATE` 완료 후).
|
||||
|
||||
### `P3-R16` / `P3-R16-GATE` 처리 결과 — 2026-07-29
|
||||
|
||||
- RED: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentControllerTest`의 83건 중 text/plain·Content-Type 누락 415 기대 13건이 기존 200으로 실패해 `BUILD FAILED in 1m 15s`였다.
|
||||
- GREEN: Character `P2-R10`의 `MultipartHttpServletRequest` header 검사 패턴을 AudioContent POST·PUT controller에만 적용했다. 기존 JSON String strict reader와 facade는 변경하지 않았다.
|
||||
- Gate: POST·PUT actual endpoint는 KO/EN/JA의 text/plain·Content-Type 누락에 localized `ApiResponse.error` 415와 `Accept: application/json`, DB/S3/event 무변경을 확인했다. JSON 정상 경로, 생성 필수 part 400, UTC/file/series 회귀도 유지했다. focused는 `BUILD SUCCESSFUL in 56s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 16s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 변경 slice를 포함하므로 실행하지 않았다.
|
||||
|
||||
**처리 결과:** `REV-056` 처리 완료.
|
||||
|
||||
## 25. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature C, OpenAPI `AudioContentCreateMultipart`·`AudioContentUpdateMultipart`
|
||||
- 검토 범위: AudioContent POST·PUT controller의 multipart binding, 파일 교체 거부와 관련 mutation 테스트
|
||||
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### `REV-061` — Medium — operation별 허용 목록 밖 multipart part를 일반적으로 거부하지 않음
|
||||
|
||||
- OpenAPI는 생성에 `contentFile`, `coverImage`, `request`, 수정에 `coverImage`, `request`만 정의하고 두 schema 모두
|
||||
`additionalProperties: false`다.
|
||||
- controller는 전체 part 이름을 검사하지 않는다. 수정의 `audioFile`, `contentFile`만 별도 nullable 인자로 받아 facade에서
|
||||
거부하므로, `unexpected` 같은 다른 이름의 part는 무시된다.
|
||||
- 생성·수정 모두 정의되지 않은 part를 포함한 요청이 정상 mutation으로 진행될 수 있어 계약 위반을 확정했다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 3.27` / `P3-R17` |
|
||||
| Gate | `P3-R17-GATE` |
|
||||
| RED | 생성·수정 미정의 part와 S3·DB·processing/event 결과 |
|
||||
| GREEN | 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` exact allow-list |
|
||||
| 회귀 | 기존 `audioFile`·`contentFile` 수정 거부, 필수/빈 파일, request part 415 |
|
||||
|
||||
**처리 결과 (2026-07-29 / P3-R17):**
|
||||
|
||||
- AudioContent POST는 multipart part 이름을 `{contentFile, coverImage, request}`로 제한하고, PUT은 `{coverImage, request}`로 제한하도록 controller 경계에 allow-list를 추가했다.
|
||||
- PUT controller/facade의 `audioFile`, `contentFile` nullable 인자는 제거했고, 기존 파일 교체 거부는 동일한 미정의 part 검증 경계로 통합했다.
|
||||
- RED에서 생성·수정 `unexpected` part KO/EN/JA 테스트 6개가 기존 정상 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다.
|
||||
|
||||
**Gate 결과 (2026-07-29 / P3-R17-GATE):**
|
||||
|
||||
- Focused multipart 회귀, Phase 3 content 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
- 생성·수정 미정의 part 400/no-side-effect, 수정 `audioFile`·`contentFile` 교체 미지원, 정상/필수/빈 파일/request 415 경계가 유지됨을 확인했다.
|
||||
|
||||
**최종 결론:** `REV-061` resolved. Phase 3 완료.
|
||||
|
||||
**다음 Goal:** `P4-R8`.
|
||||
|
||||
## 26. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: OpenAPI AudioContent create/update multipart schema의 operation별 허용 part와
|
||||
`additionalProperties: false`
|
||||
- 검토 범위: AudioContent POST·PUT controller의 allow-list와 미정의 part·파일 교체 회귀 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-066` — Medium — 일반 form-field multipart part가 allow-list 우회
|
||||
|
||||
- `AiCharacterAdminAudioContentController.kt:81-88`은 operation별 허용 집합을 받지만 실제 검사는
|
||||
`fileMap.keys`에 한정한다.
|
||||
- 기존 create/update 회귀는 각각 `AiCharacterAdminAudioContentCreateTest.kt:251-266`,
|
||||
`AiCharacterAdminAudioContentUpdateTest.kt:145-159`의 filename이 있는 `MockMultipartFile`만 사용한다.
|
||||
- filename 없는 일반 form-field `unexpected`는 생성 `{contentFile, coverImage, request}`, 수정
|
||||
`{coverImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 3.28` / `P3-R18` |
|
||||
| Gate | `P3-R18-GATE` |
|
||||
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·processing·event no-side-effect |
|
||||
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
|
||||
| 범위 제한 | 파일 교체 의미·OpenAPI·전역 resolver·legacy/public 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 3 보완 필요 — `REV-066` 확정
|
||||
|
||||
**다음 Goal:** `P3-R18` (`P2-R12-GATE` 완료 후).
|
||||
|
||||
## 25. 10차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-066`의 AudioContent POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
|
||||
- 왜: 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 거부되지 않았기 때문이다.
|
||||
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
|
||||
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
|
||||
|
||||
**최종 결론:** `REV-066` 처리 완료. Phase 3 후속 Gate 완료.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 26. 15차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature D의 기존 creator 동작 재사용, OpenAPI 오디오 생성 preview 필드,
|
||||
plan의 기존 preview 오류 key 유지 조건
|
||||
- 검토 범위: `AiCharacterAdminAudioContentFacade.create`, `AudioContentService.createAudioContent` 두 overload,
|
||||
v2 오디오 생성 actual endpoint 테스트
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 교차 대조했다. 사용자 지시에 따라
|
||||
컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-072` — High — v2 오디오 생성이 기존 preview 시간 검증을 우회
|
||||
|
||||
- v2 facade는 strict JSON parse와 UTC `releaseDate` 변환 후 `CreateAudioContentRequest`와 파싱된 날짜를 받는
|
||||
`AudioContentService.createAudioContent` overload를 호출한다.
|
||||
- 기존 `previewStartTime`·`previewEndTime`의 쌍, `HH:mm:ss` 형식, 최소 15초 검증 호출은 문자열 request를 받는
|
||||
legacy overload에만 있다. v2가 호출하는 공유 대상 overload에는 검증 호출이 없다.
|
||||
- 해당 overload는 검증 없이 DB를 저장하고 cover/audio를 S3에 업로드한 뒤 두 값이 모두 있으면 그대로 metadata에
|
||||
넣고 event를 발행한다. 따라서 한쪽만 있는 값은 조용히 무시되고, 형식 오류·15초 미만 값은 metadata로 전달될 수 있다.
|
||||
- v2 테스트에는 정상 `00:00:05`~`00:00:25` 입력만 있고 세 거부 규칙의 actual endpoint 회귀가 없다.
|
||||
- 기존 creator 검증/parity와 부작용 선검증을 깨뜨리므로 High로 확정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 3.29` / `P3-R19` |
|
||||
| Gate | `P3-R19-GATE` |
|
||||
| RED | 시작만 입력, 형식 오류, 15초 미만의 400 및 DB/S3/event no-side-effect |
|
||||
| GREEN | 기존 검증 호출을 두 경로가 공유하는 parsed request overload로 이동 |
|
||||
| 회귀 | KO/EN/JA 기존 오류 key, 정상 preview metadata, legacy/public 생성 |
|
||||
|
||||
### `REV-072` 처리 결과 — 2026-07-30
|
||||
|
||||
- `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime` 호출을 이동해 legacy
|
||||
string request 생성과 v2 parsed request 생성이 같은 preview 검증을 정확히 한 번 공유한다.
|
||||
- v2 actual endpoint에 한쪽만 입력, 형식 오류, 15초 미만 preview의 KO/EN/JA 400 응답과 DB/S3/event no-side-effect를
|
||||
추가했고, 정상 15초 이상 preview는 audio upload metadata의 `preview_start_time`·`preview_end_time` 보존을 확인했다.
|
||||
- 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest`
|
||||
→ `BUILD SUCCESSFUL in 39s`; Phase 3 content와 legacy AudioContent 영향 범위 회귀 → `BUILD SUCCESSFUL in 1m 22s`;
|
||||
`./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 32s`; `git diff --check` → 출력 없음.
|
||||
|
||||
**최종 결론:** `REV-072` 처리 완료. Phase 3 완료.
|
||||
|
||||
**다음 Goal:** `P7-R11` 통합 재판정 완료.
|
||||
|
||||
## 27. 16차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation
|
||||
- 검토 범위: 테마·목록·상세·생성·수정, signed URL·UTC 날짜, 댓글 CRUD, owner/actor/parent와 multipart 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- AudioContent 10개 operation과 controller mapping, owner-scoped 조회·수정 및 active 콘텐츠 댓글 경계가 일치한다.
|
||||
- 생성의 UTC `releaseDate`, 필수 파일, strict request, preview 쌍·형식·최소 15초 검증이 공유 service 경계에 유지된다.
|
||||
- 댓글 작성의 동일 콘텐츠 활성 원댓글, target AI 수정 권한, row-only soft delete와 UTC 응답 계약이 유지된다.
|
||||
- 신규 확정 finding이 없어 Phase 3 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 3 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
605
docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md
Normal file
605
docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md
Normal file
@@ -0,0 +1,605 @@
|
||||
# Phase 4 시리즈 관리 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 4 / 시리즈 9개 operation |
|
||||
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 4~7 working tree |
|
||||
| 리뷰 일자 | 2026-07-28 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- PRD Feature D와 OpenAPI Series 9개 operation을 controller/facade/repository/test에 직접 대조한다.
|
||||
- 공개 route 수, soft delete 방식, owner 검증과 JSON request schema 경계를 점검한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `src/main/kotlin/.../aicharacter/series/*`
|
||||
- `src/test/kotlin/.../aicharacter/series/*`
|
||||
- OpenAPI Series path/schema와 plan Phase 4
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- production 수정, legacy series API 변경, 테스트 실행
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | 보안·소유권 우회 또는 데이터 손실 |
|
||||
| High | OpenAPI operation/path 위반 또는 주요 사용자 흐름 회귀 |
|
||||
| Medium | request schema·오류 계약의 제한된 위반 |
|
||||
| Low | 유지보수성 또는 문서 정합성 문제 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
| 근거 | 판정 |
|
||||
|---|---|
|
||||
| OpenAPI `:247`~`:389` | Series는 9개 operation이며 `/series/{seriesId}`에는 GET/PUT만 존재 |
|
||||
| `AiCharacterAdminSeriesController.kt:23`~`:112` | 10개 route를 노출하며 마지막 DELETE가 계약에 없음 |
|
||||
| `AiCharacterAdminSeriesFacade.kt:166`~`:187` | 별도 DELETE facade가 PUT과 같은 `isActive=false`를 구성 |
|
||||
| `AiCharacterAdminSeriesMutationTest.kt:201` 이후 | 계약 밖 DELETE 성공·거부 동작을 테스트가 고정 |
|
||||
| OpenAPI `:1099`~`:1109` | 콘텐츠 추가와 순서 변경 schema는 `additionalProperties: false` |
|
||||
| controller `:55`~`:82` | 두 request를 기본 `@RequestBody` DTO binding으로 수신 |
|
||||
| strict reader 검색 | multipart create/update에만 `FAIL_ON_UNKNOWN_PROPERTIES`가 있고 두 JSON body에는 없음 |
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| route/schema/호출 정적 대조 | 성공 | OpenAPI 9개 대비 controller 10개, strict JSON 경계 누락 확인 |
|
||||
| Gradle/컴파일/테스트 | 미실행 | 사용자 요청에 따라 실행하지 않음 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-023` | High | 처리 완료 | OpenAPI에 없는 시리즈 DELETE route 노출 | `Task 4.7` | `P4-R1` |
|
||||
| `REV-024` | Medium | 처리 완료 | 콘텐츠 추가·순서 변경이 미지 JSON 필드를 허용 | `Task 4.7` | `P4-R1` |
|
||||
|
||||
## 6. 발견 사항 상세
|
||||
|
||||
### REV-023 — OpenAPI에 없는 시리즈 DELETE route
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature D, 공개 API schema 임의 변경 금지
|
||||
- **관련 계약:** `/series/{seriesId}`는 GET과 PUT 수정/soft delete만 제공
|
||||
- **소유 Task:** `Task 4.7`, `P4-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
controller는 `DELETE /series/{seriesId}`를 추가로 노출한다. 같은 soft delete는 계약에 있는 PUT의 `isActive=false`로 이미
|
||||
표현되므로 별도 route는 중복이자 공개 API 표면 확장이다.
|
||||
|
||||
**영향**
|
||||
|
||||
서버와 OpenAPI 기반 client가 서로 다른 operation 집합을 사용한다. 현재 테스트도 계약 밖 route를 정상 동작으로 고정해
|
||||
향후 차이를 지속시킨다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
DELETE controller/facade를 제거하고 기존 DELETE 성공 기대를 405 계약으로 교정하며 PUT `isActive=false` actual endpoint
|
||||
테스트를 보강한다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
`DELETE /series/{seriesId}` controller/facade 경로를 제거했고, actual endpoint 테스트를 405와 `PUT isActive=false` soft delete
|
||||
계약으로 교정했다.
|
||||
|
||||
### REV-024 — 두 JSON body의 `additionalProperties: false` 미적용
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** OpenAPI request schema 준수
|
||||
- **관련 계약:** `SeriesContentAddRequest`, `SeriesOrderUpdateRequest`
|
||||
- **소유 Task:** `Task 4.7`, `P4-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
multipart create/update는 facade strict reader를 사용하지만 콘텐츠 추가와 순서 변경은 기본 `@RequestBody` binding을
|
||||
사용한다. repository 전역 설정에는 unknown property 실패 설정이 없어 두 schema의 계약 밖 필드를 무시하고 요청을 처리한다.
|
||||
현재 malformed JSON 테스트는 문법 오류만 확인하고 미지 필드를 확인하지 않는다.
|
||||
|
||||
**영향**
|
||||
|
||||
잘못된 client field가 성공으로 처리되어 계약 오류를 조기에 발견할 수 없고 mutation side effect까지 진행될 수 있다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
두 body만 strict parse하고 미지 필드 요청의 400 `common.error.invalid_request`와 DB/event 0회를 actual endpoint로
|
||||
고정한다. 전역 ObjectMapper 설정은 legacy/public 영향이 있으므로 변경하지 않는다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
콘텐츠 추가와 순서 변경 body를 facade strict reader로 역직렬화하도록 변경했고, 미지 필드 요청의 400/no-side-effect 테스트를
|
||||
추가했다.
|
||||
|
||||
## 7. plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 4의 `Task 4.7` / `P4-R1`과 `P4-R1-GATE`를 완료 처리했다. 기존 `P4-GATE` 완료 이력은 유지한다.
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| OpenAPI operation 대조 | 충족 | Series controller mapping 9개, 계약 밖 `DELETE /series/{seriesId}` 제거 |
|
||||
| JSON schema 대조 | 충족 | content add/order body strict parse와 unknown-field 400 회귀 통과 |
|
||||
| ownership/soft delete 핵심 추적 | 충족 | facade owner 검증과 PUT soft delete 경로 존재 |
|
||||
| 실행 검증 | 충족 | focused 테스트, series/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
|
||||
|
||||
**최종 결론:** 후속 수정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음. 다음 Goal은 `P5-R1`이다.
|
||||
|
||||
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 검증 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature D·API Expectations, plan Phase 4, OpenAPI Series 9개 operation
|
||||
- 검토 범위: 시리즈 생성 multipart binding, 콘텐츠 연결/해제 facade·repository·legacy service와 관련 테스트
|
||||
- 검증 방식: controller → facade → repository/legacy service 상태 전이를 정적으로 추적했다.
|
||||
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 추가 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-031` | Medium | 처리 완료 | soft-delete된 linked content를 시리즈에서 해제할 수 없음 | `Task 4.8` | `P4-R2` |
|
||||
| `REV-032` | Medium | 처리 완료 | 생성 필수 `image`가 nullable binding으로 공통 오류 계약 우회 | `Task 4.8` | `P4-R2` |
|
||||
|
||||
### REV-031 — soft-delete 콘텐츠의 기존 link 해제 차단
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature D의 콘텐츠 연결/해제와 동일 owner 검증
|
||||
- **관련 계약:** 존재하는 owner link 해제 성공, 없는/cross-owner link만 400
|
||||
- **소유 Task:** `Task 4.8`, `P4-R2`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
해제 facade가 실제 link 존재 여부뿐 아니라 연결 추가/미연결 검색용 적격성 method를 호출한다. 이 method는
|
||||
`duration != null && (isActive || releaseDate != null)`을 요구하므로, 정상 연결 후 콘텐츠가 soft delete되어
|
||||
`isActive=false`, `releaseDate=null`이 되면 link가 존재해도 해제를 400으로 거부한다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `AiCharacterAdminSeriesFacade.kt:109`~`:120`은 해제 전에 `findEligibleContentByIdAndCreatorMemberId`를 요구한다.
|
||||
- 코드: `AiCharacterAdminSeriesRepository.kt:28`~`:30`은 inactive/unreleased 콘텐츠를 제외한다.
|
||||
- 코드: legacy `CreatorAdminContentSeriesService.kt:297`~`:303`은 owner series의 link를 content 상태와 무관하게 제거한다.
|
||||
- 테스트: `AiCharacterAdminSeriesContentTest`는 정상 active link 해제와 inactive 콘텐츠 연결 거부를 분리 검증하지만,
|
||||
이미 연결된 콘텐츠를 soft delete한 뒤 해제하는 상태 전이는 없다.
|
||||
|
||||
**정적 재현 절차**
|
||||
|
||||
1. owner의 active content를 active series에 연결한다.
|
||||
2. content를 soft delete해 `isActive=false`, `releaseDate=null`로 만든다.
|
||||
3. 같은 owner/series/content로 DELETE unlink를 요청한다.
|
||||
4. 실제 link는 남아 있지만 적격성 guard가 null을 반환해 400이 된다.
|
||||
|
||||
**영향**
|
||||
|
||||
관리자는 삭제된 콘텐츠의 시리즈 연결을 정리할 수 없고 orphan link가 남는다. 콘텐츠 연결 추가 적격성과 기존 link 해제
|
||||
조건이 불필요하게 결합된 문제다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
해제는 series에 존재하는 link와 link 콘텐츠의 owner만 검증한다. `findEligibleContentByIdAndCreatorMemberId`는 연결 추가에만
|
||||
유지하고 soft-delete link 해제 회귀를 추가한다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
해제 경로에서 연결 추가·검색용 active/release/duration 적격성 guard를 제거하고, 실제 series link와 link 콘텐츠의 owner만
|
||||
검증하도록 변경했다. soft-delete된 owner content의 기존 link 해제 성공 회귀를 추가했다.
|
||||
|
||||
### REV-032 — 시리즈 생성 필수 image의 nullable binding
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD API Expectations의 필수 multipart 누락 오류
|
||||
- **관련 계약:** OpenAPI `SeriesCreateMultipart.required = ["image", "request"]`
|
||||
- **소유 Task:** `Task 4.8`, `P4-R2`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
OpenAPI는 생성 `image`를 필수로 선언하지만 controller와 facade는 nullable로 받아 누락 요청을 legacy service까지
|
||||
전달한다. 결과는 400이지만 exact `MissingServletRequestPartException`과 `common.error.invalid_request` 대신
|
||||
legacy `creator.admin.series.cover_image_required`가 된다. plan의 Phase 4 오류 표가 이 legacy 결과를 기록해 PRD 공통 계약과
|
||||
충돌한다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 계약: OpenAPI `SeriesCreateMultipart`의 required `image`.
|
||||
- 요구사항: PRD `:183`~`:184`는 필수 part 누락을 exact `MissingServletRequestPartException`,
|
||||
400 `common.error.invalid_request`로 고정한다.
|
||||
- 코드: `AiCharacterAdminSeriesController.kt:83`~`:90`은 `required=false`, nullable image를 사용한다.
|
||||
- 코드: facade create와 legacy service는 null을 받아 domain key로 변환한다.
|
||||
- 테스트: `AiCharacterAdminSeriesMutationTest`는 missing image가 facade/legacy까지 도달하는 동작을 기대한다.
|
||||
|
||||
**영향**
|
||||
|
||||
클라이언트는 같은 신규 prefix의 다른 필수 multipart 누락과 다른 message/exception 계약을 받는다. 상태는 400으로 같고
|
||||
mutation은 시작되지 않으므로 영향은 오류 표면에 제한된다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
PRD/OpenAPI를 우선해 생성 image만 non-null binding으로 변경하고 세 locale의 exact exception/envelope과
|
||||
facade/DB/S3/event 0회를 검증한다. update image의 optional 계약은 유지한다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
시리즈 생성 `image` part를 non-null `MultipartFile` binding으로 변경해 누락 요청을 `MissingServletRequestPartException`,
|
||||
400 `common.error.invalid_request`로 통일했다. KO/EN/JA envelope과 no-side-effect를 actual endpoint로 고정했고 update의 optional
|
||||
`image` 계약은 유지했다.
|
||||
|
||||
**판정 기록**
|
||||
|
||||
- 2026-07-28 — `REV-031`, `REV-032` 모두 코드·문서·테스트 정적 추적으로 확정. 테스트는 미실행.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 4에 `Task 4.8` / `P4-R2`와 `P4-R2-GATE`를 추가했다.
|
||||
`DEC-P4-R2-001`로 생성 image 누락의 canonical 오류를 확정했으며 기존 완료 이력은 유지한다.
|
||||
`P4-R2`와 `P4-R2-GATE`를 완료 처리했다.
|
||||
|
||||
### 2차 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/path 대조 | 충족 | Series 9개 mapping 유지 |
|
||||
| 상태 전이 대조 | 충족 | soft-delete linked content unlink 성공 회귀 통과 |
|
||||
| multipart binding | 충족 | required image 누락이 `MissingServletRequestPartException` 400으로 처리됨 |
|
||||
| plan 반영 | 충족 | `Task 4.8`, `P4-R2`, `P4-R2-GATE` |
|
||||
| 실행 검증 | 충족 | focused 테스트, series/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
|
||||
|
||||
**최종 결론:** 후속 수정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음. 다음 Goal은 `P5-R2`다.
|
||||
|
||||
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature D, plan Phase 4, OpenAPI Series 9개 operation
|
||||
- 검토 범위: 생성·수정 multipart facade, legacy series service의 S3 upload, mutation 테스트
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항
|
||||
|
||||
#### `REV-037` — Medium — 시리즈 빈 image의 0-byte upload
|
||||
|
||||
- 생성 controller의 non-null binding은 part 누락만 차단하며 `MultipartFile.isEmpty`는 검증하지 않는다.
|
||||
- facade는 생성 image를 그대로 legacy service로 넘기고, legacy service는 size 0 metadata와 input stream을 S3에
|
||||
업로드한 뒤 cover를 교체한다.
|
||||
- 수정의 optional 빈 image도 null이 아니므로 같은 0-byte upload와 기존 cover 교체가 발생한다.
|
||||
- 현재 테스트는 생성 image 누락과 정상 image 경로를 다루지만 생성·수정의 빈 part는 다루지 않는다.
|
||||
|
||||
**권장 조치:** 생성 빈 image는 legacy 호출 전에 400으로 거부하고, 수정 빈 image는 optional part 생략으로 정규화한다.
|
||||
정상 upload, 수정 image 생략, 빈 image no-S3/no-cover-change를 actual endpoint로 고정한다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 4에 `Task 4.9` / `P4-R3`과 `P4-R3-GATE`를 추가했다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/path | 충족 | Series 9개 mapping 유지 |
|
||||
| 생성 empty file | 수정 필요 | 0-byte upload와 DB/event 부작용 발생 |
|
||||
| 수정 empty file | 수정 필요 | 기존 cover가 0-byte 객체로 교체됨 |
|
||||
| plan 반영 | 충족 | `Task 4.9`, `P4-R3`, `P4-R3-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
|
||||
|
||||
**최종 결론:** Phase 4 후속 수정 필요
|
||||
|
||||
**남은 항목:** `P3-R11-GATE` 후 `P4-R3` → `P4-R3-GATE`.
|
||||
|
||||
## 11. 3차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-037`을 처리했다.
|
||||
- 왜: 생성·수정의 빈 `image` part가 legacy service로 전달되어 0-byte S3 upload와 cover 교체를 유발했기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminSeriesMutationTest`에 빈 생성 image 400/no-side-effect, 빈 수정 image cover 유지, 빈 image-only no_changes 유지 테스트를 추가했다. focused 실행에서 신규 3건이 실패했다.
|
||||
- GREEN: `AiCharacterAdminSeriesFacade.create`는 empty image를 400으로 거부하고, `update`는 empty image를 null로 정규화해 legacy service에 전달했다.
|
||||
- 검증: focused series mutation test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
|
||||
- 결과: `REV-037` 처리 완료. 생성 empty image는 부작용 전에 400이고, 수정 empty image는 생략으로 처리해 기존 cover를 유지한다.
|
||||
|
||||
**최종 결론:** Phase 4 3차 리뷰 종결
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 12. 4차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature D, plan Phase 4, OpenAPI Series 9개 operation
|
||||
- 검토 범위: CRUD, 콘텐츠 조회·검색·연결·해제, owner-scoped 순서 변경, 최신 empty image 보완
|
||||
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
Series runtime의 신규 확정 finding은 없다. 9개 controller mapping, owner-scoped lock·연결/해제와
|
||||
생성·수정 empty image 정책은 현재 OpenAPI와 일치한다.
|
||||
|
||||
다만 plan Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE /series/{seriesId}/contents`와 request body로 적고 있어,
|
||||
OpenAPI와 실제 `DELETE /series/{seriesId}/contents/{contentId}` body 없음 route와 달랐다. 이를 `REV-039`로 확정했고,
|
||||
Phase 7 `Task 7.6` / `P7-R4`에서 설명을 정정했다. `Task 4.9` 헤더의 미완료 표기도 `REV-038`로 함께 동기화했다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| runtime operation | 충족 | Series controller mapping 9개와 OpenAPI 일치 |
|
||||
| ownership/순서 | 충족 | active owner ID 전체 검증과 ID 정렬 lock 유지 |
|
||||
| 연결/해제 | 충족 | owner link 검증 후 path `contentId`로 해제 |
|
||||
| 문서 계약 | 충족 | `REV-039`의 stale DELETE path/body 설명 정정 |
|
||||
| plan 전환 | 충족 | `Task 7.6`, `P7-R4` 완료 |
|
||||
|
||||
**최종 결론:** Phase 4 기능 추가 수정 없음, Phase 7 문서 보완 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 13. 5차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 확인된 문제
|
||||
|
||||
#### `REV-042` — 시리즈 생성 primitive의 explicit null 허용 가능성
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **계약:** OpenAPI `SeriesCreateRequest`의 `genreId`, `isAdult`은 nullable이 아니며 각각 생략 기본값 `0`,
|
||||
`false`를 가진다.
|
||||
- **구현:** `CreateSeriesRequest`의 두 값은 Kotlin primitive이고 facade strict reader는 미지 필드만 거부한다.
|
||||
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 explicit null은 primitive 기본값으로 보정될 수 있다.
|
||||
- **영향:** 특히 `isAdult: null`이 계약상 400 대신 `false`인 정상 생성으로 이어져 S3·DB·event mutation이 발생할 수 있다.
|
||||
|
||||
### 보완 결과
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 4.10` / `P4-R4` 처리 완료 |
|
||||
| 시작 조건 | `P3-R12-GATE` 완료 후 실행 |
|
||||
| Gate | `P4-R4-GATE` 완료 |
|
||||
| RED | `genreId: null`, `isAdult: null` actual POST가 보완 전 400 기대 실패 |
|
||||
| GREEN | v2 생성 strict reader에서 explicit null 거부, 필드 생략 기본값 유지 |
|
||||
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
|
||||
|
||||
### 실행 검증
|
||||
|
||||
| 명령 또는 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| `AiCharacterAdminSeriesMutationTest.shouldRejectNullPrimitiveCreateFieldsBeforeSideEffects` RED | 실패 확인 | 신규 2개 invocation이 400 기대 실패 |
|
||||
| 같은 focused RED/GREEN 명령 | 통과 | strict reader 보완 후 `BUILD SUCCESSFUL` |
|
||||
| `AiCharacterAdminSeriesMutationTest` | 통과 | 기존 생성 기본값·정상 mutation 회귀 유지 |
|
||||
| series/common 영향 범위 회귀 | 통과 | `BUILD SUCCESSFUL in 1m 12s` |
|
||||
| `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 20s`, diff check 출력 없음 |
|
||||
|
||||
**최종 결론:** Phase 4 `REV-042` 보완 완료
|
||||
|
||||
**다음 Goal:** `P5-R3`.
|
||||
|
||||
## 14. 장르 참조 API·상세 응답 후속 검토 — 2026-07-29
|
||||
|
||||
### 확인 결과
|
||||
|
||||
- **`REV-046` / Medium / 구현 대기:** 시리즈 등록용 활성 장르 목록은 레거시 관리자 service/repository에 존재하지만
|
||||
신규 v2 캐릭터 관리자 route에는 없다. 계약은 `orders` 오름차순의 `id`, `genre`, `isAdult` 직접 배열이다.
|
||||
- **`REV-047` / Medium / 정합화 대기:** 현재 시리즈 상세는 레거시 상세 DTO를 반환해 목록 item과
|
||||
`publishedDaysOfWeek`, `genreId`, `state`, `isActive` 필드·타입이 다르다.
|
||||
- 승인된 상세 `data`는 목록 `items` 단일 객체와 동일한 11개 필드이며 기존 상세 전용 `genre`, `keywords`는 제거한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 장르 목록: `Task 4.11` / `P4-R5`, Gate `P4-R5-GATE`
|
||||
- 상세 정합화: `Task 4.12` / `P4-R6`, Gate `P4-R6-GATE`
|
||||
- 범위 밖: 장르 CRUD, 목록 wrapper 변경, legacy/public 상세 DTO 변경
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 4 장르 목록 구현과 시리즈 상세 정합화 필요
|
||||
|
||||
**다음 Goal:** `P4-R5`.
|
||||
|
||||
## 15. 장르 참조 API 구현 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-046`을 처리했다.
|
||||
- 왜: 시리즈 등록 화면에서 사용할 활성 장르 목록이 레거시 관리자 API에는 있으나 신규 v2 캐릭터 관리자 route에는 없었기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminSeriesGenreTest`에 활성/비활성 장르, `orders` 오름차순, 빈 목록, ADMIN 공통 경계 actual GET 테스트 2건을 추가했고 미구현 route로 실패했다.
|
||||
- GREEN: `AiCharacterAdminSeriesReferenceController`에 `GET /api/v2/admin/ai-characters/series-genres`를 추가하고 facade에서 기존 `AdminContentSeriesGenreService.getSeriesGenreList()`를 그대로 재사용했다.
|
||||
- Gate: focused 장르 테스트, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
- 결과: `REV-046` 처리 완료. 별도 query, DTO, pagination, 장르 CRUD 변경은 추가하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 4 장르 목록 후속 기능 종결
|
||||
|
||||
**다음 Goal:** `P4-R6`.
|
||||
|
||||
## 16. 시리즈 상세 정합화 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-047`을 처리했다.
|
||||
- 왜: 시리즈 상세가 레거시 상세 DTO를 반환해 목록 item과 field/type이 달랐기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminSeriesQueryTest`에서 상세 `data`의 11개 목록 item field와 `genre`, `keywords` 부재를 고정했고 레거시 상세 응답 차이로 실패했다.
|
||||
- GREEN: v2 상세 response type을 `GetCreatorAdminContentSeriesListItem`으로 통일하고 facade에서 owner-scoped series를 동일 필드로 매핑했다.
|
||||
- Gate: focused query/contract, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행한다.
|
||||
- 결과: `REV-047` 처리 완료. legacy/public 상세 DTO와 mapper는 변경하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 4 시리즈 상세 정합화 종결
|
||||
|
||||
**다음 Goal:** `P5-R5`.
|
||||
|
||||
## 17. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature D, OpenAPI Series 10개 operation
|
||||
- 검토 범위: Series/reference controller, facade/repository, JSON·multipart mapping과 목록·상세 DTO
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### 확인 결과
|
||||
|
||||
| 항목 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| route/operation | 충족 | 장르 참조를 포함한 Series 10개 OpenAPI operation과 실제 mapping 유지 |
|
||||
| request 전체 media type | 충족 | 생성·수정 multipart, 순서 변경·콘텐츠 추가 JSON `consumes` 선언 일치 |
|
||||
| query/default | 충족 | 목록·연결 콘텐츠 page/size 기본값과 미연결 검색 필수 query 일치 |
|
||||
| response/ownership | 충족 | 상세-목록 item schema 정합화와 target owner 경계 유지 |
|
||||
|
||||
### `REV-057` — High — 처리 완료 — Series multipart request part의 JSON media type 미강제
|
||||
|
||||
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
|
||||
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
|
||||
- 정상 테스트는 JSON media type만 사용하며 미지원/누락 part media type의 415 `Accept` header와
|
||||
S3/DB/event no-side-effect를 고정하지 않는다.
|
||||
- 같은 shared converter와 signature에서 AudioContent의 `text/plain` 성공 테스트가 있어 permissive binding을
|
||||
정적으로 확인할 수 있다.
|
||||
|
||||
### 처리 결과
|
||||
|
||||
- `AiCharacterAdminSeriesController`의 POST·PUT에 `MultipartHttpServletRequest`를 받고 Character·AudioContent와
|
||||
같은 part header 검사와 `HttpMediaTypeNotSupportedException`을 적용했다. 기존 String strict reader와
|
||||
facade/domain 로직은 변경하지 않았다.
|
||||
- `AiCharacterAdminSeriesMutationTest`는 POST·PUT 각각의 `text/plain`·Content-Type 누락 request part를 KO/EN/JA
|
||||
415 `ApiResponse.error`, `Accept: application/json`, S3/DB/event no-side-effect로 고정했고, 필수 `request` part
|
||||
누락의 기존 400 binding 오류도 확인했다.
|
||||
- RED는 신규 media type 12건이 415 기대와 달리 실패했고, GREEN focused 36건과 Series/common error 영향 범위 회귀,
|
||||
`ktlintCheck`, OpenAPI encoding 정적 대조, `git diff --check`를 통과했다.
|
||||
|
||||
**최종 결론:** `REV-057`, `P4-R7`, `P4-R7-GATE` 처리 완료
|
||||
|
||||
**다음 Goal:** `P5-R7` (`P4-R7-GATE` 완료 후).
|
||||
|
||||
## 18. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature D, OpenAPI `SeriesCreateMultipart`·`SeriesUpdateMultipart`
|
||||
- 검토 범위: Series POST·PUT controller의 multipart binding과 image/genre/owner mutation 테스트
|
||||
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### `REV-062` — Medium — `image`, `request` 외 multipart part가 무시됨
|
||||
|
||||
- 두 Series multipart schema는 `additionalProperties: false`이며 허용 이름은 `image`, `request`다.
|
||||
- controller의 `requireJsonRequestPart()`는 `request` media type만 검사하고 전체 part 이름을 열거하지 않는다.
|
||||
- 정상 part와 미정의 part를 함께 보낸 요청이 facade로 전달될 수 있어 OpenAPI 입력 범위보다 runtime이 넓다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 4.14` / `P4-R8` |
|
||||
| Gate | `P4-R8-GATE` |
|
||||
| RED | POST·PUT 미정의 part와 S3·DB·event 결과 |
|
||||
| GREEN | 실제 part 이름을 `{image, request}`와 비교해 초과 이름 400 |
|
||||
| 회귀 | 필수/빈 image, request part 415, genre/owner 경계 |
|
||||
|
||||
**처리 결과 (2026-07-29 / P4-R8):**
|
||||
|
||||
- Series POST·PUT controller 경계에 multipart part allow-list를 추가해 `image`, `request` 외 file part를 400 `common.error.invalid_request`로 거부했다.
|
||||
- RED에서 POST·PUT `unexpected` part KO/EN/JA 테스트 6개가 기존 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다.
|
||||
|
||||
**Gate 결과 (2026-07-29 / P4-R8-GATE):**
|
||||
|
||||
- Focused multipart 회귀, Phase 4 Series 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
- POST·PUT 미정의 part 400/no-side-effect, 정상·필수/빈 image, request part 415, genre/owner 경계가 유지됨을 확인했다.
|
||||
|
||||
**최종 결론:** `REV-062` resolved. Phase 4 완료.
|
||||
|
||||
**다음 Goal:** `P5-R9`.
|
||||
|
||||
## 19. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature D, OpenAPI Series create/update multipart 및 장르 계약
|
||||
- 검토 범위: Series controller/facade, active genre 조회 경계, legacy genre repository와 mutation 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스와 기존 compile output을 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### `REV-067` — Medium — 일반 form-field multipart part가 allow-list 우회
|
||||
|
||||
- `AiCharacterAdminSeriesController.kt:114-117`은 `fileMap.keys`만 `{image, request}`와 비교한다.
|
||||
- 기존 `AiCharacterAdminSeriesMutationTest.kt:664-710`은 filename이 있는 `MockMultipartFile("unexpected", ...)`만
|
||||
검증하므로 filename 없는 일반 form-field part 경계는 빠져 있다.
|
||||
- OpenAPI의 create/update multipart `additionalProperties: false`를 우회할 수 있어 Medium으로 확정한다.
|
||||
|
||||
### `REV-068` — Medium — `genreId <= 0`이 active genre 검사를 우회
|
||||
|
||||
- `AiCharacterAdminSeriesFacade.kt:155-168`은 create와 non-null update `genreId`에 공통 검사를 호출하지만,
|
||||
`:213-215`의 구현은 양수인 경우에만 active genre 존재를 확인한다.
|
||||
- OpenAPI create schema는 0이 domain validation에서 유효하지 않다고 명시한다. 그러나 0 또는 음수는 legacy service로
|
||||
전달되고, `CreatorAdminContentSeriesGenreRepository.kt:19-26`의 QueryDSL `fetchFirst()` 결과를 Kotlin non-null
|
||||
반환으로 취급하는 repository 경계에서 NPE가 발생할 수 있다.
|
||||
- 기존 compile output의 해당 repository bytecode를 `javap`로 확인한 결과 `fetchFirst()` 뒤 Kotlin
|
||||
`checkNotNullExpressionValue`가 존재했다. 공통 예상 밖 예외는 500 경로이므로 요청 오류 400 계약과 다르다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| finding | 신규 Task / Goal | Gate | 최소 보완 |
|
||||
|---|---|---|---|
|
||||
| `REV-067` | `Task 4.15` / `P4-R9` | `P4-R9-GATE` | servlet 전체 part 이름을 `{image, request}`와 대조 |
|
||||
| `REV-068` | `Task 4.16` / `P4-R10` | `P4-R10-GATE` | 0 이하 또는 비활성·미존재 장르를 legacy 호출 전에 400으로 거부 |
|
||||
|
||||
**최종 결론:** Phase 4 보완 필요 — Medium 2건 확정
|
||||
|
||||
**다음 Goal:** `P4-R9` (`P3-R18-GATE` 완료 후), 이어서 `P4-R10`.
|
||||
|
||||
## 20. 8차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-067`의 Series POST·PUT filename 없는 multipart part 우회와 `REV-068`의 `genreId <= 0` 사전 거부 누락을 보완했다.
|
||||
- 왜: `{image, request}` 외 일반 form-field part와 0 이하 장르가 legacy service 호출 전 400으로 고정되어야 하기 때문이다.
|
||||
- 어떻게: `AiCharacterAdminSeriesMutationTest`에 filename 없는 part와 `genreId=0/-1` create·update KO/EN/JA actual endpoint 회귀를 추가하고, controller part 검사와 facade active genre guard를 최소 수정했다.
|
||||
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
|
||||
|
||||
**최종 결론:** `REV-067`, `REV-068` 처리 완료. Phase 4 후속 Gate 완료.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 21. 9차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature E, OpenAPI Series 10개 operation
|
||||
- 검토 범위: 목록·상세·생성·수정·연결 콘텐츠·순서·장르, owner/active genre/multipart 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정 및 plan 전환
|
||||
|
||||
- Series 10개 operation과 controller/facade의 owner·active genre·exact multipart 경계를 대조했다.
|
||||
- 기존 완료 finding 이후 신규 확정 finding은 없다.
|
||||
- Phase 4 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 4 추가 수정 없음.
|
||||
|
||||
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
|
||||
|
||||
## 22. 10차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature D, OpenAPI Series 10개 operation
|
||||
- 검토 범위: 장르·목록·상세·생성·수정, 연결 콘텐츠 조회·검색·추가·해제와 순서 변경
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- Series 10개 operation과 controller mapping, 목록 item과 동일한 상세 schema가 일치한다.
|
||||
- active target/series·genre와 owner-scoped 콘텐츠 연결/해제, 순서 변경 lock·전체 ID 선검증이 유지된다.
|
||||
- 생성·수정의 필수/빈 image, strict JSON과 exact multipart part 경계가 OpenAPI와 일치한다.
|
||||
- 신규 확정 finding이 없어 Phase 4 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 4 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
641
docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md
Normal file
641
docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md
Normal file
@@ -0,0 +1,641 @@
|
||||
# Phase 5 커뮤니티 관리 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 5 / 커뮤니티 3개 operation |
|
||||
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 5~7 working tree |
|
||||
| 리뷰 일자 | 2026-07-28 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- PRD Feature E와 OpenAPI Community 3개 operation을 facade/legacy service/test에 대조한다.
|
||||
- multipart JSON 오류, 미지 필드, pagination과 side-effect 차단 순서를 점검한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `AiCharacterAdminCommunityPostController`, `Facade`, `Repository`, DTO
|
||||
- 관리자 community 테스트와 재사용하는 `CreatorCommunityService`
|
||||
- OpenAPI Community path/schema와 plan Phase 5
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- production 수정, legacy/public community 계약 변경, 테스트 실행
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | 소유권 우회 또는 데이터 손실 |
|
||||
| High | 잘못된 요청이 500/side effect로 이어지는 주요 계약 위반 |
|
||||
| Medium | pagination 또는 제한된 request schema 위반 |
|
||||
| Low | 유지보수성 또는 문서 정합성 문제 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
| 근거 | 판정 |
|
||||
|---|---|
|
||||
| `AiCharacterAdminCommunityPostFacade.kt:36`~`:50` | create는 raw JSON을 legacy service에 전달하고 update는 기본 ObjectMapper로 직접 parse |
|
||||
| `CreatorCommunityService.kt:73`~`:87` | create JSON을 기본 ObjectMapper로 parse한 뒤 media 검증·side effect 진행 |
|
||||
| `AiCharacterAdminExceptionHandler.kt:58`~`:71` | Jackson parse 예외 전용 400 변환이 없고 미분류 예외는 500 |
|
||||
| OpenAPI `:1156`~`:1176` | create/update request는 필수 필드와 `additionalProperties: false`를 정의 |
|
||||
| `AiCharacterAdminCommunityPostFacade.kt:68`~`:81`, `:141`~`:145` | 목록에 `size in 1..50`을 강제 |
|
||||
| OpenAPI `Size` parameter `:519` | minimum 1만 있고 maximum은 없음 |
|
||||
| create/update 테스트 | request part 누락은 검증하지만 malformed/missing JSON field/unknown field는 직접 검증하지 않음 |
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| parse 흐름·예외 handler·schema 정적 추적 | 성공 | parse 예외의 500 가능성과 unknown-field 허용 경계 확인 |
|
||||
| pagination 계약 대조 | 성공 | runtime 최대 50과 OpenAPI maximum 부재 확인 |
|
||||
| Gradle/컴파일/테스트 | 미실행 | 사용자 요청에 따라 실행하지 않음 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-025` | High | 처리 완료 | multipart JSON parse 오류가 500이 될 수 있고 미지 필드를 허용 | `Task 5.7` | `P5-R1` |
|
||||
| `REV-026` | Medium | 처리 완료 | OpenAPI에 없는 목록 size 50 상한 | `Task 5.7` | `P5-R1` |
|
||||
|
||||
## 6. 발견 사항 상세
|
||||
|
||||
### REV-025 — Community JSON 오류 경계 불일치
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature E, 공통 오류/side-effect 계약
|
||||
- **관련 계약:** 잘못된 request는 400 `common.error.invalid_request`, request schema는 미지 필드 금지
|
||||
- **소유 Task:** `Task 5.7`, `P5-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
create는 raw request 문자열을 legacy service에 넘기고 update는 facade에서 기본 ObjectMapper로 읽는다. 두 경로 모두
|
||||
`FAIL_ON_UNKNOWN_PROPERTIES`를 활성화하지 않으며 `JsonProcessingException`을 관리자 API 예외로 변환하지 않는다.
|
||||
|
||||
**영향**
|
||||
|
||||
malformed JSON이나 필수 non-null 필드 누락이 공통 handler의 500 `common.error.unknown`으로 분류될 수 있다. 미지 필드는
|
||||
무시되어 잘못된 요청이 mutation과 S3/event 경로까지 진행될 수 있다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
legacy 호출 전에 create/update DTO를 strict reader로 검증하고 Jackson mapping 오류를
|
||||
400 `common.error.invalid_request`로 변환한다. malformed, 필수 필드 누락, 미지 필드의 DB/S3/event 0회를 actual
|
||||
endpoint로 고정한다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
create/update `request` part를 legacy service 호출 전에 strict reader로 검증하고 Jackson parse/mapping 오류를
|
||||
400 `common.error.invalid_request`로 변환했다. create malformed·필수 field 누락·미지 field, update malformed·미지 field의
|
||||
actual endpoint no-side-effect 테스트를 추가했다. OpenAPI상 update request에는 required field가 없어 update 필수 field 누락
|
||||
케이스는 계약 밖으로 제외했다.
|
||||
|
||||
### REV-026 — 계약에 없는 Community size 상한
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** 레거시 목록 query/pagination 유지
|
||||
- **관련 계약:** 공통 `Size` parameter는 default 20, minimum 1이며 maximum 없음
|
||||
- **소유 Task:** `Task 5.7`, `P5-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
facade는 `size !in 1..50`을 400으로 거부한다. OpenAPI 단일 원본에는 maximum 50이 없으므로 `size=51`은 계약상 유효하다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
OpenAPI를 임의 변경하지 않고 관리자 facade의 상한 guard만 제거한다. page 음수와 size 1 미만 검증은 유지한다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
목록의 `size <= 50` 상한 guard만 제거하고 `page < 0`, `size < 1` 검증은 유지했다. `size=51` actual endpoint 요청이
|
||||
정상 pagination으로 처리되는 테스트를 추가했다.
|
||||
|
||||
## 7. plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 5의 `Task 5.7` / `P5-R1`과 `P5-R1-GATE`를 완료 처리했다. 기존 `P5-GATE` 완료 이력은 유지한다.
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/path 대조 | 충족 | Community 3개 route 존재 |
|
||||
| JSON 오류/schema 대조 | 충족 | strict parse와 parse 예외 400 변환, unknown-field 거부 회귀 통과 |
|
||||
| pagination 대조 | 충족 | 계약에 없는 maximum 50 제거, `size=51` 회귀 통과 |
|
||||
| ownership 선검증 | 충족 | target/post owner 확인은 mutation 전 수행 |
|
||||
| 실행 검증 | 충족 | focused 테스트, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
|
||||
|
||||
**최종 결론:** 후속 수정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음. 다음 Goal은 `P6-R1`이다.
|
||||
|
||||
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 검증 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
|
||||
- 검토 범위: fixed update facade/legacy service/repository, transaction·lock 경계와 concurrency test
|
||||
- 검증 방식: 두 병렬 transaction의 count/read/update 순서를 코드와 테스트로 정적 추적했다.
|
||||
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 추가 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-033` | High | 처리 완료 | 최대 고정 3개가 실제 병렬 요청에서 보장되지 않음 | `Task 5.8` | `P5-R2` |
|
||||
|
||||
### REV-033 — lock 없는 count-then-update 경쟁 조건
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature E의 최대 고정 게시글 수 3개
|
||||
- **관련 계약:** plan transaction/concurrency 고려사항과 `Task 5.5`의 동시 요청 완료 증거
|
||||
- **소유 Task:** `Task 5.8`, `P5-R2`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
legacy fixed update는 활성 고정 수를 조회한 뒤 별도 게시글 entity의 `isFixed`를 변경한다. owner 또는 고정 집합을 잠그는
|
||||
lock/constraint가 없으므로, 고정 2개 상태에서 서로 다른 게시글을 고정하는 두 transaction이 모두 count 2를 읽고 커밋하면
|
||||
최종 고정 수는 4개가 된다.
|
||||
|
||||
`AiCharacterAdminCommunityPostConcurrencyTest`는 이름과 달리 두 요청을 순서대로 호출한다. plan `Task 5.5`도 실제 병렬
|
||||
재현을 하지 못했다고 기록하면서 Task objective·완료 증거와 Phase acceptance를 완료 처리했다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `AiCharacterAdminCommunityPostFacade.kt:62`~`:69`은 lock 없이 legacy fixed update를 호출한다.
|
||||
- 코드: `CreatorCommunityService.kt:247`~`:262`는 `countBy...` 후 서로 다른 post를 갱신한다.
|
||||
- 테스트: `AiCharacterAdminCommunityPostConcurrencyTest.kt:55`~`:79`는 세 번째 요청 완료 후 네 번째 요청을 실행하는
|
||||
순차 시나리오다.
|
||||
- 계획: `Task 5.5` objective/완료 증거는 동시 요청을 요구하지만 `:2849`~`:2852`에서 실제 병렬 요청은 미검증이라고
|
||||
명시한다.
|
||||
- 기존 코드: `MemberRepository.findByIdForUpdate` owner row pessimistic lock을 재사용할 수 있다.
|
||||
|
||||
**정적 재현 절차**
|
||||
|
||||
1. 같은 owner에 active fixed post 2개와 미고정 post 2개를 준비한다.
|
||||
2. 두 독립 transaction이 서로 다른 미고정 post를 `isFixed=true`로 수정한다.
|
||||
3. lock이 없으므로 두 transaction 모두 count 2를 읽을 수 있다.
|
||||
4. 서로 다른 row를 갱신해 둘 다 커밋하면 최종 active fixed count는 4가 된다.
|
||||
|
||||
**영향**
|
||||
|
||||
PRD의 최대 3개 데이터 불변식이 깨지고 관리자 목록 정렬·운영 정책이 비결정적이 된다. 단순 순차 회귀는 통과하므로
|
||||
현재 테스트 통과만으로 문제를 탐지할 수 없다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
신규 DDL이나 dependency 없이 기존 `MemberRepository.findByIdForUpdate`로 fixed/unfixed count·update 전에 owner를 잠근다.
|
||||
두 독립 transaction과 barrier/lock probe를 사용하는 결정적 병렬 테스트로 최종 3개, 한 요청 400, 실패 side effect 0건을
|
||||
검증하고 sleep·반복 확률 기반 테스트는 사용하지 않는다.
|
||||
|
||||
**처리 결과**
|
||||
|
||||
fixed 변경 요청에서 legacy count/update 전에 `MemberRepository.findByIdForUpdate`로 owner row를 잠그도록 변경했다.
|
||||
두 병렬 요청이 같은 count 경계에 진입하는 결정적 회귀 테스트를 추가했고, 최종 고정 수 3개와 한 요청 400을 확인했다.
|
||||
|
||||
**판정 기록**
|
||||
|
||||
- 2026-07-28 — 코드·plan 완료 증거·테스트 실행 구조로 경쟁 조건을 확정. 테스트는 사용자 요청에 따라 미실행.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 5에 `Task 5.8` / `P5-R2`와 `P5-R2-GATE`를 추가했다. 기존 `Task 5.5` 완료 이력은
|
||||
되돌리지 않고 실제 동시성 보완을 새 Goal로 추적한다.
|
||||
`P5-R2`와 `P5-R2-GATE`를 완료 처리했다.
|
||||
|
||||
### 2차 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema 대조 | 충족 | Community 3개 mapping과 JSON 경계 유지 |
|
||||
| 순차 fixed 정책 | 충족 | 세 번째 성공·네 번째 거부 테스트 존재 |
|
||||
| 실제 동시성 불변식 | 충족 | owner row lock과 결정적 병렬 회귀 통과 |
|
||||
| plan 반영 | 충족 | `Task 5.8`, `P5-R2`, `P5-R2-GATE` |
|
||||
| 실행 검증 | 충족 | focused concurrency, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
|
||||
|
||||
**최종 결론:** 후속 수정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음. 다음 Goal은 `P7-R2`다.
|
||||
|
||||
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
|
||||
- 검토 범위: 목록·생성·수정 facade, strict multipart JSON, owner-scoped query, fixed owner lock과 관련 테스트
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 |
|
||||
| ownership | 충족 | active target과 owner post 검증 유지 |
|
||||
| JSON/multipart | 충족 | strict parse와 malformed/unknown-field 400 유지 |
|
||||
| fixed 동시성 | 충족 | owner row lock이 count/update 앞에서 수행됨 |
|
||||
| plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 5 추가 수정 없음
|
||||
|
||||
**남은 항목:** `P7-R3`에서 Community/common 회귀를 통합 재검증한다.
|
||||
|
||||
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
|
||||
- 검토 범위: 목록·생성·수정, owner query, strict JSON, 고정 수 lock과 soft delete
|
||||
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 |
|
||||
| ownership | 충족 | active target과 owner post를 mutation 전에 확인 |
|
||||
| 고정/soft delete | 충족 | owner row lock과 fixed 상태 동시 해제 유지 |
|
||||
| 오류/부작용 | 충족 | strict JSON과 target/owner 실패 선검증 유지 |
|
||||
| plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 5 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 확인된 문제
|
||||
|
||||
#### `REV-043` — 커뮤니티 primitive의 required·null 계약 미강제
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **계약:** OpenAPI create request는 `isCommentAvailable`, `isAdult`를 required non-null boolean으로 정의하고,
|
||||
`price`와 update의 `isFixed`도 nullable로 선언하지 않는다.
|
||||
- **구현:** create DTO의 boolean/price는 Kotlin primitive이고 update `isFixed`는 nullable이라, strict reader가
|
||||
미지 필드만 거부하면 누락·explicit null을 계약대로 구분하지 못한다.
|
||||
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 create primitive는 false·0으로 보정될 수 있고,
|
||||
update `isFixed: null`은 필드 생략과 같은 null로 처리된다.
|
||||
- **영향:** 계약상 invalid 요청이 생성 mutation을 진행하거나 성공 no-op update로 처리될 수 있다.
|
||||
|
||||
### 보완 결과
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 5.9` / `P5-R3` 처리 완료 |
|
||||
| 시작 조건 | `P4-R4-GATE` 완료 후 실행 |
|
||||
| Gate | `P5-R3-GATE` 완료 |
|
||||
| RED | required boolean 누락·null, `price: null`, `isFixed: null`이 400 기대 실패 |
|
||||
| GREEN | v2 create/update 경계 required/non-null 검증, 생략 의미 유지 |
|
||||
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
|
||||
|
||||
### 실행 검증
|
||||
|
||||
| 명령 또는 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| invalid primitive RED focused | 실패 확인 | 신규 6건이 400 기대 실패 |
|
||||
| invalid primitive GREEN focused | 통과 | `BUILD SUCCESSFUL in 41s` |
|
||||
| create/update focused | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 34s` |
|
||||
| community/common 영향 범위 회귀 | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 1m 2s` |
|
||||
| `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 13s`, diff check 출력 없음 |
|
||||
|
||||
**최종 결론:** Phase 5 `REV-043` 보완 완료
|
||||
|
||||
**다음 Goal:** `P5-R4`.
|
||||
|
||||
## 13. Community 목록 요구사항 변경 판정 — 2026-07-29
|
||||
|
||||
### 확정 요구사항
|
||||
|
||||
- **추적 ID:** `DEC-P5-LIST-001`
|
||||
- GET 목록에서 실제 응답 생성에 사용하지 않는 `timezone` query를 제거한다.
|
||||
- 성공 `data`는 직접 배열 대신
|
||||
`AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`를 반환한다.
|
||||
- `totalCount`와 `hasNext`는 target creatorMember 소유 active 게시글만 기준으로 계산한다.
|
||||
- `items`의 기존 18개 필드와 고정 우선 정렬, owner/inactive 격리, page/size 오류 정책은 유지한다.
|
||||
|
||||
### 설계 판정
|
||||
|
||||
| 항목 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| query | `page`, `size`만 유지 | timezone은 facade에서 유효성 검사 외 사용되지 않음 |
|
||||
| response | 전용 pagination wrapper 추가 | UI가 전체 개수와 추가 로딩 필요 여부를 판단해야 함 |
|
||||
| count | active owner count query 1개 추가 | totalCount가 필요해 size+1 조회만으로는 충족 불가 |
|
||||
| hasNext | `pageable.offset + items.size < totalCount` | 마지막·범위 밖 page를 단순하게 처리 |
|
||||
| 기존 item | 변경 없음 | 요청 범위 밖 schema 변경 방지 |
|
||||
| 공용 추상화 | 추가하지 않음 | 단일 endpoint 전용 DTO가 최소 변경 |
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 5.10` / `P5-R4`
|
||||
- Gate: `P5-R4-GATE`
|
||||
- 시작 조건: `P5-R3-GATE` 완료
|
||||
- 통합 조건: `P7-R5` 시작 전에 `P5-R4-GATE` 완료
|
||||
|
||||
### 처리 결과
|
||||
|
||||
`P5-R4`에서 controller/facade의 `timezone` query와 미사용 검증을 제거하고, repository에 active owner count query를 추가했다.
|
||||
성공 `data`는 `AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`로 반환한다. 기존 item 18개 필드,
|
||||
고정 우선 정렬, owner/inactive 격리, `page < 0`·`size < 1` 오류 정책과 문서에 없는 size 상한 부재는 유지했다. OpenAPI
|
||||
Community GET status는 `implemented`로 복구했다.
|
||||
|
||||
### 실행 검증
|
||||
|
||||
| 명령 또는 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| query RED focused | 실패 확인 | 신규 3건이 400/직접 배열 응답 차이로 실패 |
|
||||
| query GREEN focused | 통과 | `BUILD SUCCESSFUL in 1m 28s` |
|
||||
| query+contract focused | 통과 | `BUILD SUCCESSFUL in 37s` |
|
||||
| community/common 영향 범위 회귀 | 통과 | `BUILD SUCCESSFUL in 1m 6s` |
|
||||
| OpenAPI jq assertion | 통과 | `true` |
|
||||
| `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 12s`, diff check 출력 없음 |
|
||||
|
||||
**최종 결론:** Phase 5 `DEC-P5-LIST-001` 목록 계약 정합화 및 Gate 완료
|
||||
|
||||
**다음 Goal:** `P7-R5`.
|
||||
|
||||
## 14. 커뮤니티 댓글 후속 검토 — 2026-07-29
|
||||
|
||||
### 확인 결과
|
||||
|
||||
- **`REV-048` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 커뮤니티 게시글의 댓글·답글
|
||||
조회/작성/수정/삭제 5개 operation이 없다.
|
||||
- 조회는 필수 `timezone`과 `page`, `size`, 레거시 `totalCount/items`를 유지한다.
|
||||
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 게시글의 row를
|
||||
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
|
||||
- 답글 `parentId`는 같은 게시글의 활성 원댓글이어야 한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 5.11` / `P5-R5`
|
||||
- Gate: `P5-R5-GATE`
|
||||
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 5 커뮤니티 댓글 CRUD 구현 필요
|
||||
|
||||
**다음 Goal:** `P5-R5`.
|
||||
|
||||
## 15. 커뮤니티 댓글 CRUD 구현 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-048`을 처리했다.
|
||||
- 왜: target AI 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 5개 operation이 신규 v2 관리자 경계에 없었기 때문이다.
|
||||
- 어떻게:
|
||||
- RED: `AiCharacterAdminCommunityPostCommentTest`에 root/reply 목록, target AI 작성, parent 검증, target 작성자 수정 제한, row-only soft delete, 요청 오류 계약 테스트 7건을 추가했고 미구현 route로 실패했다.
|
||||
- GREEN: `AiCharacterAdminCommunityPostController`에 5개 route를 추가하고, facade에서 target/owner/parent/actor를 선검증한 뒤 기존 `CreatorCommunityService` 댓글 조회·작성·수정 의미를 재사용했다.
|
||||
- Gate: focused 댓글 테스트, community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
|
||||
- 결과: `REV-048` 처리 완료. 캐릭터 직접 댓글, hard delete, cascade, legacy/public endpoint 변경은 추가하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 5 커뮤니티 댓글 후속 기능 종결
|
||||
|
||||
**다음 Goal:** `P6-R2`.
|
||||
|
||||
## 16. 커뮤니티 댓글 UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature E, OpenAPI 2.2.0 Community 8개 operation, `DEC-UTC-DATE-001`
|
||||
- 검토 범위: 커뮤니티 댓글·답글 GET의 controller/facade/repository와 관련 테스트
|
||||
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-051` — High — 처리 완료: 커뮤니티 댓글 2개 GET의 timezone/UTC 계약 불일치
|
||||
|
||||
- 처리 전 댓글·답글 GET은 필수 `timezone` query를 controller/facade/repository로 전달하고, 각 댓글 `date`를
|
||||
요청 timezone에 맞춘 표시 문자열로 반환한다.
|
||||
- 승인된 최신 계약은 `timezone` query 없이 `page`, `size`만 받고 기존 `totalCount`, `items`, `date` 필드명을
|
||||
유지하되 `date` 값을 ISO-8601 UTC(`Z`)로 반환한다.
|
||||
- 댓글 작성·수정·삭제의 actor/owner/parent/soft delete 의미와 커뮤니티 게시글 목록의 pagination wrapper는
|
||||
변경 대상이 아니다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| route 수 | 유지 | Community 8개 operation 자체는 변경 없음 |
|
||||
| 댓글·답글 query | 처리 완료 | v2 controller/facade에서 필수 `timezone`을 제거하고 `page`·`size`만 사용 |
|
||||
| 댓글 `date` | 처리 완료 | owner-scoped 조회 결과를 `createdAt.toUtcIso()`로 재매핑해 UTC `date-time` 반환 |
|
||||
| 기존 댓글 의미 | 유지 | pagination·ownership·block/secret·mutation 정책 변경 없이 영향 범위 회귀 통과 |
|
||||
| legacy/public 격리 | 충족 | 기존 community 댓글 timezone service/repository 계약을 변경하지 않음 |
|
||||
| OpenAPI 상태 | 처리 완료 | 영향 2개 operation을 `implemented`로 동기화 |
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
- 신규 Task: `Task 5.12` / `P5-R6`
|
||||
- Gate: `P5-R6-GATE`
|
||||
- 시작 조건: `P3-R14-GATE`
|
||||
- 완료 조건: 댓글·답글 actual GET UTC exact JSON, 기존 pagination/ownership/block/secret 의미와
|
||||
legacy/public 회귀
|
||||
|
||||
### `P5-R6` / `P5-R6-GATE` 처리 결과
|
||||
|
||||
- RED: production 변경 전 `AiCharacterAdminCommunityPostCommentTest`는 timezone 없는 root/reply GET이 400을 반환해 2건 실패했고 `BUILD FAILED in 38s`였다.
|
||||
- GREEN: controller/facade의 timezone 입력·검증을 제거하고 legacy 조회 결과의 `date`만 `createdAt.toUtcIso()`로 재매핑한 뒤 같은 focused 테스트는 `BUILD SUCCESSFUL in 43s`였다.
|
||||
- Gate: community/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 11s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였고, OpenAPI는 36개 `implemented`와 0개 `alignment-required`, `git diff --check`는 출력 없음을 확인했다.
|
||||
|
||||
**최종 결론:** `REV-051` 처리 완료, Phase 5 UTC 계약 정합화 완료
|
||||
|
||||
**다음 Goal:** `P7-R7`.
|
||||
|
||||
## 17. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature E, OpenAPI Community 8개 operation
|
||||
- 검토 범위: Community controller의 multipart/JSON mapping, 댓글 facade와 관련 actual endpoint 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-053` — High — 처리 완료
|
||||
|
||||
- OpenAPI는 댓글 POST와 PUT의 requestBody media type을 `application/json` 하나로 정의하고 415 response를 선언한다.
|
||||
- 검토 당시 두 controller mapping에는 `consumes = [MediaType.APPLICATION_JSON_VALUE]`가 없었다.
|
||||
- body를 `String`으로 받으므로 mapping 단계에서 media type을 제한하지 않으면 `text/plain` 같은 요청이
|
||||
`HttpMediaTypeNotSupportedException`으로 차단되지 않고 handler/parser까지 진입할 수 있다.
|
||||
- 기존 댓글 테스트는 정상·오류 JSON 요청을 모두 `application/json`으로만 보내 미지원 media type과
|
||||
415 `Accept` header/no-side-effect를 고정하지 않는다.
|
||||
- 외부 HTTP 요청 수용 범위와 명시된 415가 달라 High로 판정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 5.13` / `P5-R7`
|
||||
- Gate: `P5-R7-GATE`
|
||||
- 최소 수정: 댓글 POST·PUT mapping에 JSON `consumes` 추가
|
||||
- 완료 조건: 정상 JSON 회귀, 미지원 media type의 KO/EN/JA 415 envelope, `Accept` header,
|
||||
작성 insert/event 0회와 수정 row 불변
|
||||
- 범위 밖: facade/parser·댓글 actor/owner/parent 의미, OpenAPI·legacy/public API 변경
|
||||
|
||||
### 처리 결과
|
||||
|
||||
- 댓글 POST·PUT mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가했다.
|
||||
- actual endpoint 회귀에서 KO/EN/JA `text/plain` 요청의 localized 415 `ApiResponse.error`, `Accept: application/json`,
|
||||
작성 insert/event 0회와 수정 row 불변을 확인했다.
|
||||
|
||||
### `REV-058` — High — 처리 완료
|
||||
|
||||
- OpenAPI와 계약 설명은 게시글 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
|
||||
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
|
||||
- 정상 테스트는 JSON media type만 사용하며 미지원/누락 part media type의 415 `Accept` header와
|
||||
S3/DB/event no-side-effect를 고정하지 않는다.
|
||||
- 같은 shared converter와 signature에서 AudioContent의 `text/plain` 성공 테스트가 있어 permissive binding을
|
||||
정적으로 확인할 수 있다.
|
||||
|
||||
### 추가 plan 전환
|
||||
|
||||
- 신규 Task: `Task 5.14` / `P5-R8`
|
||||
- Gate: `P5-R8-GATE`
|
||||
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
|
||||
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
|
||||
S3/DB/event no-side-effect
|
||||
|
||||
### `P5-R8` / `P5-R8-GATE` 처리 결과
|
||||
|
||||
- RED: production 변경 전 `AiCharacterAdminCommunityPostCreateTest`와 `AiCharacterAdminCommunityPostUpdateTest`에
|
||||
KO/EN/JA `text/plain` 및 Content-Type 누락 `request` part 415 matrix를 추가했고, focused 명령은 12개 invocation이
|
||||
415 기대 실패로 `BUILD FAILED in 56s`였다.
|
||||
- GREEN: controller POST·PUT 경계에서 `request` part의 JSON 호환 media type만 확인하도록 추가했다. facade strict reader,
|
||||
media/fixed/owner 의미, OpenAPI schema는 변경하지 않았다.
|
||||
- Gate: 같은 focused 명령은 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위와
|
||||
`AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 55s`였다.
|
||||
|
||||
**최종 결론:** `REV-053`, `REV-058` 처리 완료, Phase 5 HTTP media type 계약 정합화 완료
|
||||
|
||||
**다음 Goal:** `P6-R3`.
|
||||
|
||||
## 18. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature E, OpenAPI `CommunityPostCreateMultipart`·`CommunityPostUpdateMultipart`
|
||||
- 검토 범위: Community POST·PUT controller의 multipart binding과 media/fixed/owner mutation 테스트
|
||||
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### `REV-063` — Medium — 생성·수정의 서로 다른 허용 part 집합을 강제하지 않음
|
||||
|
||||
- OpenAPI는 생성에 `audioFile`, `postImage`, `request`, 수정에 `postImage`, `request`만 허용하고 두 schema 모두
|
||||
`additionalProperties: false`다.
|
||||
- controller는 각 `@RequestPart`와 `request` media type만 처리하며 전체 part 이름을 검사하지 않는다.
|
||||
- 특히 수정 요청에 OpenAPI가 금지한 `audioFile`이나 임의 `unexpected` part를 추가해도 해당 part가 무시된 채 게시글
|
||||
mutation이 진행될 수 있다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 5.15` / `P5-R9` |
|
||||
| Gate | `P5-R9-GATE` |
|
||||
| RED | 생성·수정 미정의 part, 수정 `audioFile`, S3·DB·event 결과 |
|
||||
| GREEN | 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` exact allow-list |
|
||||
| 회귀 | 정상 media/fixed/owner, request part 누락·415 |
|
||||
|
||||
### `P5-R9` / `P5-R9-GATE` 처리 결과
|
||||
|
||||
- RED: production 변경 전 Community POST `unexpected`, PUT `unexpected`·`audioFile` actual endpoint 테스트를 추가했고,
|
||||
focused 명령은 3개 케이스 모두 400 기대 실패로 `BUILD FAILED in 3m 23s`였다.
|
||||
- GREEN: Community controller POST는 `{audioFile, postImage, request}`, PUT은 `{postImage, request}` exact allow-list를
|
||||
적용해 초과 part를 400 `common.error.invalid_request`로 거부한다. media/fixed/owner 의미와 OpenAPI schema는 변경하지 않았다.
|
||||
- Gate: focused 명령은 `BUILD SUCCESSFUL in 2m 30s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 44s`,
|
||||
`ktlintCheck`는 `BUILD SUCCESSFUL in 55s`였다.
|
||||
|
||||
**최종 결론:** `REV-063` 처리 완료, Phase 5 multipart part 이름 계약 정합화 완료
|
||||
|
||||
**다음 Goal:** `P7-R9`.
|
||||
|
||||
## 19. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: OpenAPI Community create/update multipart schema의 operation별 허용 part와
|
||||
`additionalProperties: false`
|
||||
- 검토 범위: Community POST·PUT controller와 미정의 part·media/fixed/owner 회귀 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-069` — Medium — 일반 form-field multipart part가 allow-list 우회
|
||||
|
||||
- `AiCharacterAdminCommunityPostController.kt:63-69`는 operation별 allow-list를 받지만 실제 검사는
|
||||
`fileMap.keys`에 한정한다.
|
||||
- 기존 create/update 회귀는 `AiCharacterAdminCommunityPostCreateTest.kt:202-210`과
|
||||
`AiCharacterAdminCommunityPostUpdateTest.kt:256-268`에서 filename이 있는 `MockMultipartFile`만 사용한다.
|
||||
- filename 없는 일반 form-field `unexpected`는 생성 `{audioFile, postImage, request}`, 수정
|
||||
`{postImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 5.16` / `P5-R10` |
|
||||
| Gate | `P5-R10-GATE` |
|
||||
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·event no-side-effect |
|
||||
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
|
||||
| 범위 제한 | media/fixed/concurrency·OpenAPI·전역 resolver·legacy/public 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 5 보완 필요 — `REV-069` 확정
|
||||
|
||||
**다음 Goal:** `P5-R10` (`P4-R10-GATE` 완료 후).
|
||||
|
||||
## 20. 8차 후속 수정 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-069`의 Community POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
|
||||
- 왜: 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 mutation 전 거부되지 않았기 때문이다.
|
||||
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
|
||||
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
|
||||
|
||||
**최종 결론:** `REV-069` 처리 완료. Phase 5 후속 Gate 완료.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 21. 9차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F의 Community 요구사항, OpenAPI Community 8개 operation
|
||||
- 검토 범위: 게시글 목록·생성·수정, 댓글 CRUD, owner/actor/parent, multipart·concurrency 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정 및 plan 전환
|
||||
|
||||
- Community 8개 operation과 target owner, 댓글 actor/parent, exact multipart, 고정 제한 동시성 경계를 대조했다.
|
||||
- 기존 완료 finding 이후 신규 확정 finding은 없다.
|
||||
- Phase 5 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 5 추가 수정 없음.
|
||||
|
||||
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
|
||||
|
||||
## 22. 10차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature E, OpenAPI Community 8개 operation
|
||||
- 검토 범위: 게시글 목록·생성·수정, 고정 동시성, 댓글 CRUD, owner/actor/parent와 multipart 경계
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- Community 8개 operation과 controller mapping, active owner pagination wrapper·고정 우선 정렬이 일치한다.
|
||||
- 게시글 생성·수정의 strict request와 operation별 multipart part, 최대 고정 3개 owner lock·soft delete 정리가 유지된다.
|
||||
- 댓글의 target AI 작성/수정, 동일 게시글 활성 원댓글, row-only soft delete와 UTC 응답 계약이 유지된다.
|
||||
- 신규 확정 finding이 없어 Phase 5 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 5 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
488
docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md
Normal file
488
docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# Phase 6 FanTalk 관리 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 6 / FanTalk 2개 operation |
|
||||
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 6~7 working tree |
|
||||
| 리뷰 일자 | 2026-07-28 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- PRD Feature F와 OpenAPI FanTalk 2개 operation을 관리자/public v2 query policy, controller, facade, 테스트에 대조한다.
|
||||
- pagination 보정과 reply JSON/side-effect 경계를 점검한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- 관리자 FanTalk controller/facade/repository/DTO와 관련 테스트
|
||||
- 공개 v2 `CreatorChannelFanTalkQueryPolicy`
|
||||
- OpenAPI FanTalk path/parameter/schema와 plan Phase 6
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- 공개 v2 정책 변경
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | cross-owner reply 또는 데이터 손실 |
|
||||
| High | 승인된 공개 v2 parity나 주요 조회 계약 위반 |
|
||||
| Medium | reply request schema·오류 계약의 제한된 위반 |
|
||||
| Low | 유지보수성 또는 문서 정합성 문제 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
| 근거 | 판정 |
|
||||
|---|---|
|
||||
| OpenAPI `FanTalkPage`/`FanTalkSize` `:520`~`:521` | page는 0 이상, size는 20..50으로 보정 |
|
||||
| `CreatorChannelFanTalkQueryPolicy.kt:8`~`:12`, `:23`~`:28` | 공개 v2가 실제로 같은 보정을 수행 |
|
||||
| `AiCharacterAdminFanTalkFacade.kt:30`~`:55` | 관리자는 범위 밖 값을 400으로 거부하고 size 1도 허용 |
|
||||
| `AiCharacterAdminFanTalkQueryTest.kt:120`~`:189` | size 1 성공과 -1/0/51 거부를 테스트가 반대 계약으로 고정 |
|
||||
| OpenAPI `FanTalkReplyCreateRequest` `:1238`~`:1242` | `content` required, `additionalProperties: false` |
|
||||
| `AiCharacterAdminFanTalkController.kt:27`~`:33` | reply를 기본 `@RequestBody` DTO binding으로 수신 |
|
||||
| reply contract test `:50`~`:100` | blank/malformed/missing은 검증하지만 미지 필드는 검증하지 않음 |
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| query policy·facade·test 정적 대조 | 성공 | pagination 구현과 테스트가 PRD/OpenAPI에 반대임을 확인 |
|
||||
| reply schema/binding 정적 대조 | 성공 | unknown-field strict 경계 누락 확인 |
|
||||
| Gradle/컴파일/테스트 | 실행 | `P6-R1` RED/GREEN focused test 수행 |
|
||||
| `P6-R1` RED | 성공 | pagination 400과 reply unknown-field 허용으로 4건 실패 확인 |
|
||||
| `P6-R1` GREEN | 성공 | focused 재실행 `BUILD SUCCESSFUL in 3m 14s` |
|
||||
| `P6-R1-GATE` FanTalk/common 회귀 | 성공 | `BUILD SUCCESSFUL in 1m 36s` |
|
||||
| `P6-R1-GATE` lint/diff | 성공 | `ktlintCheck` `BUILD SUCCESSFUL in 35s`, `git diff --check` 출력 없음 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-027` | High | 처리 완료 | 관리자 목록 pagination이 공개 v2 보정 정책과 반대 | `Task 6.6` | `P6-R1-GATE` |
|
||||
| `REV-028` | Medium | 처리 완료 | reply body가 미지 JSON 필드를 허용 | `Task 6.6` | `P6-R1-GATE` |
|
||||
|
||||
## 6. 발견 사항 상세
|
||||
|
||||
### REV-027 — FanTalk pagination 보정 불일치
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** PRD Feature F, 공개 v2 응답/query policy parity
|
||||
- **관련 계약:** page default 0·최소 0 보정, size default 20·20..50 보정
|
||||
- **소유 Task:** `Task 6.6`, `P6-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
관리자 facade는 음수 page, size 0, size 51을 400으로 거부하고 size 1을 허용한다. 공개 v2 policy와 OpenAPI는 각각
|
||||
page 0, size 20, size 50으로 보정해야 하며 size 1도 20으로 올려야 한다. 현재 query 테스트가 잘못된 구현을 의도한
|
||||
동작으로 고정한다.
|
||||
|
||||
**후속 수정 결과**
|
||||
|
||||
관리자 목록 facade가 공개 v2 `CreatorChannelFanTalkQueryPolicy`를 재사용하도록 변경되어 `page < 0 -> 0`,
|
||||
`size < 20 -> 20`, `size > 50 -> 50` 보정이 actual endpoint 테스트로 고정됐다.
|
||||
|
||||
**영향**
|
||||
|
||||
OpenAPI client가 보정 계약을 신뢰하면 관리자 endpoint에서 예상하지 못한 400을 받으며, size 1 요청의 응답 metadata도
|
||||
계약과 달라진다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
공개 v2 query policy와 동일한 정규화를 적용하고 기존 pagination 테스트를 경계값 기반으로 교정한다.
|
||||
|
||||
### REV-028 — FanTalk reply unknown-field 미거부
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** OpenAPI request schema 준수, 잘못된 request no-side-effect
|
||||
- **관련 계약:** `FanTalkReplyCreateRequest.additionalProperties: false`
|
||||
- **소유 Task:** `Task 6.6`, `P6-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
controller의 기본 DTO binding은 malformed/missing content는 거부하지만 계약 밖 필드를 무시한다. repository 전역 설정에
|
||||
unknown property 실패 설정이 없고 현재 contract test도 extra field를 다루지 않는다.
|
||||
|
||||
**후속 수정 결과**
|
||||
|
||||
reply controller는 raw JSON 문자열을 facade로 넘기고, facade가 `FAIL_ON_UNKNOWN_PROPERTIES` strict reader로
|
||||
`AiCharacterAdminFanTalkReplyRequest`를 역직렬화한다. 미지 필드 요청은 400 `common.error.invalid_request`, reply insert
|
||||
0건, `LanguageDetectEvent` 0회로 actual endpoint 테스트에 고정됐다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
reply body만 strict parse하고 미지 필드가 있으면 400 `common.error.invalid_request`, reply insert 0건,
|
||||
`LanguageDetectEvent` 0회를 actual endpoint로 고정한다.
|
||||
|
||||
## 7. plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 6에 두 finding을 함께 처리하는 `Task 6.6` / `P6-R1`과 `P6-R1-GATE`를 추가했다. 기존
|
||||
`P6-GATE` 완료 이력은 유지한다.
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| operation/path 대조 | 충족 | FanTalk 2개 route 존재 |
|
||||
| 공개 v2 pagination parity | 충족 | 공개 v2 query policy 재사용과 경계값 actual test 통과 |
|
||||
| reply ownership/storage 추적 | 충족 | active owner root 선검증과 target creator 저장 확인 |
|
||||
| reply JSON schema | 충족 | unknown-field 400/no insert/no event actual test 통과 |
|
||||
| 실행 검증 | 충족 | focused, FanTalk/common 회귀, lint, diff 성공 |
|
||||
|
||||
**최종 결론:** Phase 6 후속 리뷰 종료
|
||||
|
||||
**남은 항목:** 없음. 다음 Goal은 `P7-R1`이다.
|
||||
|
||||
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
|
||||
- 검토 범위: 관리자 root/reply query, 공개 v2 pagination policy, reply strict JSON·owner/root/active 검증,
|
||||
writer/creator 저장과 언어 감지 event 테스트
|
||||
- 검증 방식: 코드·문서·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 목록 query/pagination | 충족 | 공개 v2 `CreatorChannelFanTalkQueryPolicy` 재사용 |
|
||||
| reply JSON | 충족 | strict reader와 blank/malformed/unknown-field 거부 |
|
||||
| root/ownership | 충족 | active owner root만 조회하고 nested/cross-owner를 저장 전 차단 |
|
||||
| writer/event | 충족 | target creator를 writer/creator로 저장하고 언어 감지 event 발행 |
|
||||
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 6 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음. `P7-R2` 통합 재판정에서 기존 FanTalk/common 회귀만 확인한다.
|
||||
|
||||
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
|
||||
- 검토 범위: 관리자 목록·답변 facade, 공개 v2 pagination policy, strict JSON, root ownership과 event 테스트
|
||||
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 목록 query | 충족 | 공개 v2 page/size 보정 정책 재사용 |
|
||||
| reply JSON | 충족 | malformed/blank/unknown-field 저장 전 거부 |
|
||||
| root/ownership | 충족 | active owner root만 허용하고 nested/cross-owner 차단 |
|
||||
| writer/event | 충족 | target creator 저장과 언어 감지 event 유지 |
|
||||
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 6 추가 수정 없음
|
||||
|
||||
**남은 항목:** `P7-R3`에서 FanTalk/common 회귀를 통합 재검증한다.
|
||||
|
||||
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
|
||||
- 검토 범위: 관리자 root/reply 목록, pagination policy, strict reply JSON, root ownership·event
|
||||
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
확정 발견 사항 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 목록/pagination | 충족 | 공개 v2 query policy와 root/reply owner query 유지 |
|
||||
| reply JSON | 충족 | malformed·blank·unknown field를 저장 전에 거부 |
|
||||
| root/ownership | 충족 | active owner root만 답변 허용 |
|
||||
| writer/event | 충족 | target creator를 writer/creator로 저장하고 언어 감지 발행 |
|
||||
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
|
||||
|
||||
**최종 결론:** Phase 6 추가 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위
|
||||
|
||||
- OpenAPI FanTalk 2개 operation과 controller/facade/query 구현
|
||||
- reply request의 required/non-null, malformed·unknown·blank 입력 처리
|
||||
- pagination 보정, root ownership, writer/event 경계
|
||||
|
||||
### 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 목록/pagination | 충족 | 공개 v2 query policy 보정과 owner root/reply query 유지 |
|
||||
| reply JSON | 충족 | request는 non-null `String content` 하나이며 null/malformed/blank/unknown을 저장 전 거부 |
|
||||
| root/ownership | 충족 | active owner root만 답변 허용 |
|
||||
| primitive nullability | 해당 없음 | FanTalk JSON request에 primitive 필드가 없음 |
|
||||
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 6 신규 수정 없음
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 13. 팬 작성 FanTalk 원글 삭제 후속 검토 — 2026-07-29
|
||||
|
||||
### 확인 결과
|
||||
|
||||
- **`REV-049` / High / 구현 대기:** 신규 v2 관리자 경계에 target 채널의 팬 작성 FanTalk root를 삭제할
|
||||
operation이 없다.
|
||||
- 삭제는 팬 작성 root row만 `isActive=false`로 변경하고 연결 creator reply row는 유지한다.
|
||||
- target AI가 작성한 row, reply row, 다른 채널 root는 거부하며 이미 비활성인 같은 target 팬 root는 성공 no-op이다.
|
||||
- 캐릭터 직접 댓글 삭제는 v2 미사용 API로 별도 구현하지 않는다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 6.7` / `P6-R2`
|
||||
- Gate: `P6-R2-GATE`
|
||||
- 범위 밖: hard delete·cascade, FanTalk 원글 작성, public v2 endpoint 변경
|
||||
|
||||
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** Phase 6 팬 작성 FanTalk 원글 삭제 구현 필요
|
||||
|
||||
**다음 Goal:** `P6-R2`.
|
||||
|
||||
## 14. 팬 작성 FanTalk 원글 삭제 구현 검토 — 2026-07-29
|
||||
|
||||
### 구현 결과
|
||||
|
||||
- `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`를 추가했다.
|
||||
- target 채널의 팬 작성 root만 `CreatorCheers.isActive=false`로 변경한다.
|
||||
- 연결 creator reply row는 변경하지 않고, 목록·`fanTalkCount`에서는 삭제된 root가 제외된다.
|
||||
- target AI 작성 root, reply row, 다른 채널 root, 비활성 target, 누락 ID는 400/no mutation으로 거부한다.
|
||||
- 같은 target의 이미 비활성인 팬 root는 200 no-op으로 처리한다.
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkDeleteTest` | 성공 | RED 6건 미구현 route 실패 확인 후 GREEN focused `BUILD SUCCESSFUL in 29s` |
|
||||
| `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest` | 성공 | DELETE 인가 matrix 보강 후 `BUILD SUCCESSFUL in 29s` |
|
||||
| `./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` | 성공 | FanTalk/common 영향 범위 회귀 `BUILD SUCCESSFUL in 58s` |
|
||||
| `./gradlew ktlintCheck` | 성공 | `BUILD SUCCESSFUL in 14s` |
|
||||
| `git diff --check` | 성공 | 출력 없음 |
|
||||
|
||||
**최종 결론:** `REV-049` 처리 완료. Phase 6 후속 Gate 완료.
|
||||
|
||||
**다음 Goal:** `P7-R6`.
|
||||
|
||||
## 15. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F, OpenAPI FanTalk 3개 operation
|
||||
- 검토 범위: FanTalk controller의 query/JSON mapping, reply strict parser·저장 경계와 관련 테스트
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-054` — High — 처리 완료
|
||||
|
||||
- OpenAPI는 reply POST의 requestBody media type을 `application/json` 하나로 정의하고 415 response를 선언한다.
|
||||
- reply controller mapping에는 `consumes = [MediaType.APPLICATION_JSON_VALUE]`가 없다.
|
||||
- body를 `String`으로 받으므로 미지원 media type이 mapping 단계에서 차단되지 않고 handler/parser까지 진입할 수 있다.
|
||||
- 기존 reply 계약·생성·ownership 테스트는 `application/json` 요청만 사용해 415 `Accept` header와
|
||||
insert/event no-side-effect를 고정하지 않는다.
|
||||
- 외부 HTTP 요청 수용 범위와 명시된 415가 달라 High로 판정한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
- 신규 Task: `Task 6.8` / `P6-R3`
|
||||
- Gate: `P6-R3-GATE`
|
||||
- 최소 수정: reply POST mapping에 JSON `consumes` 추가
|
||||
- 완료 조건: 정상 JSON 축약 응답 회귀, 미지원 media type의 KO/EN/JA 415 envelope, `Accept` header,
|
||||
reply insert/event 0회
|
||||
- 범위 밖: strict parser·root/ownership·언어 감지, 목록/삭제, OpenAPI·legacy/public API 변경
|
||||
|
||||
### `P6-R3` / `P6-R3-GATE` 처리 결과
|
||||
|
||||
- RED: production 변경 전 `AiCharacterAdminFanTalkReplyContractTest`에 KO/EN/JA `text/plain` reply POST 415 matrix를
|
||||
추가했고, focused 명령은 3개 invocation이 415 기대 실패로 `BUILD FAILED in 33s`였다.
|
||||
- GREEN: reply POST mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가했다. strict parser,
|
||||
root/ownership, 언어 감지, 목록/삭제, OpenAPI schema는 변경하지 않았다.
|
||||
- Gate: 같은 focused 명령은 `BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위와
|
||||
`AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 47s`였다.
|
||||
|
||||
**최종 결론:** `REV-054` 처리 완료. Phase 6은 `P6-R4` 완료 전 종결할 수 없다.
|
||||
|
||||
**다음 Goal:** `P6-R4`.
|
||||
|
||||
## 16. FanTalk 답변 수정 계약 검토 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 근거
|
||||
|
||||
- 요청: FanTalk 답변을 수정하는 V2 관리자 API 추가, 레거시 `PUT /explorer/profile/cheers` 계약 유지
|
||||
- 레거시 근거: `ExplorerController.modifyCheers`, `ExplorerService.modifyCheers`, `PutWriteCheersRequest`,
|
||||
`CreatorChannelFanTalkResponse`
|
||||
- 현재 V2 근거: FanTalk controller/facade/repository/DTO와 목록·답변 작성·팬 원글 삭제 3개 operation
|
||||
- 검증 방식: 문서·레거시·현재 V2 코드 정적 대조와 `./gradlew tasks --all` 프로젝트 인식 확인. 사용자 지시에 따라
|
||||
컴파일·테스트·lint는 실행하지 않았다.
|
||||
|
||||
### `REV-059` — High — FanTalk 답변 수정 V2 관리자 operation 부재
|
||||
|
||||
- 처리 전 V2 관리자 FanTalk에는 선택한 AI 캐릭터가 작성한 기존 reply의 내용이나 활성 상태를 수정할 route가 없었다.
|
||||
- 레거시 request는 `cheersId`와 optional/nullable `content`, `isActive`를 받고 non-null 값만 반영한다. 두 필드를
|
||||
함께 입력할 수 있고 `{}` 또는 explicit null은 성공 no-op이다.
|
||||
- 레거시는 비활성 row도 조회하므로 `isActive=true` 재활성화가 가능하고, 수정 시 `languageCode`와 event를 변경하지 않는다.
|
||||
- 성공 `data`는 `CreatorChannelFanTalkResponse`이며 reply row를 매핑하므로 `fanTalkId`는 reply ID,
|
||||
`creatorReplies`는 빈 배열이다.
|
||||
- 관리자 V2에서는 위 계약에 `characterId`, root `fanTalkId`, `replyId` path를 적용하고 target AI가 writer이자
|
||||
creator이며 지정한 활성 root의 direct child인 reply로 소유 경계를 강화해야 한다.
|
||||
|
||||
### 확정 계약과 plan 전환
|
||||
|
||||
- 신규 operation:
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`
|
||||
- request: optional/nullable `content`, `isActive`; 동시 입력과 빈 객체 no-op 허용, JSON-only·미지 필드 거부
|
||||
- response: 레거시 `CreatorChannelFanTalkResponse` 필드 형태
|
||||
- inactive reply 재활성화 허용, inactive root·cross-target/root·팬 작성 row·direct-parent mismatch는 400/no mutation
|
||||
- 신규 Task: `Task 6.9` / `P6-R4`
|
||||
- Gate: `P6-R4-GATE`
|
||||
- OpenAPI 상태: 전체 37개 operation 모두 `implemented`
|
||||
|
||||
### 구현 결과와 Gate
|
||||
|
||||
- RED: `AiCharacterAdminFanTalkReplyUpdateTest`와 `AiCharacterAdminFanTalkReplyUpdateContractTest` 신규 15건이
|
||||
미구현 route 404로 `BUILD FAILED in 49s`였다.
|
||||
- GREEN: 신규 PUT route, JSON `consumes`, strict request DTO, active root와 target AI writer/creator direct reply를
|
||||
검증하는 repository query, non-null field만 반영하는 facade를 추가했다.
|
||||
- Gate: 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`는 출력이 없었다.
|
||||
|
||||
**최종 결론:** `REV-059` 처리 완료. Phase 6의 P6-R3/P6-R4 후속 보완은 완료됐다.
|
||||
|
||||
**다음 Goal:** `P7-R8`.
|
||||
|
||||
## 17. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F, OpenAPI FanTalk 4개 operation
|
||||
- 검토 범위: 목록 pagination, 답변 작성·수정 JSON 경계, 팬 root 삭제, target/root/reply ownership
|
||||
- 검증 방식: 현재 working tree의 문서·코드·관련 테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
|
||||
실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
| 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| route/operation | 충족 | FanTalk 4개 OpenAPI operation과 controller mapping 일치 |
|
||||
| JSON request | 충족 | 답변 작성·수정의 JSON-only mapping과 strict unknown-field 거부 유지 |
|
||||
| ownership/state | 충족 | active root, target AI direct reply, fan root soft delete 조건 유지 |
|
||||
| 7차 multipart finding 영향 | 없음 | FanTalk에는 multipart request가 없음 |
|
||||
|
||||
### finding 및 plan 전환
|
||||
|
||||
- 신규 Phase 6 finding 없음.
|
||||
- Phase 6 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 6 추가 수정 없음
|
||||
|
||||
**남은 항목:** `P7-R9` 통합 재판정.
|
||||
|
||||
## 18. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F/Edge Cases, OpenAPI FanTalk DELETE description, `api-contract.md`
|
||||
- 검토 범위: FanTalk root delete facade/repository와 `AiCharacterAdminFanTalkDeleteTest`
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### `REV-070` — Low — 비활성 팬 root 삭제 설명 상충
|
||||
|
||||
- OpenAPI `api-contract.openapi.json:785`와
|
||||
`AiCharacterAdminFanTalkDeleteTest.kt:76-101`은 같은 target의 이미 비활성인 팬 root 삭제를 성공 no-op으로
|
||||
정의한다. `api-contract.md:229-231`도 같은 결과를 설명한다.
|
||||
- 반면 PRD `prd.md:227-228`은 비활성 root를 400 거부 대상으로 묶고, `api-contract.md:41`도 “활성 root만”이라고
|
||||
적어 같은 문서 안에서 뒤쪽 no-op 설명과 상충한다.
|
||||
- 기계 계약인 OpenAPI와 현재 구현·회귀가 일치하므로 runtime 변경보다 설명 문서를 no-op 계약에 맞추는 최소 보완이
|
||||
적절하다. 실행 오류가 아니라 문서 불일치이므로 Low로 판정한다.
|
||||
- 이 판정은 OpenAPI를 기계 계약 원본으로 두고 구현·테스트와 일치하는 쪽을 유지한 결과다. PRD의 400 문장이 최신 제품
|
||||
의도라면 `P6-R5`를 실행하기 전에 OpenAPI와 runtime/test까지 변경하는 별도 범위로 재확정해야 한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 6.10` / `P6-R5` |
|
||||
| Gate | `P6-R5-GATE` |
|
||||
| 변경 | PRD와 `api-contract.md`의 상충 문장만 OpenAPI/runtime no-op 계약에 동기화 |
|
||||
| TDD 예외 | 문서 전용 Task이며 OpenAPI·구현·test 소스 정적 대조로 검증 |
|
||||
| 범위 제한 | runtime/test/OpenAPI·legacy/public 변경 없음 |
|
||||
|
||||
**최종 결론:** Phase 6 문서 보완 필요 — `REV-070` 확정
|
||||
|
||||
**다음 Goal:** `P6-R5` (`P5-R10-GATE` 완료 후).
|
||||
|
||||
## 19. 8차 후속 문서 정합화 및 Gate — 2026-07-29
|
||||
|
||||
- 무엇을: `REV-070`의 FanTalk 비활성 팬 root 삭제 설명 상충을 정리했다.
|
||||
- 왜: OpenAPI·구현·`AiCharacterAdminFanTalkDeleteTest`는 같은 target의 이미 비활성인 팬 root 삭제를 200 `data:null` no-op으로 고정하지만 PRD 일부 문장이 400 거부로 설명했기 때문이다.
|
||||
- 어떻게: PRD Edge Cases와 `api-contract.md` 삭제 설명을 같은 target 비활성 팬 root no-op, creator root·reply·다른 target·미존재 root 400으로 동기화했다. runtime/test/OpenAPI는 변경하지 않았다.
|
||||
- 결과: 문서-only 보완으로 `REV-070` 처리 완료. 정적 대조와 diff check 결과는 `P7-R10-GATE`에 통합 기록한다.
|
||||
|
||||
**최종 결론:** `REV-070` 처리 완료. Phase 6 후속 Gate 완료.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 20. 9차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F/Edge Cases, OpenAPI FanTalk 4개 operation
|
||||
- 검토 범위: 목록, 답변 작성·수정, 팬 root 삭제, target/root/direct reply ownership
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정 및 plan 전환
|
||||
|
||||
- FanTalk 4개 operation과 pagination, strict JSON, target AI reply ownership, root 삭제 no-op 계약을 대조했다.
|
||||
- 기존 완료 finding 이후 신규 확정 finding은 없다.
|
||||
- Phase 6 신규 Task/Gate 없음.
|
||||
|
||||
**최종 결론:** Phase 6 추가 수정 없음.
|
||||
|
||||
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
|
||||
|
||||
## 21. 10차 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F, OpenAPI FanTalk 4개 operation
|
||||
- 검토 범위: 목록, creator reply 작성·수정, 팬 root 삭제와 target/root/direct reply ownership
|
||||
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
|
||||
테스트는 실행하지 않았다.
|
||||
|
||||
### 판정
|
||||
|
||||
- FanTalk 4개 operation과 controller mapping, 공개 v2 page/size 보정·응답 필드가 일치한다.
|
||||
- 답변 작성은 active root와 target creator writer/creator를, 수정은 target의 active root direct reply를 검증한다.
|
||||
- 팬 root row-only soft delete와 동일 target 비활성 root 성공 no-op 계약이 문서·구현에 일치한다.
|
||||
- 신규 확정 finding이 없어 Phase 6 회귀 수정 Task/Gate를 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 6 요구사항 충족, 추가 수정 없음.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
759
docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md
Normal file
759
docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# Phase 7 통합·문서 정합성 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 7 / 23개 operation 통합 상태와 문서 추적성 |
|
||||
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 2~7 working tree |
|
||||
| 리뷰 일자 | 2026-07-28 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.md`, `api-contract.openapi.json` |
|
||||
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- Phase 7 완료 기록과 실제 신규 prefix controller/OpenAPI operation 수를 대조한다.
|
||||
- 계획, 사람이 읽는 계약 설명, OpenAPI 구현 상태 metadata가 현재 구현 상태를 정확히 나타내는지 확인한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- 신규 prefix controller mapping 전체
|
||||
- OpenAPI operation과 `x-implementation-status`
|
||||
- plan 현재 상태/Endpoint Contract Summary/Phase 7 Progress
|
||||
- `api-contract.md` 구현 현황
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- Phase 2~6 finding의 production 수정, API schema 변경
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | 인수·배포 판정을 무효화하는 미구현 핵심 기능 |
|
||||
| High | operation 누락 또는 공개 schema 불일치 |
|
||||
| Medium | 완료 상태·생성 client 판단에 영향을 주는 metadata/문서 불일치 |
|
||||
| Low | 비핵심 설명·형식 정합성 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
| 근거 | 판정 |
|
||||
|---|---|
|
||||
| OpenAPI 정적 집계 | 23개 operation: Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 |
|
||||
| controller mapping 정적 집계 | 후속 Gate 후 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2로 총 23개 |
|
||||
| OpenAPI status 정적 집계 | 후속 수정 후 23개 `implemented` |
|
||||
| `api-contract.md:9`~`:12` | 후속 수정 후 endpoint 23개, route 구현 23개, 구현 완료 23개, 예정 0개 |
|
||||
| plan Endpoint Contract Summary | 후속 수정 후 5개 domain 모두 구현 완료로 표시 |
|
||||
| plan `P7-GATE`와 후속 기록 | 23개 operation 구현 완료로 판정 |
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| `jq` operation/status 집계 | 성공 | operation 23, status 9/14 확인 |
|
||||
| controller annotation `rg` 집계 | 성공 | mapping 24, 초과 1개는 Series DELETE |
|
||||
| `P7-R1` 후속 `jq` operation/status 집계 | 성공 | operation 23, implemented 23 |
|
||||
| `P7-R1` 후속 controller annotation 집계 | 성공 | mapping 23 |
|
||||
| OpenAPI validate/client 생성/compile | 성공 | validate 이슈 없음, TypeScript compile exit 0 |
|
||||
| Gradle/문서 diff | 성공 | `./gradlew tasks --all` 성공, `git diff --check` 출력 없음 |
|
||||
| `P7-R1-GATE` 최종 대조 | 성공 | OpenAPI 23개 implemented, controller mapping 23개, 미처리 finding 0건 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-029` | Medium | 처리 완료 | 구현 완료 기록과 계약 metadata/현황 문서 불일치 | `Task 7.3` | `P7-R1-GATE` |
|
||||
|
||||
## 6. 발견 사항 상세
|
||||
|
||||
### REV-029 — 구현 상태 metadata와 완료 기록 불일치
|
||||
|
||||
- **심각도:** Medium
|
||||
- **상태:** 처리 완료
|
||||
- **관련 요구사항:** Phase 7 API contract·diff·문서 추적성 완료 조건
|
||||
- **관련 계약:** OpenAPI 23개 operation과 실제 controller mapping 일치
|
||||
- **소유 Task:** `Task 7.3`, `P7-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
Phase 7 완료 기록은 모든 operation 구현을 선언하지만 `api-contract.md`, plan Endpoint Contract Summary,
|
||||
OpenAPI `x-implementation-status`는 계약 확정 당시의 9개 정합화 필요/14개 예정 상태를 유지한다. 실제 controller는
|
||||
23개가 아니라 계약 밖 Series DELETE를 포함한 24개다.
|
||||
|
||||
**후속 수정 결과**
|
||||
|
||||
`P4-R1-GATE`에서 계약 밖 Series DELETE route를 제거했고, `P7-R1`에서 plan/API 설명/OpenAPI status를 실제 구현 상태와
|
||||
동기화했다. OpenAPI는 23개 operation 모두 `implemented`이며, 신규 prefix controller mapping도 23개로 일치한다.
|
||||
|
||||
**영향**
|
||||
|
||||
문서 독자와 생성 도구가 구현 완료 여부를 다르게 판단하며, Phase 7의 “OpenAPI와 route 일치” 완료 증거를 현재 정적 집계로
|
||||
재현할 수 없다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
먼저 `P4-R1-GATE`에서 계약 밖 route를 제거해 controller를 23개로 맞춘다. 나머지 Phase 후속 Gate가 끝난 뒤
|
||||
`P7-R1`에서 plan/API 설명/OpenAPI status를 모두 23개 `implemented`로 동기화하고 validator와 client 생성을 재검증한다.
|
||||
path/request/response schema는 변경하지 않는다.
|
||||
|
||||
## 7. plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 7에 `Task 7.3` / `P7-R1`과 `P7-R1-GATE`를 추가했다. `P7-R1`은 Phase 2~6 후속 Gate가 모두
|
||||
끝난 뒤 실행한다.
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| OpenAPI operation 수 | 충족 | 23개 |
|
||||
| controller mapping 수 | 충족 | 후속 Gate 후 23개 |
|
||||
| 구현 상태 문서 | 충족 | plan/API 설명/OpenAPI status 모두 23개 구현 완료로 동기화 |
|
||||
| dependency/DDL 신규 변경 | 신규 finding 없음 | 정적 변경 범위에서 관련 추가 없음 |
|
||||
| 실행 검증 | 충족 | jq, validator, TypeScript client 생성·compile, Gradle tasks, diff check 성공 |
|
||||
|
||||
**최종 결론:** Phase 7 후속 리뷰 종료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Acceptance Criteria, plan Phase 7, OpenAPI 23개 operation
|
||||
- 검토 범위: Phase별 신규 finding 종결 상태, controller/OpenAPI operation 수, 구현 status, dependency/DDL·최종 Gate 조건
|
||||
- 검증 방식: `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
Phase 7 자체의 신규 독립 결함은 없다. 정적 집계는 OpenAPI 23개 operation과 23개 `implemented`, controller mapping
|
||||
23개를 유지한다. Phase 3~5의 `REV-030`~`REV-033`은 모두 처리 완료됐고, targeted·전체 회귀·lint·diff 검증도 통과했다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 7에 `Task 7.4` / `P7-R2`와 `P7-R2-GATE`를 추가했다. 이 Task는 독립 production 수정이 아니라
|
||||
`P3-R10-GATE`, `P4-R2-GATE`, `P5-R2-GATE` 뒤 targeted·전체 회귀와 문서/operation 상태를 재판정한다.
|
||||
`P7-R2`와 `P7-R2-GATE`를 완료 처리했고, plan 상태를 `구현 완료`로 되돌렸다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` |
|
||||
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
|
||||
| 미처리 finding | 충족 | `REV-030`~`REV-033` 처리 완료 |
|
||||
| 최종 Gate | 충족 | `P7-R2`, `P7-R2-GATE` 완료 |
|
||||
| 실행 검증 | 충족 | targeted, 전체 회귀, lint, OpenAPI/controller/diff 점검 통과 |
|
||||
|
||||
**최종 결론:** 통합 재판정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 16. 후속 기능 통합 최종 판정 — 2026-07-29
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 변경 없음 | 공통 ADMIN·resolver·오류 경계 재사용 |
|
||||
| 2 | 처리 완료 | `REV-044`, `P2-R9` / `P2-R9-GATE` |
|
||||
| 3 | 처리 완료 | `REV-045`, `P3-R13` / `P3-R13-GATE` |
|
||||
| 4 | 처리 완료 | `REV-046`~`REV-047`, `P4-R5`~`P4-R6-GATE` |
|
||||
| 5 | 처리 완료 | `REV-048`, `P5-R5` / `P5-R5-GATE` |
|
||||
| 6 | 처리 완료 | `REV-049`, `P6-R2` / `P6-R2-GATE` |
|
||||
| 7 | 통합 재판정 완료 | `P7-R6` / `P7-R6-GATE` |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- 기존 23개 route와 후속 13개 operation을 합쳐 OpenAPI 계약은 36개다.
|
||||
- 현재 상태는 36개 operation 모두 `implemented`다.
|
||||
- 캐릭터 직접 댓글 API는 v2 미사용 결정에 따라 operation과 Task를 추가하지 않는다.
|
||||
- Phase 2~6 신규 Gate 완료 뒤 36개 operation/mapping/`implemented`, 공통 보안·오류, actor·ownership,
|
||||
row-only soft delete와 legacy/public 회귀를 `Task 7.8`에서 재판정했다.
|
||||
|
||||
targeted 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/controller/diff 정적 검증이 모두 성공했다.
|
||||
|
||||
**최종 결론:** 36개 operation 통합 재판정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 12. 4차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Acceptance Criteria, plan Phase 1~7, OpenAPI 23개 operation
|
||||
- 검토 범위: Phase별 runtime 경계, operation/status/mapping, 완료 상태표·Task header, dependency/DDL 범위
|
||||
- 검증 방식: `sed`, `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-038` | Low | 처리 완료 | 완료된 후속 Task 헤더와 상단 완료 상태가 모순됨 | `Task 7.6` | `P7-R4` |
|
||||
| `REV-039` | Low | 처리 완료 | Phase 4 콘텐츠 해제 설명이 OpenAPI/controller route와 다름 | `Task 7.6` | `P7-R4` |
|
||||
|
||||
### `REV-038` — 완료 Task 헤더와 현재 상태 불일치
|
||||
|
||||
- **심각도:** Low
|
||||
- **상태:** 처리 완료
|
||||
- **처리 상태:** 처리 완료 (`P7-R4`)
|
||||
- **관련 요구사항:** 작업절차의 구현 완료 즉시 Task 체크박스 갱신, 문서유지보수의 완료 상태 동기화
|
||||
- **관련 계약:** plan 상단 현재 상태·Goal Progress·Phase별 Task 완료 증거
|
||||
- **소유 Task:** `Task 7.6`, `P7-R4`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
`Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5`는 하위 단계·Gate·2026-07-29 검증 기록에서 완료됐지만,
|
||||
Task 헤더는 `[ ]`다. 반면 상단 표는 각 Phase를 전체 완료로 표시해 동일 문서 안의 상태가 모순된다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 계획: 네 Task header의 `[ ]`
|
||||
- Gate: `P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE`, `P7-R3-GATE`의 `[x]`
|
||||
- Progress: 2026-07-29 후속 수정·통합 검증 완료 기록
|
||||
|
||||
**영향**
|
||||
|
||||
후속 agent가 이미 완료된 기능 Task를 다시 실행하거나 Phase 완료 조건을 잘못 판정할 수 있다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
기존 완료 증거를 삭제하지 않고 네 Task header, 상단 상태표와 Progress만 같은 완료 상태로 동기화한다.
|
||||
|
||||
### `REV-039` — Phase 4 DELETE 설명의 stale path/body
|
||||
|
||||
- **심각도:** Low
|
||||
- **상태:** 처리 완료
|
||||
- **처리 상태:** 처리 완료 (`P7-R4`)
|
||||
- **관련 요구사항:** OpenAPI를 request/response의 기계 검증 가능한 단일 기준으로 사용
|
||||
- **관련 계약:** `removeAiCharacterSeriesContent`
|
||||
- **소유 Task:** `Task 7.6`, `P7-R4`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
plan Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE /series/{seriesId}/contents`와
|
||||
`RemoveContentToTheSeriesRequest(contentId)` body로 적는다. OpenAPI와 실제 controller는
|
||||
`DELETE /series/{seriesId}/contents/{contentId}`이며 request body가 없다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 계획: Phase 4 `API endpoint와 request/response contract`
|
||||
- OpenAPI: operationId `removeAiCharacterSeriesContent`
|
||||
- 코드: `AiCharacterAdminSeriesController.removeContent`
|
||||
|
||||
**영향**
|
||||
|
||||
runtime은 올바르지만 계획만 읽는 후속 구현·클라이언트 작업이 폐기된 body route를 사용할 수 있다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
production/OpenAPI는 변경하지 않고 Phase 4 설명만 현재 path parameter 계약으로 정정한다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
두 항목은 모두 문서 정합성이고 같은 파일에서 최소 수정할 수 있어 `plan-task.md` Phase 7의
|
||||
`Task 7.6` / `P7-R4`로 묶었다.
|
||||
|
||||
### 실행한 정적 검증
|
||||
|
||||
- `jq empty api-contract.openapi.json` — 성공.
|
||||
- OpenAPI 23개 operation, 고유 operationId 23개, `implemented` 23개, 200 response 누락 0개 — 성공.
|
||||
- controller mapping — Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개.
|
||||
- `git diff --check` — 출력 없음.
|
||||
- 신규 dependency/DDL 파일 변경 — 없음.
|
||||
- Gradle·컴파일·테스트 — 사용자 요청에 따라 실행하지 않음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| Phase 1~6 runtime | 충족 | 신규 기능 finding 없음 |
|
||||
| OpenAPI operation/status | 충족 | 23개 operation·고유 ID·implemented 유지 |
|
||||
| controller mapping | 충족 | domain별 합계 23개 |
|
||||
| 완료 상태 문서 | 충족 | `REV-038` 처리 완료 |
|
||||
| Phase 4 route 설명 | 충족 | `REV-039` 처리 완료 |
|
||||
| plan 반영 | 충족 | `Task 7.6`, `P7-R4` 추가 |
|
||||
|
||||
**최종 결론:** 기능 추가 수정 없음, 문서 정합성 goal 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
### `P7-R4` 처리 결과
|
||||
|
||||
- `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5`, `Task 7.6` 헤더를 완료 상태로 동기화했다.
|
||||
- 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은 23개였다.
|
||||
|
||||
### `P7-R2` 실행 검증
|
||||
|
||||
- `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` — `BUILD SUCCESSFUL in 2m 45s`.
|
||||
- `./gradlew test` — `BUILD SUCCESSFUL in 7m 58s`.
|
||||
- `./gradlew ktlintCheck` — `BUILD SUCCESSFUL in 1s`.
|
||||
- OpenAPI operation/status `jq` assertion — `true`.
|
||||
- controller mapping 23개 assertion, dependency/DDL diff, `git diff --check` — 출력 없이 통과.
|
||||
|
||||
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
|
||||
|
||||
### 리뷰 정보와 범위
|
||||
|
||||
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
|
||||
- 기준 문서: PRD Acceptance Criteria, plan Phase 7, OpenAPI 23개 operation
|
||||
- 검토 범위: Phase별 신규 finding, operation/status/mapping, 최종 완료 Gate와 dependency/DDL 범위
|
||||
- 검증 방식: `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
Phase 7 자체의 신규 독립 결함은 없다. OpenAPI는 23개 operation과 23개 `implemented` status를 유지하고 controller
|
||||
mapping 수도 23개다. 다만 Phase 2~4의 `REV-034`~`REV-037`이 미처리이므로 현재 최종 완료 판정은 유지할 수 없다.
|
||||
|
||||
### plan·goal 전환
|
||||
|
||||
`plan-task.md` Phase 7에 검증 전용 `Task 7.5` / `P7-R3`과 `P7-R3-GATE`를 추가했다. 이 Goal은
|
||||
`P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE` 완료 후 targeted·전체 회귀와 문서/operation 상태를 fresh 재판정한다.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` 유지 |
|
||||
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
|
||||
| 독립 Phase 7 결함 | 없음 | route/schema/dependency/DDL 추가 문제 없음 |
|
||||
| 최종 완료 상태 | 보류 | `REV-034`~`REV-037` 미처리 |
|
||||
| plan 반영 | 충족 | `Task 7.5`, `P7-R3`, `P7-R3-GATE` 추가 |
|
||||
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
|
||||
|
||||
**최종 결론:** Phase 7 통합 재판정 요청(당시 판정, 15절에서 처리 완료)
|
||||
|
||||
**남은 항목:** `P2-R7-GATE` → `P3-R11-GATE` → `P4-R3-GATE` → `P7-R3` → `P7-R3-GATE`.
|
||||
|
||||
## 11. 3차 통합 재판정 및 Gate — 2026-07-29
|
||||
|
||||
### 발견 사항과 판정
|
||||
|
||||
Phase 7 자체의 신규 독립 결함은 없다. `REV-034`~`REV-037`은 각 소유 Phase에서 처리 완료됐고, OpenAPI 23개 operation과 23개 `implemented` status 및 controller mapping 23개를 유지한다.
|
||||
|
||||
### 실행 검증
|
||||
|
||||
- `./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 operation/status `jq` assertion — `true`.
|
||||
- controller mapping count — 23.
|
||||
- `git diff --check` — 출력 없음.
|
||||
- 변경 파일명 점검 결과 신규 dependency/DDL 파일 변경 없음.
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| `REV-034`~`REV-037` | 충족 | Phase 2~4 후속 Gate 처리 완료 |
|
||||
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` |
|
||||
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
|
||||
| targeted/전체 회귀 | 충족 | targeted와 전체 Gradle test 성공 |
|
||||
| lint/diff/dependency/DDL | 충족 | ktlint 성공, diff check 출력 없음, 신규 dependency/DDL 없음 |
|
||||
|
||||
**최종 결론:** 통합 재판정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 13. 5차 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 신규 finding 없음 | 공통 보안·resolver·오류 경계 유지 |
|
||||
| 2 | 후속 처리 요청(당시 판정) | `REV-040`, `P2-R8` / `P2-R8-GATE` |
|
||||
| 3 | 후속 처리 요청(당시 판정) | `REV-041`, `P3-R12` / `P3-R12-GATE` |
|
||||
| 4 | 후속 처리 요청(당시 판정) | `REV-042`, `P4-R4` / `P4-R4-GATE` |
|
||||
| 5 | 후속 처리 요청(당시 판정) | `REV-043`, `P5-R3` / `P5-R3-GATE` |
|
||||
| 6 | 신규 finding 없음 | FanTalk request에는 primitive 필드 없음 |
|
||||
| 7 | 통합 재판정 요청(당시 판정) | `P7-R5` / `P7-R5-GATE` |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- OpenAPI operation과 controller mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2,
|
||||
합계 23개를 유지한다.
|
||||
- 신규 finding은 route 수가 아니라 Phase 2~5 multipart JSON의 primitive required/non-null 의미에 있다.
|
||||
- 전역 Jackson 정책은 legacy endpoint까지 영향을 넓히므로 각 v2 request 경계의 최소 보완으로 계획했다.
|
||||
- 네 Phase Gate 완료 전에는 문서의 `구현 완료` 최종 판정을 유지하지 않는다.
|
||||
- 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
**최종 결론:** `REV-040`~`REV-043` 처리 뒤 통합 재판정 요청(당시 판정, 15절에서 처리 완료)
|
||||
|
||||
**당시 Goal:** `P2-R8`.
|
||||
|
||||
## 14. Community 목록 계약 변경 영향 판정 — 2026-07-29
|
||||
|
||||
### 변경 영향
|
||||
|
||||
- `DEC-P5-LIST-001`에 따라 Community GET의 route 수는 유지되지만 query와 성공 response schema가 변경됐다.
|
||||
- OpenAPI operation은 runtime 정합화 전까지 `implemented-contract-alignment-required`로 표시한다.
|
||||
- `P7-R5`는 기존 `REV-040`~`REV-043`뿐 아니라 `P5-R4-GATE`의 timezone 제거, pagination wrapper,
|
||||
active owner count·hasNext 증거를 함께 대조해야 한다.
|
||||
- 구현 완료 뒤 OpenAPI 23개 operation이 모두 `implemented`로 복구됐는지 확인한다.
|
||||
|
||||
**최종 결론:** Phase 7 통합 재판정 시작 조건에 `P5-R4-GATE` 추가
|
||||
|
||||
**다음 Goal:** 기존 실행 순서대로 `P2-R8`.
|
||||
|
||||
## 15. 5차 통합 재판정 및 Gate — 2026-07-29
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 신규 finding 없음 | 공통 보안·resolver·오류 경계 유지 |
|
||||
| 2 | 처리 완료 | `REV-040`, `P2-R8` / `P2-R8-GATE` |
|
||||
| 3 | 처리 완료 | `REV-041`, `P3-R12` / `P3-R12-GATE` |
|
||||
| 4 | 처리 완료 | `REV-042`, `P4-R4` / `P4-R4-GATE` |
|
||||
| 5 | 처리 완료 | `REV-043`, `P5-R3` / `P5-R3-GATE`, `DEC-P5-LIST-001`, `P5-R4` / `P5-R4-GATE` |
|
||||
| 6 | 신규 finding 없음 | FanTalk request에는 primitive 필드 없음 |
|
||||
| 7 | 통합 재판정 완료 | `P7-R5` / `P7-R5-GATE` |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- Phase 2~5 focused와 package/common 영향 범위 회귀 증거가 모두 완료 상태다.
|
||||
- Community 목록은 `timezone` query 없이 `totalCount/page/size/hasNext/items` wrapper를 반환하며 active owner count와 `hasNext` 계약을 유지한다.
|
||||
- targeted 통합 test와 전체 `./gradlew test`, `ktlintCheck`가 성공했다.
|
||||
- OpenAPI는 23개 operation과 23개 `implemented`를 유지하고 controller mapping도 23개다.
|
||||
- 변경 파일 중 신규 dependency, migration, DDL, `.sql` 경로는 없다.
|
||||
|
||||
**최종 결론:** 통합 재판정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 17. UTC 날짜 계약 변경 통합 판정 — 2026-07-29
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 영향 없음 | 공통 보안·resolver·오류 계약 변경 없음 |
|
||||
| 2 | 영향 없음 | Character 계약 변경 없음 |
|
||||
| 3 | 처리 완료 | `REV-050`, `P3-R14` / `P3-R14-GATE` |
|
||||
| 4 | 영향 없음 | Series 계약 변경 없음 |
|
||||
| 5 | 처리 완료 | `REV-051`, `P5-R6` / `P5-R6-GATE` |
|
||||
| 6 | 영향 없음 | FanTalk 계약 변경 없음 |
|
||||
| 7 | 통합 재판정 완료 | `P7-R7` / `P7-R7-GATE` |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- route와 operation 수는 36개로 유지된다.
|
||||
- 최신 OpenAPI 상태는 `implemented` 36개, `alignment-required` 0개, `planned` 0개다.
|
||||
- 영향 operation은 오디오 생성·상세·댓글·답글 4개와 커뮤니티 댓글·답글 2개다.
|
||||
- OpenAPI의 query parameter/schema `Timezone`은 0개이고 생성 request의 `timezone` property도 제거했다.
|
||||
- 생성 nullable `releaseDate`, 상세 nullable `releaseDate`, 댓글 `date`는 기존 필드명을 유지한
|
||||
ISO-8601 UTC(`Z`) `date-time` 계약이다.
|
||||
- 기존 `P7-R6-GATE`의 36개 구현 완료 판정에 UTC 날짜 계약 6개 operation 정합화 결과를 누적했다.
|
||||
- 오디오 focused 재실행은 `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행은
|
||||
`BUILD SUCCESSFUL in 4m 33s`였고, 각 Gate의 영향 범위 회귀·lint·diff 성공 기록과 OpenAPI/controller 정적 집계를
|
||||
대조했다.
|
||||
|
||||
**최종 결론:** UTC 날짜 계약 36개 operation 통합 재판정 및 Gate 완료
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
## 18. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 36개 operation
|
||||
- 검토 범위: Phase별 controller/facade/test, operation/mapping 수, request media type, pagination,
|
||||
dependency·DDL 변경 범위
|
||||
- 기준 상태: 현재 working tree
|
||||
- 검증 방식: `jq`, `rg`, diff 기반 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 신규 finding 없음 | 공통 ADMIN 인가·resolver·오류/CORS 경계 유지 |
|
||||
| 2 | 보완 필요 | `REV-055`, `P2-R10` / `P2-R10-GATE` |
|
||||
| 3 | 보완 필요 | `REV-052`, `REV-056`, `P3-R15`~`P3-R16-GATE` |
|
||||
| 4 | 보완 필요 | `REV-057`, `P4-R7` / `P4-R7-GATE` |
|
||||
| 5 | 보완 필요 | `REV-053`, `REV-058`, `P5-R7`~`P5-R8-GATE` |
|
||||
| 6 | 보완 필요 | `REV-054`, `P6-R3` / `P6-R3-GATE` |
|
||||
| 7 | 재판정 대기 | `P7-R8` / `P7-R8-GATE` |
|
||||
|
||||
### 정적 검증 결과
|
||||
|
||||
- OpenAPI JSON 문법은 유효하고 operationId는 36개 모두 고유하다.
|
||||
- 실제 controller mapping도 36개이며 신규 dependency·migration·DDL 변경은 없다.
|
||||
- 영향 operation은 오디오 댓글·답글 GET 2개, 커뮤니티 댓글 POST·PUT 2개, FanTalk reply POST 1개와
|
||||
Character·AudioContent·Series·Community multipart 생성·수정 8개로 총 13개다.
|
||||
- OpenAPI의 36개 `x-implementation-status`는 모두 `implemented`지만 위 13개 HTTP 경계가 아직 계약과 달라
|
||||
전체 구현 완료 판정은 보류한다.
|
||||
|
||||
### plan 전환 및 종결 조건
|
||||
|
||||
- 소유 Phase 순서: `P2-R10` → `P3-R15` → `P3-R16` → `P4-R7` → `P5-R7` → `P5-R8` → `P6-R3`
|
||||
- 통합 재판정: `Task 7.10` / `P7-R8`, Gate `P7-R8-GATE`
|
||||
- 종결 조건: `REV-052`~`REV-058` 처리 완료, 영향 13개 operation 회귀, 36개 contract/mapping 유지,
|
||||
lint·diff와 dependency/DDL 무변경 확인
|
||||
|
||||
**최종 결론:** Phase 7 완료 판정 보류, 일곱 HTTP 계약 보완 후 통합 재판정 필요
|
||||
|
||||
**다음 Goal:** `P2-R10`.
|
||||
|
||||
## 19. FanTalk 답변 수정 계약 통합 영향 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD Feature F, OpenAPI 2.3.0, plan Phase 6·7
|
||||
- 검토 범위: 신규 FanTalk 답변 수정 계약, 기존 36개 operation/mapping 상태, `P7-R8` 종결 조건
|
||||
- 검증 방식: 문서·OpenAPI·controller mapping 정적 대조와 `./gradlew tasks --all` 프로젝트 인식 확인. 사용자 지시에
|
||||
따라 컴파일·테스트·lint는 실행하지 않았다.
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- OpenAPI 계약은 기존 36개 `implemented` operation에 FanTalk 답변 수정 `planned` operation 1개를 추가해 총 37개다.
|
||||
- 현재 controller mapping은 36개이므로 신규 PUT이 구현되기 전 전체 계약 완료로 판정할 수 없다.
|
||||
- `REV-059`는 Phase 6 `Task 6.9` / `P6-R4`와 `P6-R4-GATE`가 소유한다.
|
||||
- 기존 미처리 `REV-052`~`REV-058`과 함께 최종 `P7-R8`에서 37개 operation/고유 operationId와 controller 37개
|
||||
mapping, 영향 14개 operation, 공통 ADMIN·오류·CORS, dependency·DDL·legacy/public 무변경을 재판정한다.
|
||||
- 별도 Phase 7 Task를 추가하지 않고 아직 미실행인 `Task 7.10` / `P7-R8`의 시작 조건과 완료 증거를 확장했다.
|
||||
|
||||
**최종 결론:** Phase 7 완료 판정 보류. `P6-R4-GATE`를 포함한 여덟 소유 Gate 후 37개 operation을 통합 재판정한다.
|
||||
|
||||
**다음 Goal:** `P2-R10`.
|
||||
|
||||
## 20. HTTP 경계 최종 통합 재판정 및 Gate — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, OpenAPI 2.3.0, plan `P2-R10`~`P7-R8-GATE`
|
||||
- 검토 범위: `REV-052`~`REV-059`, 37개 operation/mapping, 영향 14개 operation의 pagination·JSON-only·multipart part-level JSON/415·FanTalk 답변 수정 계약, dependency·DDL·legacy/public 변경 범위
|
||||
- 검증 방식: OpenAPI/controller 정적 대조, focused 회귀, 전체 회귀, lint, diff 확인
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- OpenAPI는 operationId 37개, 고유 operationId 37개, `x-implementation-status=implemented` 37개다.
|
||||
- controller mapping은 37개로 OpenAPI operation 수와 일치한다.
|
||||
- `REV-052`~`REV-059`는 모두 소유 Phase Gate와 회귀 검증으로 처리 완료 상태다.
|
||||
- dependency·DDL 추가와 legacy/public API 변경은 없다.
|
||||
|
||||
### 검증 결과
|
||||
|
||||
- 영향 14개 operation과 공통 error/authorization focused 회귀: `BUILD SUCCESSFUL in 1m 26s`
|
||||
- 전체 회귀: `./gradlew test` → `BUILD SUCCESSFUL in 8m 8s`
|
||||
- lint: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 1s`
|
||||
- OpenAPI/controller 정적 대조: 37개 operationId/status와 37개 controller mapping 일치
|
||||
- `git diff --check`: 출력 없음
|
||||
|
||||
**최종 결론:** Phase 7 HTTP 경계 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
|
||||
|
||||
**다음 Goal:** 없음.
|
||||
|
||||
## 23. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD, plan, OpenAPI 37개 operation, API 계약 설명, Phase 1~6 최신 구현·테스트 소스
|
||||
- 검토 범위: endpoint/controller 집계, multipart 8개 operation, Phase별 신규 finding과 plan 상태
|
||||
- 기준 상태: 현재 working tree
|
||||
- 리뷰어/상태: Codex / 판정 완료
|
||||
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### Phase별 판정
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 충족 | 신규 finding 없음 |
|
||||
| 2 | 보완 필요 | `REV-065` / `P2-R12` |
|
||||
| 3 | 보완 필요 | `REV-066` / `P3-R18` |
|
||||
| 4 | 보완 필요 | `REV-067`, `REV-068` / `P4-R9`, `P4-R10` |
|
||||
| 5 | 보완 필요 | `REV-069` / `P5-R10` |
|
||||
| 6 | 문서 보완 필요 | `REV-070` / `P6-R5` |
|
||||
| 7 | 통합 보완 필요 | `REV-071` / `P7-R10` |
|
||||
|
||||
### `REV-071` — Low — 완료 Gate와 finding 상태 불일치
|
||||
|
||||
- `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE`, `P7-R9-GATE`는 완료 기록이 있고
|
||||
OpenAPI/controller/API 계약 설명도 37개 구현 완료로 동기화돼 있다.
|
||||
- 그러나 `plan-task.md` finding 표의 `REV-060`~`REV-062`, `REV-064`는 여전히 `확정`으로 남아 완료 상태와
|
||||
모순된다. 기존 완료 이력을 다시 열지 않고 8차 후속 Gate가 끝난 뒤 상태 표만 정리해야 한다.
|
||||
|
||||
### plan 전환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 신규 Task | `Task 7.12` / `P7-R10` |
|
||||
| Gate | `P7-R10-GATE` |
|
||||
| 선행조건 | `P2-R12-GATE`, `P3-R18-GATE`, `P4-R9-GATE`, `P4-R10-GATE`, `P5-R10-GATE`, `P6-R5-GATE` |
|
||||
| 통합 검증 | 8개 multipart operation, Series 장르 ID, FanTalk 문서, OpenAPI/controller 37개, finding/Phase 상태 |
|
||||
| 범위 제한 | 신규 기능·route/schema·legacy/public·dependency·DDL 변경 없음 |
|
||||
|
||||
### 현재 통합 집계
|
||||
|
||||
- OpenAPI: 37개 operationId, 37개 `implemented`
|
||||
- controller mapping: Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37
|
||||
- 이번 리뷰의 production/test/OpenAPI 변경: 없음
|
||||
- 이번 리뷰에서 실행한 컴파일·테스트: 없음
|
||||
- 문서 검증: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공
|
||||
|
||||
**최종 결론:** Phase 7 보완 필요 — Phase 2~6의 6개 소유 Task와 통합 Task 완료 전에는 전체 구현 완료로
|
||||
재판정할 수 없다.
|
||||
|
||||
**다음 Goal:** `P2-R12`부터 실행하고 마지막에 `P7-R10`으로 통합한다.
|
||||
|
||||
## 21. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
|
||||
- 검토 범위: Phase별 controller/facade/test, 8개 multipart schema와 runtime part binding,
|
||||
operation/mapping/status 및 계약 설명 문서
|
||||
- 검증 방식: `jq`, `rg`, diff 기반 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | finding / 후속 Goal |
|
||||
|---:|---|---|
|
||||
| 1 | 신규 finding 없음 | 공통 ADMIN 인가·resolver·오류/CORS 유지 |
|
||||
| 2 | 보완 필요 | `REV-060`, `P2-R11` / `P2-R11-GATE` |
|
||||
| 3 | 보완 필요 | `REV-061`, `P3-R17` / `P3-R17-GATE` |
|
||||
| 4 | 보완 필요 | `REV-062`, `P4-R8` / `P4-R8-GATE` |
|
||||
| 5 | 보완 필요 | `REV-063`, `P5-R9` / `P5-R9-GATE` |
|
||||
| 6 | 신규 finding 없음 | FanTalk 4개 operation 계약 유지 |
|
||||
| 7 | 보완·재판정 필요 | `REV-064`, `P7-R9` / `P7-R9-GATE` |
|
||||
|
||||
### `REV-064` — Low — 구현 현황 설명이 실제 37개 구현 상태보다 오래됨
|
||||
|
||||
- OpenAPI는 37개 operationId가 모두 고유하고 `x-implementation-status=implemented` 37개다.
|
||||
- 실제 controller mapping도 37개이며 FanTalk 답변 수정 PUT이 구현돼 있다.
|
||||
- 그러나 `plan-task.md` Endpoint Contract Summary는 여전히 36개 구현과 답변 수정 1개 `planned`,
|
||||
선행 보완 완료 전 상태를 기술한다.
|
||||
- `api-contract.md`도 상단 집계, endpoint 표, client 생성 설명에서 route 36개·구현 예정 1개로 남아 있다.
|
||||
- 후속 작업자가 완료 상태를 잘못 판단할 수 있지만 runtime 결함은 아니므로 Low로 판정한다.
|
||||
|
||||
### multipart 통합 판정
|
||||
|
||||
- OpenAPI의 Character·AudioContent·Series·Community 생성·수정 8개 schema는 모두
|
||||
`additionalProperties: false`다.
|
||||
- Phase 2~5 controller는 request part media type은 확인하지만 전체 part 이름 집합을 operation별 허용 목록과
|
||||
비교하지 않아 `REV-060`~`REV-063`을 확정했다.
|
||||
- OpenAPI와 production route/schema는 변경하지 않고 각 소유 Phase controller 경계에서 최소 보완한다.
|
||||
|
||||
### plan 전환 및 종결 조건
|
||||
|
||||
- 소유 Phase 순서:
|
||||
`P2-R11` → `P2-R11-GATE` → `P3-R17` → `P3-R17-GATE` → `P4-R8` → `P4-R8-GATE` →
|
||||
`P5-R9` → `P5-R9-GATE`
|
||||
- 통합 재판정: `Task 7.11` / `P7-R9`, Gate `P7-R9-GATE`
|
||||
- 종결 조건: 미정의 multipart part 400/no-side-effect, 기존 정상/필수/415 회귀, 37개
|
||||
operation/mapping/implemented 일치, `plan-task.md`·`api-contract.md` 구현 상태 동기화
|
||||
|
||||
**최종 결론:** Phase 7 완료 판정 보류. `REV-060`~`REV-064` 처리 후 통합 재판정이 필요하다.
|
||||
|
||||
**다음 Goal:** `P2-R11`.
|
||||
|
||||
## 22. multipart part 이름·문서 상태 최종 통합 재판정 및 Gate — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, OpenAPI 37개 operation, plan `P2-R11`~`P7-R9-GATE`
|
||||
- 검토 범위: `REV-060`~`REV-064`, 8개 multipart schema의 part allow-list, 37개 operation/mapping/status, 계약 설명 문서 상태
|
||||
- 검증 방식: OpenAPI/controller 정적 대조, Phase 2~5 소유 Gate 증거 대조, 문서 diff 확인
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- `REV-060`~`REV-063`은 각 소유 Phase Gate에서 처리 완료됐다.
|
||||
- OpenAPI의 8개 multipart schema는 모두 `additionalProperties: false`이며, runtime allow-list와 actual endpoint 회귀가 이를 따른다.
|
||||
- OpenAPI는 operation 37개, 고유 operationId 37개, `implemented` 37개, `alignment-required` 0개, `planned` 0개다.
|
||||
- controller mapping은 Character 5, AudioContent 10, Series 10, Community 8, FanTalk 4로 총 37개다.
|
||||
- `api-contract.md`의 상단 집계, FanTalk 답변 수정 endpoint 상태, client 생성 설명을 37개 구현 완료/예정 0개로 동기화했다.
|
||||
- dependency·DDL 추가와 legacy/public API 변경은 없다.
|
||||
|
||||
### 검증 결과
|
||||
|
||||
- OpenAPI operation/status 집계: `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`
|
||||
- controller mapping 집계: Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37
|
||||
- 8개 multipart schema 집계: Character/Series `{image, request}`, AudioContent create `{contentFile, coverImage, request}`, AudioContent update `{coverImage, request}`, Community create `{audioFile, postImage, request}`, Community update `{postImage, request}`, 모두 `additionalProperties=false`
|
||||
- Phase 2~5 Gate 회귀: focused/영향 범위 회귀와 `ktlintCheck` 성공 기록 대조 완료
|
||||
- `git diff --check`: 출력 없음
|
||||
|
||||
**최종 결론:** Phase 7 multipart part 이름·문서 상태 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
|
||||
|
||||
## 24. 8차 후속 통합 Gate 완료 — 2026-07-29
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- `REV-065`~`REV-069`는 Phase 2~5 controller가 파일 map과 servlet 전체 part 이름을 모두 operation별 allow-list와 대조하도록 보완해 처리 완료됐다.
|
||||
- `REV-068`은 Series 생성·수정의 `genreId <= 0`을 legacy 호출 전 400으로 거부하도록 보완해 처리 완료됐다.
|
||||
- `REV-070`은 FanTalk 비활성 팬 root 삭제를 OpenAPI·구현·테스트와 같은 200 no-op 계약으로 PRD와 `api-contract.md`에 동기화해 처리 완료됐다.
|
||||
- `REV-071`은 plan finding 표와 상단 Phase 상태를 실제 완료 상태로 동기화해 처리 완료됐다.
|
||||
|
||||
### 검증 결과
|
||||
|
||||
- RED: Phase 2~5 multipart 일반 form-field와 Phase 4 `genreId <= 0` focused RED 묶음에서 신규 multipart/genre 36건 실패.
|
||||
- GREEN: 같은 focused 묶음 재실행 `BUILD SUCCESSFUL in 1m 17s`.
|
||||
- 통합 회귀: ai-character admin character/content/series/community/fantalk focused와 authorization/error/token 회귀 `BUILD SUCCESSFUL in 4m 11s`.
|
||||
- 정적 검증: OpenAPI `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`, controller mapping 37개, FanTalk 삭제 no-op 정적 대조 완료.
|
||||
- 정적 품질: `./gradlew ktlintCheck` `BUILD SUCCESSFUL in 51s`, `git diff --check` 출력 없음.
|
||||
|
||||
**최종 결론:** Phase 7 8차 후속 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료 상태와 문서 상태가 일치한다.
|
||||
|
||||
**남은 항목:** 없음.
|
||||
|
||||
**다음 Goal:** 없음.
|
||||
|
||||
## 25. 9차 통합 정적 리뷰 및 판정 — 2026-07-29
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
|
||||
- 검토 범위: Phase별 최신 production/test 소스, operation/controller 집계, 신규·미처리 finding과 문서 상태
|
||||
- 검증 방식: `rg`·`sed`·`jq` 기반 정적 대조. 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다.
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | 신규 finding/후속 |
|
||||
|---:|---|---|
|
||||
| 1 | 추가 수정 없음 | 없음 |
|
||||
| 2 | 추가 수정 없음 | 없음 |
|
||||
| 3 | 완료 | `REV-072` 처리 완료 |
|
||||
| 4 | 추가 수정 없음 | 없음 |
|
||||
| 5 | 추가 수정 없음 | 없음 |
|
||||
| 6 | 추가 수정 없음 | 없음 |
|
||||
| 7 | 완료 | `P7-R11` / `P7-R11-GATE` 완료 |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- OpenAPI JSON 문법, operation 37개, 고유 operationId 37개, `x-implementation-status=implemented` 37개는
|
||||
정적으로 확인했다.
|
||||
- `REV-072`는 Phase 3에서 처리 완료됐다. v2 actual endpoint의 preview 오류 3종 KO/EN/JA, no-side-effect,
|
||||
정상 preview metadata, legacy 생성 회귀가 통과했다.
|
||||
- OpenAPI implemented count는 37개이고 controller mapping은 파일별 4/5/10/9/8/1 합계 37개다. 신규 dependency·DDL 변경은 없다.
|
||||
- 기존 완료 이력은 변경하지 않고 `Task 3.29`와 그 후속 `Task 7.13`만 완료로 동기화했다.
|
||||
|
||||
**최종 결론:** Phase 7 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
|
||||
|
||||
**다음 Goal:** 없음.
|
||||
|
||||
## 26. 10차 통합 정적 리뷰 및 판정 — 2026-07-30
|
||||
|
||||
### 리뷰 범위와 방식
|
||||
|
||||
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
|
||||
- 검토 범위: Phase별 최신 production/test 소스, operation/controller 집계, 내부 `$ref`, finding·Task 상태
|
||||
- 검증 방식: `rg`·`sed`·`jq` 기반 정적 대조. 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다.
|
||||
|
||||
### Phase별 결과
|
||||
|
||||
| Phase | 판정 | 신규 finding/후속 |
|
||||
|---:|---|---|
|
||||
| 1 | 충족 | 없음 |
|
||||
| 2 | 충족 | 없음 |
|
||||
| 3 | 충족 | 없음 |
|
||||
| 4 | 충족 | 없음 |
|
||||
| 5 | 충족 | 없음 |
|
||||
| 6 | 충족 | 없음 |
|
||||
| 7 | 완료 유지 | 없음 |
|
||||
|
||||
### 통합 판정
|
||||
|
||||
- OpenAPI JSON과 내부 `$ref`가 유효하고 operation 37개·고유 operationId 37개·`implemented` 37개다.
|
||||
- controller mapping은 Character 5, AudioContent 10, Series 10, Community 8, FanTalk 4로 총 37개다.
|
||||
- 기존 `REV-001`~`REV-072`는 모두 `처리 완료`이며 신규 확정 finding과 미완료 Task/Gate가 없다.
|
||||
- Phase별 완료 수와 계획 상태가 실제 구현 현황과 일치하므로 신규 회귀 수정 Task/Goal을 추가하지 않는다.
|
||||
|
||||
**최종 결론:** Phase 7 통합 완료 상태 유지. AI 캐릭터 관리자 API는 문서 기준 37개 operation 구현 완료다.
|
||||
|
||||
**다음 Goal:** 없음.
|
||||
Reference in New Issue
Block a user