docs(ai-character): 리소스 관리 문서 정리

This commit is contained in:
Yu Sung
2026-08-01 01:30:10 +09:00
parent 00e3221035
commit 55ba0df77a
31 changed files with 10021 additions and 390 deletions

View File

@@ -46,7 +46,7 @@ A bright Korean operations console: dense, calm, and explicit. The signature is
| Input | `--input` | `#577581` | Required control boundary |
| Primary | `--primary` | `#00BDF7` | Main CTA |
| Primary foreground | `--primary-foreground` | `#062B36` | Text/icons on primary |
| Ring / Link | `--ring`, `--link` | `#007EA8` | Focus and link |
| Ring / Link | `--ring`, `--link` | `#007EA8` / `#086789` | Focus and link |
| Info | `--info` | `#086789` | Small informational labels |
| Success | `--success` | `#167347` | Open/success state |
| Success surface | `--success-surface` | `#EAF8F0` | Success Badge surface |
@@ -165,6 +165,14 @@ Primary font stack: `Pretendard`, `Noto Sans KR`, `Apple SD Gothic Neo`, `system
- Accessibility: progressbar exposes `aria-valuenow`; cancel/retry are native buttons.
- Motion: none.
### SuccessNotification
- Structure: one shell-level `role="status"` message after successful create, update, or deactivate navigation.
- Variants: success only.
- Tokens: `--success-surface`, `--success`, `--border`, `--radius-lg`, `--space-3`, `--space-4`.
- Accessibility: `aria-label="작업 성공"`; no timer or dismiss control in P3-T3.
- Motion: none.
### AdminAudioPlayer
- Structure: native audio element wrapped with play/pause, seek, time, volume, speed, generic error, and manual retry controls.
@@ -179,6 +187,13 @@ Primary font stack: `Pretendard`, `Noto Sans KR`, `Apple SD Gothic Neo`, `system
- Accessibility: no direct rendered surface.
- Motion: none.
### CommentThread
- Structure: two-level comment management surface with root comment form, root rows, direct reply expansion, reply form, and edit/delete row actions.
- Variants: audio target includes `languageCode`; community target omits it. Replies never expose nested reply controls.
- Accessibility: forms use visible labels, rows are named articles, reply lists are named regions, and mutation actions are native buttons.
- Motion: none beyond existing control state color.
## 6. Motion & Interaction
- Motion is limited to 150ms micro-interactions for real control state changes.

View File

@@ -11,15 +11,15 @@ React, TypeScript, Vite 기반 독립 관리자 SPA입니다.
```bash
npm ci
npx playwright install chromium webkit
npx playwright install chromium
```
## Environment
Vite mode별 API base URL은 아래 파일에 둡니다.
- `.env.development`: `https://test-character-admin.sodalive.net`
- `.env.production`: `https://character-admin.sodalive.net`
- `.env.development`: `https://test-api.sodalive.net`
- `.env.production`: `https://api.sodalive.net`
- `server mode`: `VITE_API_MODE=server`이며 기본 개발 서버가 실제 개발 API를 사용합니다.
- `mock mode`: `VITE_API_MODE=mock`이며 개발 전용 browser MSW fixture로 제공 계약 범위만 미리 봅니다.
- `mock data reset`: mock data는 browser storage에 영구 저장하지 않고 새 mock store/session이 시작될 때 seed 기준으로 초기화됩니다.
@@ -38,10 +38,23 @@ Vite mode별 API base URL은 아래 파일에 둡니다.
- `npm run test`: Vitest watch입니다.
- `npm run test:run`: Vitest 단발 실행입니다.
- `npm run e2e (VITE_API_MODE=server playwright test)`: server mode Playwright입니다. 대상 spec은 `playwright.config.ts`의 server mode `testMatch`가 제한하며, 추가 file filter를 넘기면 교집합만 실행합니다.
- `npm run e2e:mock (VITE_API_MODE=mock playwright test)`: mock mode Playwright입니다. 대상 spec은 `playwright.config.ts`의 mock mode `testMatch`가 제한하며, 추가 file filter를 넘기면 교집합만 실행합니다.
- `npm run e2e:mock`: mock mode Playwright입니다. 인자 없이 실행하면 Chromium/mobile Chrome matrix를 분할 실행하고, file filter나 `--project` 인자를 넘기면 `playwright.config.ts`의 mock mode `testMatch` 교집합만 실행합니다.
## Browser Support
- 지원 기준: 데스크톱 Chrome과 모바일 Chrome입니다.
- 로컬 자동 검증은 Playwright Chromium/mobile Chrome project로 수행합니다.
## Mock Preview Ownership
- Phase 2 mock preview는 auth shell과 제공 계약 기반 공통 fixture까지만 포함합니다.
- 후속 도메인 Phase는 자기 domain handler, fixture, mock E2E를 같은 Phase에서 추가하고 검증합니다.
- mock mode 통과는 최종 UI 확인 증거이며 실제 server integration 완료 증거가 아닙니다.
## Known Backend Constraints
- 원작 lookup과 장르 lookup은 OpenAPI 2.3.0의 v2 계약으로 구현됐습니다. 이전 후보 endpoint는 이력 문서에만 보존합니다.
- FanTalk 답변 수정·팬 원글 삭제, Comments CRUD, Community pagination은 Phase 10에서 구현됐습니다. FanTalk 별도 상세·filter/sort·중복 오류 계약은 제품 범위 밖이며, active-only 반환 보장은 실제 개발 API 수동 QA에서 분리 확인합니다.
- price 최대값은 `99999`이며, 파일 용량·MIME 등은 backend가 동일 검증합니다. 이미지 비율·crop 결과는 client가 보장하고 backend는 검증하지 않습니다.
- OpenAPI 밖의 도메인별 오류는 `알 수 없는 오류가 발생했습니다.`로 처리합니다.
- 현재 server mode E2E allowlist는 shell/auth/server-boundary 검증 중심입니다. 도메인별 mock E2E 통과를 실제 개발 API integration 완료로 해석하지 않습니다.

View File

@@ -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",
@@ -812,11 +1186,10 @@
"title": {"type": "string"},
"detail": {"type": "string"},
"tags": {"type": "string"},
"price": {"type": "integer", "format": "int32"},
"price": {"type": "integer", "format": "int32", "minimum": 0, "maximum": 99999},
"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},
@@ -836,7 +1209,7 @@
"title": {"$ref": "#/components/schemas/NullableString"},
"detail": {"$ref": "#/components/schemas/NullableString"},
"tags": {"$ref": "#/components/schemas/NullableString"},
"price": {"$ref": "#/components/schemas/NullableInt32"},
"price": {"$ref": "#/components/schemas/NullableInt32", "minimum": 0, "maximum": 99999},
"isAdult": {"$ref": "#/components/schemas/NullableBoolean"},
"isActive": {"$ref": "#/components/schemas/NullableBoolean"},
"isPointAvailable": {"$ref": "#/components/schemas/NullableBoolean"},
@@ -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,
@@ -1161,7 +1586,7 @@
"content": {"type": "string"},
"isCommentAvailable": {"type": "boolean"},
"isAdult": {"type": "boolean"},
"price": {"type": "integer", "format": "int32", "default": 0}
"price": {"type": "integer", "format": "int32", "minimum": 0, "maximum": 99999, "default": 0}
}
},
"CommunityPostUpdateRequest": {
@@ -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

View File

@@ -6,7 +6,7 @@
|---|---|
| 문서 상태 | OpenAPI 반영 구현 기준 |
| 작성일 | 2026-07-25 |
| 최종 수정일 | 2026-07-28 |
| 최종 수정일 | 2026-07-30 |
| 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 |
| 구현 대상 | React + TypeScript + Vite SPA |
| UI 기반 | Tailwind CSS + shadcn/ui |
@@ -38,7 +38,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
관리자는 ADMIN 권한으로 로그인한 뒤 AI 캐릭터를 선택한다. 이후의 모든 생성·수정·비활성화 작업은 선택한 `characterId`와 연결된 AI 캐릭터 크리에이터의 활동으로 저장된다. 관리자가 AI 캐릭터 계정으로 직접 로그인하거나 토큰을 교환하는 방식은 사용하지 않는다.
이번 문서는 요구사항 구현 계획만 정의한다. 애플리케이션 코드는 이번 단계에서 수정하지 않는다.
이번 문서는 요구사항, 구현 계획, 완료된 후속 계약 반영 상태를 정의한다.
## 2. Problem Statement
@@ -59,7 +59,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- AI 캐릭터 목록 검색, 상세 조회, 생성, 수정, 비활성화를 제공한다.
- 선택한 캐릭터 문맥에서 오디오 콘텐츠, 시리즈, 커뮤니티 게시글을 관리한다.
- 선택한 캐릭터로 FanTalk에 한 번 답변할 수 있게 한다. 기존 답변 수정은 OpenAPI에 수정 endpoint가 추가된 뒤 활성화한다.
- 선택한 캐릭터로 FanTalk에 한 번 답변하고 기존 답변 수정할 수 있게 한다.
- 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다.
- 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다.
- 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다.
@@ -88,6 +88,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- 정식 WCAG 2.2 AA 인증 또는 외부 접근성 감사
- 백엔드가 담당할 creator 생성·프로필 동기화의 조건부 정책 변경
- 비활성 ID에 대한 상세 GET 반환 여부와 오류 status 등 백엔드 조회 정책 결정
- 감사 로그 조회 UI
- 실제 API의 404를 감지해 mock 응답으로 자동 전환하는 production fallback
## 5. Target Users
@@ -112,7 +113,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
3. 캐릭터 워크스페이스의 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk 탭을 이동한다.
4. 활성 캐릭터라면 데스크톱·태블릿에서 리소스를 생성·수정·비활성화한다.
5. 오디오 플레이어로 캐릭터가 올린 오디오를 검수한다.
6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변한다. 기존 답변 수정은 수정 계약이 제공된 뒤 추가한다.
6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변하고, 답변이 있으면 기존 답변을 수정한다.
7. 오디오 또는 커뮤니티 댓글에 캐릭터 명의로 댓글·답글을 작성하거나 운영 정책에 따라 삭제한다.
8. 모바일에서는 리소스를 조회하고 오디오를 재생하며 댓글과 FanTalk 답변을 관리한다.
9. JWT가 만료되거나 폐기되면 인증 정보를 지우고 로그인 화면으로 이동한다.
@@ -193,9 +194,9 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| CHAR-009 | 확정 | 캐릭터 이름·설명·이미지 변경 시 creator의 nickname·introduce·profile image 동기화는 현재 백엔드가 수행한다. |
| CHAR-010 | 확정 | creator 상황에 따른 조건부 생성·동기화는 다음 백엔드 범위이며 현재 UI 범위가 아니다. |
| CHAR-011 | 제외 | 현 OpenAPI의 Character 목록·상세 응답에는 `creatorMemberId`, `creatorNickname`이 없다. 계약에 추가되기 전에는 creator 정보 UI와 DTO를 만들지 않는다. |
| CHAR-012 | 외부 의존 | OpenAPI는 `searchTerm`을 생략하면 활성 목록을 반환한다고 명시하지만, `searchTerm` 지정 시에는 “레거시 검색”만 명시해 active-only 여부가 불명확하다. 검색 결과 보장이 추가되기 전에도 client 활성 filter는 만들지 않고 서버 반환값을 표시한다. |
| CHAR-013 | 외부 의존 | `originalWorkId`는 생성·수정 request에서 optional nullable이므로 미선택 시 key 생략과 `null`이 모두 계약상 가능하다. 원작 검색 선택기에 필요한 lookup API는 OpenAPI에 없으므로 제공 전에는 원작 선택 network integration을 구현하지 않는다. |
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
| CHAR-012 | 확정 | 이름 검색 여부와 관계없이 캐릭터 목록 API는 `isActive=true`인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않고 서버 반환값을 사용한다. |
| CHAR-013 | 확정 | 원작 검색은 필수 `searchTerm` query를 사용하는 `GET /api/v2/admin/ai-characters/original-works/search`를 호출하고 `data[]``id`, `title`, `contentType`, `category`, `isAdult`, `description`, `originalWork`, `originalLink`, `writer`, `studio`, `originalLinks`, `tags`, `imageUrl`을 사용한다. `originalWorkId` 미선택은 serializer의 canonical 생략으로 고정한다. |
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. |
| CHAR-015 | 확정 | 목록은 `searchTerm`, `page`, `size`를 사용하고 `data.totalCount`, `data.content[]`를 소비한다. 목록 ID field는 `id`다. |
| CHAR-016 | 확정 | 생성·수정 성공은 `data=null`이므로 생성 후 목록을 무효화해 이동하고, 수정 후 기존 `characterId` 상세와 목록을 다시 조회한다. 생성 응답에서 새 ID나 상세 DTO를 추정하지 않는다. |
| CHAR-017 | 확정 | 상세의 `characterUUID`는 OpenAPI에 존재하는 읽기 전용 값이며, 제거된 `externalCharacterId`와 다른 field다. |
@@ -204,10 +205,10 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
#### 캐릭터 생성·수정 폼
- 이름, system prompt와 설명은 명시적인 레이블과 필드 오류 영역을 가진다.
- OpenAPI의 optional scalar(`age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType`)와 배열(`tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`)을 생성 form에서 편집할 수 있게 한다. `originalWorkId`lookup 계약 제공 후 선택기로 편집하고, 수정 request에 없는 `region`은 수정 화면에서 읽기 전용이다.
- OpenAPI의 optional scalar(`age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType`)와 배열(`tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`)을 생성 form에서 편집할 수 있게 한다. `originalWorkId`원작 검색 선택기로 편집하고, 수정 request에 없는 `region`은 수정 화면에서 읽기 전용이다.
- 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다.
- 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다.
- 원작 lookup 계약이 제공되면 이름 검색형 Combobox로 선택한다. 미선택은 허용하고 serializer는 key 생략 또는 `null` 중 한 가지 canonical form을 contract test로 고정한다.
- 원작은 제목·콘텐츠 타입·카테고리 부분 검색을 지원하는 Combobox로 선택한다. 미선택은 허용하고 serializer는 `originalWorkId` key 생략을 canonical form으로 사용한다.
- 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다.
- 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다.
@@ -217,15 +218,15 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|---|---|---|
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·제목 검색·상세·생성·수정·비활성화를 제공한다. |
| AUDIO-002 | 제외 | 현 OpenAPI 목록·상세에는 `OPEN`, `SCHEDULED` status field가 없고 목록 status query도 없다. 상태 badge와 server status filter는 계약에 추가되기 전에는 제공하지 않는다. |
| AUDIO-003 | 확정 | 공개 예약은 생성 request의 nullable `releaseDate``timezone`으로 표현한다. 목록·상세에서는 OpenAPI가 반환한 `releaseDate` 문자열을 그대로 표시하고 별도 status enum을 만들지 않는다. |
| AUDIO-003 | 확정 | 공개 예약은 생성 request의 nullable `releaseDate`로 표현한다. 예약 값은 클라이언트가 UTC로 변환한 ISO-8601 `Z` 문자열이며 `timezone` field는 보내지 않는다. 목록·상세의 `releaseDate`도 UTC `Z` 문자열 또는 `null`로 소비하고 별도 status enum을 만들지 않는다. |
| AUDIO-004 | 확정 | 프론트엔드는 `releaseDate``OPEN`·`SCHEDULED` 같은 API status를 재계산하거나 DTO에 추가하지 않는다. |
| AUDIO-005 | 제외 | 현 OpenAPI에 status filter가 없으므로 status query를 보내거나 현재 page를 client에서 status별로 거르지 않는다. |
| AUDIO-006 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
| AUDIO-007 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
| AUDIO-008 | 확정 | 공개 방식은 “지금 즉시 공개”와 “예약 공개” 두 선택 버튼으로 제공한다. |
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null` `timezone="Asia/Seoul"`다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null`을 보내고 `timezone`내지 않는다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. |
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하 생성 API에는 `yyyy-MM-dd HH:mm` 형식의 `releaseDate``timezone="Asia/Seoul"`을 보낸다. UTC `Z` 값으로 변환하지 않는다. |
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하되, 생성 API에는 해당 시각을 클라이언트에서 UTC로 변환한 ISO-8601 `Z` 형식의 `releaseDate`를 보낸다. 예를 들어 `2026-07-29 18:00` Asia/Seoul은 `2026-07-29T09:00:00Z`로 전송한다. |
| AUDIO-012 | 확정 | 생성 multipart의 `contentFile`, `coverImage`, `request`는 필수다. 수정은 `coverImage``request`만 허용하므로 오디오 원본 파일 교체 UI를 제공하지 않는다. |
| AUDIO-013 | 확정 | 오디오 확장자는 `.mp3`, `.aac`, `.m4a`를 허용한다. WAV는 허용하지 않는다. |
| AUDIO-014 | 확정 | canonical MIME은 `audio/mpeg`, `audio/aac`, `audio/mp4`다. |
@@ -233,23 +234,23 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| AUDIO-016 | 확정 | 확장자와 MIME만 신뢰하지 않고 실제 컨테이너·코덱 검증은 백엔드가 수행해야 한다. |
| AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. |
| AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. |
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 0 이상의 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 `0..99999` 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
| AUDIO-020 | 확정 | Audio 생성·수정 request에는 `seriesIds`가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다. |
| AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. |
| AUDIO-022 | 외부 의존 | 오디오 목록 request에는 활성 상태 query와 status query가 없고 응답에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side 활성 filter는 만들지 않는다. |
| AUDIO-022 | 확정 | 오디오 목록 API는 `isActive=true`인 항목만 반환한다. request에는 활성 상태 query를 추가하지 않고 응답에도 client-side 활성 filter를 적용하지 않는다. status query는 여전히 제공하지 않는다. |
| AUDIO-023 | 확정 | 최대 크기는 decimal 1,024MB인 `1,024,000,000 bytes` 이하이며 `1,024,000,001 bytes`부터 거부한다. 오디오 콘텐츠와 커뮤니티 첨부 audio에 동일하게 적용한다. |
| AUDIO-024 | 확정 | `audio/x-m4a``.m4a` 파일에 한해 호환 MIME으로 허용한다. 실제 MP4/M4A 컨테이너·코덱 검증을 통과해야 하며 다른 확장자와의 조합은 거부한다. |
| AUDIO-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
| AUDIO-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. |
| AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 유효한 `themeId`가 필수다. OpenAPI의 기본값 `0`은 binding 기본값일 뿐 domain에서 유효하지 않으므로 프론트엔드는 테마 선택을 강제한다. |
| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답 `data[]``id`, `theme`, `image`를 사용한다. |
| AUDIO-029 | 확정 | 목록 검색 query는 `search_word`이며 검색어가 2자 이상일 때만 보낸다. 목록 응답은 `data.totalCount`, `data.items[]`를 사용한다. |
| AUDIO-030 | 확정 | 상세 GET은 필수 `timezone=Asia/Seoul` query를 보낸다. |
| AUDIO-030 | 확정 | 상세 GET에는 `timezone` query를 보내지 않는다. 응답의 nullable `releaseDate`는 ISO-8601 UTC `Z` 값으로 소비하고 화면 표시 시 Asia/Seoul로 변환한다. |
| AUDIO-031 | 확정 | 생성 성공은 `data.contentId`를 사용해 상세로 이동할 수 있다. 수정·soft delete 성공은 `data=null`이므로 기존 ID 기준 cache를 무효화한다. |
| AUDIO-032 | 확정 | 생성 `request`는 필수 `title`, `detail`, `tags`, `price`와 OpenAPI의 optional field만 보낸다. 수정은 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable`만 변경할 수 있다. |
| AUDIO-033 | 확정 | 생성 form은 OpenAPI의 `purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 계약 enum·type과 default에 맞춰 제공한다. 계약에 없는 추가 상관관계 validation은 만들지 않는다. |
현 수정 계약에는 `releaseDate`, `timezone`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
현 수정 계약에는 `releaseDate`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
#### 관리자 오디오 플레이어
@@ -271,18 +272,18 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| SERIES-004 | 확정 | 수정 시 state를 선택할 수 있으며 선택하지 않으면 필드를 생략해 이전 상태를 유지한다. |
| SERIES-005 | 확정 | 연재 요일 enum은 `SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `RANDOM`이다. |
| SERIES-006 | 확정 | `RANDOM`은 다른 요일과 함께 보낼 수 없다. 값은 RANDOM 단독 또는 하나 이상의 실제 요일 목록이어야 한다. |
| SERIES-007 | 외부 의존 | 생성 시 유효한 `genreId`가 필요하고 OpenAPI 기본값 `0`은 domain에서 유효하지 않다. 장르 이름 검색 API는 OpenAPI에 없으므로 제공 전에는 장르 선택 network integration과 Series 생성을 완료할 수 없다. |
| SERIES-007 | 확정 | 생성 시 유효한 `genreId`가 필요하고 OpenAPI 기본값 `0`은 domain에서 유효하지 않다. 장르 선택지는 query/body 없는 `GET /api/v2/admin/ai-characters/series-genres``data[]`에서 `id`, `genre`, `isAdult`를 사용한다. |
| SERIES-008 | 확정 | `data.totalCount`, `data.items[]`를 page별로 읽어 서버가 반환한 시리즈 전체를 별도 순서 변경 모드에 표시하고 최종 순서의 모든 ID를 `{ "ids": [...] }`로 전송한다. active-only 여부는 `SERIES-012` 계약 제공 후 검증한다. |
| SERIES-009 | 확정 | drag-and-drop 외에 키보드와 위/아래 버튼으로 순서를 바꿀 수 있어야 한다. |
| SERIES-010 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`, 복원과 hard delete는 제공하지 않는다. |
| SERIES-011 | 외부 의존 | 장르 이름 검색 endpoint·DTO는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
| SERIES-012 | 외부 의존 | 시리즈 목록 request에는 활성 상태 query가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 list item의 `isActive`를 client에서 숨기는 방식으로 대체하지 않는다. |
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
| SERIES-011 | 확정 | 장르 목록 endpoint는 검색·페이지 query 없이 활성 장르 전체를 반환한다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
| SERIES-012 | 확정 | 시리즈 목록 API는 `isActive=true`인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않는다. |
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. |
| SERIES-014 | 확정 | 생성 multipart의 `image``request`는 필수다. 생성 request는 `keyword` 단일 문자열을 사용하며 `keywords` 배열을 보내지 않는다. |
| SERIES-015 | 확정 | 목록은 enum `publishedDaysOfWeek`, `genreId`, enum `state`를 사용하지만 상세는 표시용 문자열 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state`사용한다. 상세 표시 문자열을 update enum으로 재사용하지 않는다. |
| SERIES-015 | 확정 | 목록과 상세는 동일한 `SeriesListItem` schema를 사용하고 `genreId`, enum 배열 `publishedDaysOfWeek`, enum `state`반환한다. 화면 label은 클라이언트에서 표시용으로 변환하되 원본 enum과 ID를 수정 payload에 사용한다. |
| SERIES-016 | 확정 | 연결 후보는 `GET .../contents/search?search_word=...`, 연결은 `{ "contentIdList": [...] }`, 해제는 body 없는 DELETE를 사용한다. |
| SERIES-017 | 외부 의존 | 상세 응답에는 update에 필요한 `genreId`, enum `publishedDaysOfWeek`, enum `state`가 없다. 직접 링크에서도 안전하게 수정 form을 초기화할 edit DTO 또는 별도 mapping 계약이 제공되기 전에는 상세 표시 문자열을 역변환하지 않고 수정 network integration을 완료하지 않는다. |
| SERIES-018 | 확정 | 생성 request의 `keyword`는 수정 request에 없다. 수정 화면에서 상세의 `keywords`를 읽기 전용으로 표시하고 update payload에 보내지 않는다. |
| SERIES-017 | 확정 | 시리즈 상세 응답은 목록 item과 동일한 수정용 원본값 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 제공하므로 별도 edit DTO가 필요 없다. 수정 화면은 상세 응답으로 기존 선택값을 초기화하고, 장르 API는 option 목록 표시용으로 호출한다. |
| SERIES-018 | 확정 | 생성 request의 `keyword`는 수정 request와 상세 응답에 없다. 수정 화면에서 keyword 편집·표시를 추가하거나 update payload에 보내지 않는다. |
#### 시리즈 콘텐츠 연결
@@ -302,12 +303,12 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false``isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 0 이상의 정수 “캔” 단위를 사용한다. |
| COMMUNITY-008 | 외부 의존 | 커뮤니티 목록 request에는 활성 상태 query가 없고 item에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side filter는 만들지 않는다. |
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 `0..99999` 정수 “캔” 단위를 사용한다. |
| COMMUNITY-008 | 확정 | 커뮤니티 목록 API는 `isActive=true`인 게시글만 반환한다. request에는 활성 상태 query를 추가하지 않고 item에도 client-side 활성 filter를 적용하지 않는다. |
| COMMUNITY-009 | 확정 | 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다. |
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 해당 항목 제외는 active-only 계약 제공 후 검증한다. |
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 서버의 active-only 목록에서 해당 게시글이 제외돼야 한다. |
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioUrl`을 사용한다. |
| COMMUNITY-012 | 확정 | 목록 GET은 필수 `timezone=Asia/Seoul`, `page`, `size`를 사용하고 `data`의 게시글 배열을 소비한다. 응답에 `totalCount`, `page`, `hasNext`가 없으므로 전체 건수·마지막 page를 추정하지 않는다. |
| COMMUNITY-012 | 확정 | 목록 GET은 `timezone` 없이 `page`, `size`를 사용하고 `data.totalCount`, `data.page`, `data.size`, `data.hasNext`, `data.items[]`를 소비한다. 전체 건수와 다음 page 여부는 서버 metadata를 그대로 사용한다. |
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용하고 request에 필수 `content`, `isCommentAvailable`, `isAdult`와 optional `price`만 보낸다. |
| COMMUNITY-014 | 확정 | 수정 multipart는 optional `postImage`와 필수 `request`만 허용한다. 수정에서 가격·첨부 audio 교체는 제공하지 않고, 고정은 `isFixed`, soft delete는 `isActive=false`로 처리한다. |
| COMMUNITY-015 | 확정 | 생성·수정·고정·soft delete 성공은 `data=null`이므로 목록을 무효화·재조회하고 mutation 응답에 게시글 DTO가 있다고 가정하지 않는다. |
@@ -317,16 +318,17 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| ID | 상태 | 요구사항 |
|---|---|---|
| FANTALK-001 | 확정 | 기본 목록은 backend가 반환한 순서를 유지한다. 현 계약에 sort query가 없으므로 client가 page 사이의 최신순을 재정렬하지 않는다. |
| FANTALK-002 | 외부 의존 | 전체·미답변·답변 완료 server filter query가 OpenAPI에 없다. 전체 결과 filter 계약이 제공되기 전에는 현재 page만 거르는 filter를 완성 기능으로 제공하지 않는다. |
| FANTALK-002 | 제외 | 현재 UI에는 전체·미답변·답변 완료 filter control을 제공하지 않는다. 현재 page만 client에서 거르는 불완전한 filter도 만들지 않는다. 후속 제품 범위에서 전체 결과 filter가 필요해지면 server query 계약과 함께 별도 요구사항으로 다시 포함한다. |
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
| FANTALK-004 | 외부 의존 | 답변이 있으면 추가 작성 UI에서 차단한다. 기존 답변 수정 endpoint는 OpenAPI에 없으므로 계약 제공 전에는 수정 network integration을 구현하지 않는다. |
| FANTALK-004 | 확정 | `creatorReplies`가 비어 있으면 답변 작성 UI를, 비어 있지 않으면 기존 답변 수정 UI를 제공한다. 수정은 `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}``{ "content": string }`만 보내며 path의 `replyId`에는 `creatorReplies[].fanTalkId`를 사용한다. 백엔드 구현 완료 확인에 따라 OpenAPI operation도 `implemented`로 정정했으므로 mock/client와 실제 server integration을 모두 구현·검증한다. |
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 목록 조회 답변 작성을 지원한다. 답변 수정은 수정 계약이 제공된 뒤 같은 viewport 범위에 추가한다. |
| FANTALK-007 | 외부 의존 | 목록 GET과 답변 POST는 제공됐다. 별도 상세 GET, 답변 수정 endpoint·DTO, 답변 상태 filter sort 계약은 백엔드가 제공해야 하며 제공 전에는 해당 network integration을 구현하지 않는다. |
| FANTALK-008 | 외부 의존 | 답변 1개 불변식의 원자적 강제와 중복 생성의 정확한 비2xx status/message key는 백엔드가 결정·제공해야 한다. |
| FANTALK-009 | 확정 | 목록은 `page`, `size`를 사용하고 `data.fanTalkCount`, `data.fanTalks`, `data.page`, `data.size`, `data.hasNext`를 소비한다. 각 item의 `creatorReplies[]`로 답변 유무를 판단한다. |
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 목록 조회, 답변 작성과 기존 답변 수정을 지원한다. |
| FANTALK-007 | 확정 | 별도 상세 GET·직접 route, 답변 상태 filter, sort control은 현재 UI 범위가 아니다. 목록 item을 source로 Sheet/panel을 열고 backend 반환 순서를 유지하므로 추가 network 계약 없이 목록·작성·수정·팬 원글 삭제를 구현한다. |
| FANTALK-008 | 제외 | 중복 생성의 정확한 비2xx status/message key를 사용하는 도메인별 오류 분기는 만들지 않는다. 답변 1개 불변식 자체는 `FANTALK-003`의 backend 수용 기준이며, client는 한 화면의 중복 submit을 막고 일반 오류 후 현재 목록을 재조회한다. 동시 POST 후에도 답변이 하나인지 실제 server integration에서 검증하며 위반 시 backend 결함으로 기록한다. |
| FANTALK-009 | 확정 | 목록은 `page`, `size`를 사용하고 `data.fanTalkCount`, `data.fanTalks`, `data.page`, `data.size`, `data.hasNext`를 소비한다. `creatorReplies.length === 0`이면 미답변, 하나 이상이면 답변 완료로 판단하며 기존 답변 수정 시 첫 답변의 `fanTalkId``replyId`로 사용한다. |
| FANTALK-010 | 확정 | 답변 작성은 `{ "content": string }`을 보내고 성공 응답의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. |
| FANTALK-011 | 확정 | 별도 상세 endpoint가 없으므로 목록 item을 source로 collection Sheet 또는 panel을 열며 `/fan-talks/:fanTalkId` 직접 route를 만들지 않는다. |
| FANTALK-012 | 확정 | 관리자는 팬 작성 FanTalk 원글을 `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`로 soft delete할 수 있다. 성공 후 현재 목록을 재조회하고 해당 원글을 닫으며, 연결된 creator reply를 별도 삭제했다고 가정하지 않는다. |
### 8.7 댓글과 답글
@@ -334,10 +336,12 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|---|---|---|
| COMMENT-001 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글에 댓글 영역을 제공한다. |
| COMMENT-002 | 확정 | 구조는 루트 댓글과 그 댓글의 직접 답글까지 정확히 2단계다. 답글의 답글은 허용하지 않는다. |
| COMMENT-003 | 확정 | AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정·soft delete할 수 있다. |
| COMMENT-004 | 확정 | 팬 작성 루트 댓글과 답글 수정할 수 없고 운영 목적의 soft delete만 가능하다. |
| COMMENT-003 | 확정 | AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정할 수 있다. 댓글의 `writerId`와 대상 오디오·커뮤니티 게시글의 `creatorId`가 같으면 AI 캐릭터 작성으로 판단한다. |
| COMMENT-004 | 확정 | `writerId !== creatorId`팬 작성 루트 댓글과 답글에는 수정 UI를 제공하지 않는다. 관리자는 작성자와 관계없이 운영 목적으로 해당 row만 soft delete할 수 있으며 하위 답글을 함께 삭제했다고 가정하지 않는다. |
| COMMENT-005 | 확정 | 모바일에서도 조회·작성·수정·soft delete를 모두 지원한다. |
| COMMENT-006 | 외부 의존 | 댓글 목록·작성·수정·soft delete API와 팬 댓글 삭제 권한 오류 계약은 백엔드가 결정·제공해야 한다. 제공 전에는 댓글 network integration을 구현하지 않는다. |
| COMMENT-006 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글은 각각 루트 댓글 목록 GET, 댓글·답글 POST, AI 캐릭터 작성 댓글 PUT, 작성자 무관 DELETE, 루트별 답글 목록 GET을 제공한다. 모든 mutation 성공 `data=null`을 처리하고 목록·답글 cache를 재조회한다. |
| COMMENT-007 | 확정 | 루트 댓글은 `.../comments`, 직접 답글은 `.../comments/{commentId}/replies`에서 조회한다. 작성 POST의 `parentId`를 생략하거나 `null`로 보내면 루트, 같은 target의 활성 루트 ID를 보내면 직접 답글이며 답글의 답글은 UI에서 허용하지 않는다. |
| COMMENT-008 | 확정 | 댓글 날짜는 ISO-8601 UTC `Z` 값으로 소비해 Asia/Seoul로 표시한다. 오디오 댓글 작성 request는 `comment`, optional `parentId`, `isSecret`, `languageCode`를 사용하고 커뮤니티 댓글 작성 request는 `comment`, optional `parentId`, `isSecret`을 사용한다. 수정 request는 공통 `{ "comment": string }`이다. |
### 8.8 공통 파일 정책
@@ -401,7 +405,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| 커뮤니티 등록·수정·고정·비활성화 | 전체 | 전체 | 미지원 |
| 댓글·답글 관리 | 전체 | 전체 | 전체 |
| FanTalk 조회·답변 작성 | 전체 | 전체 | 전체 |
| FanTalk 답변 수정 | 계약 제공 후 전체 | 계약 제공 후 전체 | 계약 제공 후 전체 |
| FanTalk 답변 수정·팬 원글 soft delete | 전체 | 전체 | 전체 |
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
@@ -527,7 +531,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- 오류: 서버의 한국어 message, 재시도, 필요한 경우 목록으로 이동
- 저장 중: 제출 버튼 비활성화와 진행 표시
- 저장 성공: toast와 최신 서버 응답 반영
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 비활성 항목 제외는 active-only 계약 제공 후 검증
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 서버 active-only 목록에서 비활성 항목 제외를 확인하며 client filter로 보정하지 않음
- 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 `aria-invalid`, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 `errorProperty`가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
- 업로드: 파일별 진행률, 취소, 재시도
@@ -590,8 +594,8 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
## 11. API 계약
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.0.0`
`api-contract.openapi.json`15개 path·23개 operation이다.
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.3.0`
`api-contract.openapi.json`25개 path·37개 operation이다.
OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 `EXT-006 현재 구현
기준 계약`에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀
검증의 임시 기준으로 사용한다.
@@ -630,7 +634,7 @@ response schema를 따르며 공통 client가 임의의 상세 응답으로 정
- OpenAPI의 `Accept-Language`는 optional이고 기본값은 `ko`다. `ko|en|ja` 이외 값과 header 누락은 KO로 fallback하며, 프론트엔드는 일관되게 `ko`를 보낸다.
- OpenAPI operation은 전역 `bearerAuth`를 사용한다. 로그인 이외의 관리자 API에는 `Authorization: Bearer {jwt-token}`을 보낸다.
- 공통 `page`는 기본 `0`, 최소 `0`이고 공통 `size`는 기본 `20`, 최소 `1`이다. 전역 최대 `50`은 없다. FanTalk `size`만 설명에 따라 `20..50`으로 보정된다.
- Audio 상세와 Community 목록은 필수 `timezone=Asia/Seoul` query를 보낸다.
- AI 캐릭터 관리자 API의 날짜·시간 request/response는 OpenAPI가 별도로 명시한 nullable 조건을 제외하고 ISO-8601 UTC `Z`를 사용하며 `timezone` query/body field를 보내지 않는다.
- `characterId`는 선택된 대상 캐릭터가 필요한 하위 resource endpoint에만 사용한다.
- 캐릭터 목록·검색과 캐릭터 생성에는 path `characterId`가 없다.
- `/admin/member/login`, `/member/logout`은 현 OpenAPI 범위 밖의 기존 인증 계약이다. 두 endpoint의 현재 구현 기준은 `11.5 EXT-006`에 기록하고 Phase 1 구현과 회귀 검증을 유지하되, 정식 OpenAPI operation으로 표기하지 않는다.
@@ -665,10 +669,15 @@ AI 캐릭터 관리자 domain의 query, multipart part, request/response field
| 인증 | POST | `/admin/member/login` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
| 인증 | POST | `/member/logout` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨 |
| 원작 검색 | GET | `/api/v2/admin/ai-characters/original-works/search` | 제공됨, 필수 `searchTerm` |
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 |
| 오디오 테마 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | 제공됨, query/body 없음 |
| 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨 |
| 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 |
| 오디오 댓글 | GET, POST | `.../audio-contents/{contentId}/comments` | 제공됨 |
| 오디오 댓글 | PUT, DELETE | `.../audio-contents/{contentId}/comments/{commentId}` | 제공됨 |
| 오디오 댓글 답글 | GET | `.../audio-contents/{contentId}/comments/{commentId}/replies` | 제공됨 |
| 시리즈 장르 | GET | `/api/v2/admin/ai-characters/series-genres` | 제공됨, query/body 없음 |
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨 |
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨 |
@@ -677,8 +686,13 @@ AI 캐릭터 관리자 domain의 query, multipart part, request/response field
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨 |
| 커뮤니티 | PUT | `.../community-posts/{postId}` | 제공됨 |
| 커뮤니티 댓글 | GET, POST | `.../community-posts/{postId}/comments` | 제공됨 |
| 커뮤니티 댓글 | PUT, DELETE | `.../community-posts/{postId}/comments/{commentId}` | 제공됨 |
| 커뮤니티 댓글 답글 | GET | `.../community-posts/{postId}/comments/{commentId}/replies` | 제공됨 |
| FanTalk 목록 | GET | `.../{characterId}/fan-talks` | 제공됨 |
| FanTalk 팬 원글 | DELETE | `.../{characterId}/fan-talks/{fanTalkId}` | 제공됨 |
| FanTalk 답변 | POST | `.../{characterId}/fan-talks/{fanTalkId}/replies` | 제공됨 |
| FanTalk 답변 | PUT | `.../{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` | 제공됨, backend 구현 완료 |
### 11.4 OpenAPI 소비 시 주의사항
@@ -689,40 +703,45 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
| 영역 | OpenAPI 계약 | 프론트엔드 처리 |
|---|---|---|
| Character 목록 | query `searchTerm`; `data={totalCount,content}`; item ID `id` | `search`·`items`·`hasNext`로 바꾸지 않는다. |
| Character 원작 검색 | 필수 `searchTerm`; `data``OriginalWorkSearchItem[]` | legacy lookup을 호출하지 않고 반환 `id``originalWorkId`로 사용한다. |
| Character 생성 | 필수 multipart `image`, `request`; request의 필수 `name`, `systemPrompt`, `description`; 성공 `data=null` | 생성 후 목록을 재조회하고 새 ID를 응답에서 추정하지 않는다. |
| Character 상세 | `characterUUID`, `originalWork`를 포함하고 creator field는 없음 | `characterUUID``externalCharacterId`로 취급하지 않고 creator UI는 만들지 않는다. |
| Audio 목록 | query `search_word`; `data={totalCount,items}`; status query/field 없음 | 2자 이상 제목 검색만 보내고 status filter·badge를 만들지 않는다. |
| Audio 테마 | `data=[{id,theme,image}]` | `themeId`·`themeName`·`imageUrl`로 역직렬화하지 않는다. |
| Audio 생성 | multipart `contentFile`, `coverImage`, `request`; `releaseDate``yyyy-MM-dd HH:mm`; 성공 `data.contentId` | `audioFile`, `releaseDateUtc`, `seriesIds`를 보내지 않는다. |
| Audio 생성 | multipart `contentFile`, `coverImage`, `request`; `releaseDate`nullable ISO-8601 UTC `Z`; `timezone` field 없음; 성공 `data.contentId` | 예약 입력을 client에서 UTC로 변환하고 `audioFile`, `releaseDateUtc`, `timezone`, `seriesIds`를 보내지 않는다. |
| Audio 상세 | `timezone` query 없음; nullable `releaseDate`는 ISO-8601 UTC `Z` | UTC 값을 Asia/Seoul 표시로 변환하되 API status를 재계산하지 않는다. |
| Audio 수정 | optional `coverImage`와 제한된 request field만 제공 | content file, 공개 예약, theme, series 연결 수정 UI를 제공하지 않는다. |
| 가격 | Audio 생성·수정과 Community 생성 request의 `price``0..99999` 정수 | `-1`, `100000`, 소수는 제출 전에 거부하고 `0`, `99999`는 허용한다. |
| Series 생성 | 필수 `image`; request의 `keyword`는 문자열; 성공 `data=null` | `keywords` 배열을 보내지 않고 목록으로 이동해 재조회한다. |
| Series 상세 | 요일·장르·keywords·state가 표시용 문자열 | 목록 enum 또는 update payload 값으로 재사용하지 않는다. |
| Series 장르 | query/body 없는 활성 장르 목록; `data=[{id,genre,isAdult}]` | `id``genreId` 선택값으로 사용하고 `0`을 유효값으로 허용하지 않는다. |
| Series 상세 | 목록과 같은 `SeriesListItem`; `genreId`, enum `publishedDaysOfWeek`, enum `state` | 직접 수정 form을 원본값으로 초기화하고 create-only `keyword`를 상세·수정 field로 만들지 않는다. |
| Series 연결·순서 | 후보 `contents/search`; 연결 `{contentIdList}`; 순서 `{ids}` | `{contentIds}`, `{seriesIds}`를 보내지 않는다. |
| Community 목록 | 필수 `timezone`; `data`는 배열이고 pagination metadata 없음 | `audioUrl`을 사용하고 total/hasNext를 추정하지 않는다. |
| Community 목록 | `timezone` query 없음; `data={totalCount,page,size,hasNext,items}` | `audioUrl`과 서버 pagination metadata를 그대로 사용한다. |
| Community 생성·수정 | 생성 part `postImage`/`audioFile`; 수정은 `postImage`만 교체 가능; mutation `data=null` | `image`, `audioSignedUrl`, 수정 audio/price field를 만들지 않는다. |
| FanTalk | 목록 GET 답변 POST만 제공 | 목록 item 기반 UI와 답변 생성만 구현하고 상세·수정·filter/sort는 외부 의존으로 둔다. |
| FanTalk | 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT이 모두 구현됨 | `creatorReplies`의 빈 배열 여부로 작성/수정을 나누고 `creatorReplies[].fanTalkId`를 PUT의 `replyId`로 사용한다. 별도 상세·filter/sort와 중복 오류 key 분기는 현재 UI 범위에서 제외한다. |
| 댓글 | Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 제공 | `writerId === creatorId`일 때만 수정 UI를 노출하고 삭제는 작성자와 관계없이 제공한다. |
### 11.5 백엔드 제공 대기 계약
### 11.5 백엔드 계약 제공·대기 상태
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부
의존**이다. P0 계약 제공받기 전에는 영향을 받는 network integration을
구현하지 않는다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수
있다. 단, `EXT-006`은 이미 구현·검증된 기존 인증 endpoint의 정식 문서화
의존이므로 Phase 3~9 진행을 차단하지 않는다.
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 backend
계약 제공·해결 상태를 추적한다. `해결`된 항목은 완료된 Phase를
묵시적으로 다시 열지 않고 `plan-task.md`의 완료된 Phase 10 후속
vertical slice와 남은 수동 QA에서 검증한다. `EXT-006`은 현재 확정 기능 구현을 차단하지
않으며 정식 OpenAPI 추적성만 후속으로 관리한다.
| ID | 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|---|---:|---|---|
| EXT-001 | P0 | 원작 검색 lookup | Character 원작 선택기 network integration 대기 |
| EXT-002 | P0 | 장르 검색 lookup | 유효한 `genreId` 선택이 필요한 Series 생성 integration 대기 |
| EXT-003 | P0 | Series 수정 form 초기화용 edit DTO 또는 표시 문자열 mapping | 직접 링크에서 genreId·요일 enum·state enum을 안전하게 복원하는 수정 integration 대기 |
| EXT-004 | P0 | FanTalk 상세·답변 수정·답변 상태 filter·sort·유일성 오류 | 목록 item 밖의 상세·수정, 전체 결과 filter와 동시 중복 답변 처리 대기 |
| EXT-005 | P0 | 오디오·커뮤니티 댓글 CRUD와 팬 댓글 삭제 권한 오류 | 댓글·답글 연동과 권한별 오류 처리 대기 |
| EXT-006 | P0 비차단 | 현재 구현된 `POST /admin/member/login`, `POST /member/logout`의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 | 현재 구현과 Phase 1 회귀는 유지하며 Phase 3~9를 차단하지 않는다. 신규 인증 변경과 전체 계약의 단일 추적성만 정식 계약 제공 대기다. |
| EXT-007 | P0 | Character 검색 결과와 Audio·Series·Community 목록의 active-only 반환 보장 | soft delete 뒤 비활성 항목이 서버 목록에서 제외된다는 수용 기준 검증 대기 |
| EXT-008 | P1 | Community pagination의 total/hasNext 또는 종료 규칙 | 신뢰할 수 있는 전체 건수와 마지막 page UI 대기 |
| EXT-009 | P1 | price 최대값 | 현재 0 이상 정수 규칙만 적용하며 상한 계약 제공 시 Audio·Community schema와 경계값 test 갱신 |
| EXT-010 | P1 | 파일 크기·MIME·crop·container/codec의 backend 검증 계약 | PRD의 client 사전 검증 유지하 server와 동일 경계라는 완료 주장은 계약 제공 후 검증 |
| EXT-011 | P0 | 도메인별 오류 | 기능별 정확한 비2xx status와 message key 분기가 필요한 흐름 대기 |
| ID | 우선순위 | 백엔드 제공 필요 계약 | 사용하는 화면 | 관련 API 흐름 | 현재 프론트엔드 영향 |
|---|---:|---|---|---|---|
| EXT-001 | 해결 | `GET /api/v2/admin/ai-characters/original-works/search?searchTerm=...``OriginalWorkSearchItem[]`가 OpenAPI에 `implemented`로 추가됐다. | 캐릭터 생성 `/ai-characters/new`, 캐릭터 수정 `/ai-characters/:characterId/profile` | 원작 검색 GET과 Character create/update의 `originalWorkId` | legacy 후보 대신 v2 lookup을 사용하는 선택기·contract test를 Phase 10에서 구현 완료했다. |
| EXT-002 | 해결 | query/body 없는 `GET /api/v2/admin/ai-characters/series-genres``{id,genre,isAdult}[]`가 OpenAPI에 `implemented`로 추가됐다. | 시리즈 생성 `/ai-characters/:characterId/series/new`, 수정 `/ai-characters/:characterId/series/:seriesId/edit` | 장르 목록 GET과 Series create/update의 `genreId` | 장르 선택과 Series 생성·수정 후속 구현을 Phase 10에서 완료했다. |
| EXT-003 | 필요 없음 | 별도 Series 수정 DTO 또는 표시 문자열 mapping 계약 | 시리즈 수정 `/ai-characters/:characterId/series/:seriesId/edit`, 시리즈 상세 직접 진입 | `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}``SeriesListItem`과 같은 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 내려주면 충분하다. | 별도 외부 의존으로 추적하지 않는다. 수정 화면은 상세 응답으로 기존 값을 초기화하고 장르 API는 option 목록 표시용으로만 사용한다. |
| EXT-004 | 해결 | 답변 유무는 `creatorReplies`의 빈 배열 여부로 판정한다. 답변 PUT과 팬 원글 DELETE가 `implemented`이며 PUT path의 `replyId``creatorReplies[].fanTalkId`를 사용한다. 별도 상세·filter·sort와 중복 오류 key는 현재 UI에 필요하지 않아 `FANTALK-002`, `FANTALK-008`에서 제외했다. | FanTalk 목록 `/ai-characters/:characterId/fan-talks`, FanTalk item Sheet/panel | 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT을 사용한다. 목록 item이 Sheet source이고 backend 반환 순서를 유지하므로 별도 상세 GET·filter·sort query를 요청하지 않는다. | Phase 10에서 답변 수정·팬 원글 삭제의 mock/client integration을 구현 완료했다. 실제 개발 API 수동 QA에서 동시 POST 후 활성 답변 1개 유지 등 server 수용 기준을 분리 확인한다. |
| EXT-005 | 해결 | Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 operation과 DTO가 OpenAPI에 `implemented`로 추가됐다. `writerId === creatorId`이면 AI 작성 댓글로 판정한다. | 오디오 상세 `/ai-characters/:characterId/audio-contents/:contentId`, 커뮤니티 Sheet `/ai-characters/:characterId/community-posts` | 두 target의 `comments`, `comments/{commentId}`, `comments/{commentId}/replies` | Phase 10에서 2단계 thread, 작성·수정·삭제, Comments mock/client integration을 구현 완료했다. 팬 작성 댓글은 수정하지 않고 관리자 DELETE만 제공한다. |
| EXT-006 | P0 비차단 | 현재 구현된 `POST /admin/member/login`, `POST /member/logout`의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 | 로그인 `/login`, 보호 shell 로그아웃 | `POST /admin/member/login`, `POST /member/logout` | 현재 구현과 Phase 1 회귀는 유지한다. 정식 OpenAPI 추적성만 대기하며 Phase 3~9를 차단하지 않는다. |
| EXT-007 | 해결 | Character 검색 결과와 Audio·Series·Community 목록 API는 `isActive=true`인 항목만 반환한다. | 캐릭터 목록, 오디오 목록, 시리즈 목록, 커뮤니티 목록 | 각 목록 GET: `/ai-characters`, `/audio-contents`, `/series`, `/community-posts` | 프론트엔드 변경 없음. 활성 query·client-side filter를 만들지 않고 soft delete 후 목록 재조회 결과를 server integration에서 확인한다. |
| EXT-008 | 해결 | Community 목록 응답이 `data={totalCount,page,size,hasNext,items}`로 보완됐다. | 커뮤니티 목록 `/ai-characters/:characterId/community-posts` | `GET /api/v2/admin/ai-characters/{characterId}/community-posts` | Phase 10에서 기존 배열 parser와 `timezone` query 제거, 서버 pagination metadata 기반 UI·test 갱신을 완료했다. |
| EXT-009 | 해결 | price 최대값`99999` 캔이며 관련 OpenAPI request schema에도 `minimum=0`, `maximum=99999`를 명시했다. | 오디오 생성·수정, 커뮤니티 생성 | Audio `POST/PUT .../audio-contents`, Community `POST .../community-posts` | Phase 10에서 schema·form과 경계값 test를 `0..99999` 정수로 갱신 완료했다. |
| EXT-010 | 해결 | 파일 정책은 `FILE-001~015`로 확정됐다. 이미지 비율·crop 결과는 client가 보장하고 backend는 검증하지 않는다. 파일 용량, MIME 등 서버에서 확인 가능한 항목은 backend가 동일하게 검증한다. | 캐릭터 생성·수정, 오디오 생성·수정, 시리즈 생성, 커뮤니티 생성·수정 | 각 multipart 생성·수정 API의 image/audio file part | 외부 의존에서 제외한다. client는 기존 사전 검증 유지하고, server integration에서는 용량·MIME reject만 확인한다. |
| EXT-011 | 해결 | OpenAPI에 명시된 공통 오류 envelope/status만 도메인 화면에서 사용한다. OpenAPI 밖의 도메인별 오류 key는 추정하지 않고, 미정의 오류는 `알 수 없는 오류가 발생했습니다.`로 표시한다. | Character, Audio, Series, Community, FanTalk의 form·list·detail·mutation 화면 | 각 도메인 API의 비2xx 응답 `status`, `message`, `errorProperty` | 기능별 정확한 message key 분기가 필요한 inline 오류·충돌·중복·missing-id 처리는 후속 계약 전까지 만들지 않는다. |
#### EXT-006 현재 구현 기준 계약
@@ -773,19 +792,19 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
### 감사 로그 미결 사항과 권고
현재 감사 로그 정책은 **미결**다.
감사 로그 조회 UI는 현재 릴리스에서 **제외**다.
**권고:** 이번 범위에서는 백엔드가 관리자 ID, 대상 캐릭터 ID, resource 종류와 ID, action, 성공/실패, 서버 시각, request ID, 민감정보를 제거한 변경 요약을 기록한다. 관리자 UI의 감사 로그 조회 화면은 후속 범위로 둔다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.
**처리:** 이번 범위에서는 백엔드 감사 기록을 우선한다. 조회 화면이 필요해지면 조회 endpoint, 권한, 필터 계약을 포함한 별도 Phase로 다시 계획한다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.
## 13. 성능과 품질 요구사항
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 종료 metadata 계약이 제공되기 전까지 전체 건수·마지막 page를 표시하지 않는다.
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 서버가 제공하는 `totalCount`, `page`, `size`, `hasNext` metadata로 전체 건수와 다음 page 여부를 표시다.
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
- 목록 검색 debounce 시간은 구현 시 300ms 전후로 일관되게 적용한다.
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
- 브라우저 지원 범위는 데스크톱 Chrome/Edge/Safari 최신 2개 주요 버전과 모바일 Chrome/Safari 최신 2개 주요 버전이다.
- 브라우저 지원 범위는 데스크톱 Chrome과 모바일 Chrome이다. 로컬 자동 Gate는 Chromium/mobile Chrome project로 검증한다.
- mock/server mode는 build-time 환경 설정으로 명시적으로 선택하며 runtime 404 fallback을 사용하지 않는다.
- domain fixture와 browser handler는 `api-contract.openapi.json`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
@@ -800,10 +819,10 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
- 로그아웃 API가 성공하거나 네트워크·비2xx 오류로 실패해도 로컬 session이 제거되고 로그인 화면으로 이동하며, 실패한 경우 경고가 표시되고 session이 복원되지 않는다.
- 캐릭터 생성 multipart가 필수 `image``request`를 보내고 request에 `name`, `systemPrompt`, `description`이 포함되며 `isActive``externalCharacterId`는 포함되지 않는다.
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 서버 목록에서 비활성 항목이 제외되는지는 active-only 계약 제공 후 검증한다.
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 재조회된 각 서버 목록에는 `isActive=false`인 항목이 없어야 하며 클라이언트 필터로 이 결과를 만들지 않는다.
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 목록 이동·재조회를 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
- 오디오 생성에서 즉시 공개는 `releaseDate=null`, 예약 공개는 미래 Asia/Seoul 시각을 `yyyy-MM-dd HH:mm`로 보내고 `timezone="Asia/Seoul"`을 포함한다.
- 오디오 생성에서 즉시 공개는 `releaseDate=null`을 보내고, 예약 공개는 미래 Asia/Seoul 입력을 클라이언트에서 ISO-8601 UTC `Z`로 변환해 보낸다. 생성·상세·커뮤니티 목록 요청에는 `timezone` field/query가 없어야 한다.
- MP3, AAC, M4A 업로드의 진행률·취소·재시도와 `1,024,000,000 bytes` 허용·`1,024,000,001 bytes` 거부 경계 검증이 동작한다.
- 오디오 콘텐츠와 커뮤니티 첨부 audio가 동일한 확장자·MIME·최대 크기·재생 길이 정책을 사용한다.
- `.m4a``audio/mp4``audio/x-m4a`를 허용하되 호환 MIME도 실제 MP4/M4A container·codec 검증을 통과해야 한다.
@@ -816,14 +835,19 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
- crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다.
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
- 오디오 콘텐츠 생성 시 테마 목록 `data[]``id`, `theme`, `image`를 query/body 없이 조회하고 선택한 `id`를 request의 `themeId`에 포함한다.
- 캐릭터 원작 선택기는 필수 `searchTerm`으로 v2 원작 검색 API를 호출하고 선택한 `OriginalWorkSearchItem.id`를 create/update의 `originalWorkId`로 보낸다.
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
- 오디오 생성·수정과 커뮤니티 생성의 가격은 `0`, `99999`를 허용하고 `-1`, `100000`, 소수를 제출 전에 거부한다.
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
- Series 생성에는 state를 보내지 않고 `keyword` 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
- FanTalk 목록 item의 `creatorReplies`에 답변이 있으면 두 번째 POST를 UI에서 차단한다. 답변 수정·전체 결과 filter·직접 또는 동시 중복 요청 거부는 외부 계약이 제공된 뒤 수용 기준을 활성화한다.
- 댓글 계약이 제공된 뒤 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
- 모바일에서 조회·오디오 재생과 FanTalk 답변 작성이 가능하다. 댓글 관리와 FanTalk 답변 수정은 각 외부 계약 제공 후 같은 capability로 활성화한다.
- 시리즈 장르 목록을 query/body 없이 조회하고 선택한 `SeriesGenreItem.id`를 create/update의 `genreId`로 보내며, 상세의 `genreId`·요일 enum·state enum으로 수정 form을 초기화한다.
- 커뮤니티 목록은 `timezone` 없이 `page`, `size`를 보내고 서버의 `totalCount`, `page`, `size`, `hasNext`, `items`를 사용한다.
- FanTalk 목록 item의 `creatorReplies`가 비어 있으면 답변 작성, 비어 있지 않으면 답변 수정 UI를 제공한다. 수정 path의 `replyId`에는 첫 답변의 `fanTalkId`를 사용하고 client의 두 번째 POST를 차단한다. 실제 server에서는 동일 root에 대한 동시 POST 후 재조회해도 활성 creator reply가 하나만 존재해야 하며, 정확한 충돌 status/message key를 client에서 분기하지 않는다.
- 팬 작성 FanTalk 원글 soft delete가 성공하면 목록을 재조회하고 열린 Sheet를 닫는다. 연결된 creator reply가 함께 삭제됐다고 가정하지 않는다.
- 댓글은 루트와 직접 답글의 2단계를 넘지 않는다. `writerId === creatorId`인 댓글에만 수정 UI를 제공하고, soft delete는 작성자와 관계없이 해당 row에 제공한다.
- 모바일에서 조회·오디오 재생, 댓글 전체 관리, FanTalk 답변 작성·수정과 팬 원글 soft delete가 가능하다.
- `npm run dev:mock`에서 실제 backend 요청 없이 제공 계약 범위의 최종 UI happy path를 확인할 수 있고 mock mode 안내가 표시된다.
- 기본 `npm run dev`에서는 실제 개발 API를 사용하며 404·network error가 mock 응답으로 바뀌지 않는다.
- production build에는 browser mock이 활성화되지 않고 mock mode 설정을 허용하지 않는다.
@@ -848,7 +872,7 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|---|---|---|---|
| OQ-009 | 확정 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다. |
| OQ-010 | 미결 | 감사 로그 UI 제공 여부 | backend 기록 우선, 조회 UI는 후속 범위 |
| OQ-010 | 제외 | 감사 로그 UI 제공 여부 | 현재 릴리스에서는 조회 UI를 만들지 않고 backend 기록 우선한다. 포함 시 backend 조회 계약을 포함한 별도 Phase로 계획한다. |
## 16. 결정 기록
@@ -890,7 +914,17 @@ OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI
| 2026-07-27 | backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 확인할 수 있도록 명시적 개발 전용 browser MSW mode를 제공한다. 실제 404 자동 fallback은 금지하고 mock UI 완료와 실제 server 연동 완료를 분리한다. |
| 2026-07-28 | 삭제된 `api-contract.md``api-contract.openapi.json`으로 대체하고, endpoint·query·multipart·request/response·오류는 OpenAPI를 단일 진실 원천으로 사용한다. OpenAPI에 없는 제품·UI 정책만 PRD가 소유한다. |
| 2026-07-28 | 새 OpenAPI에 맞춰 Character 생성 image/systemPrompt 필수, Audio의 `search_word`·`contentFile`·`releaseDate`·테마 field, Series의 `keyword`·`contentIdList`·`ids`, Community의 `timezone`·`postImage`·`audioUrl`, FanTalk 목록 GET을 구현 기준으로 정정한다. |
| 2026-07-28 | OpenAPI에 없는 인증 정식 명세, 원작·장르 lookup, Series 수정 form의 edit DTO, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. |
| 2026-07-28 | OpenAPI에 없는 인증 정식 명세, 원작·장르 lookup, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. Series 수정 form은 상세 응답이 list item과 같은 `genreId`, enum 요일 배열, enum state를 제공하면 별도 edit DTO 없이 초기화한다. |
| 2026-07-28 | `EXT-003`은 별도 backend 계약 항목에서 제외한다. 시리즈 상세 API가 `SeriesListItem`과 같은 수정용 원본값을 내려주고, 장르 API는 선택 option 목록 표시용으로 호출하는 방식으로 처리한다. |
| 2026-07-28 | OQ-009는 초기 UI를 먼저 구현하고 실제 페이지를 보며 문자열 최대 길이와 배열 최대 개수를 제안한 뒤 backend 호환을 확인해 확정하는 절차로 종결한다. 실제 최대값 확정 전에는 임의 상한을 추가하지 않는다. |
| 2026-07-28 | `EXT-006`에 현재 구현된 `POST /admin/member/login``POST /member/logout`의 request·response·인증·client 처리 기준을 기록한다. 정식 OpenAPI 포함은 비차단 외부 의존이며 기존 인증 구현과 Phase 3~9 진행을 유지한다. |
| 2026-07-28 | Phase 3부터는 OpenAPI 제공 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행한다. 미제공 계약은 영향을 받는 기능만 후속 범위로 남기고, 계약 도착 후 별도 vertical slice와 관련 Phase Gate·Phase 9를 다시 실행한다. |
| 2026-07-28 | 감사 로그 조회 UI는 현재 릴리스에서 제외한다. backend 감사 기록을 우선하고, 조회 UI가 필요해지면 조회 endpoint·권한·필터 계약을 포함한 후속 Phase로 다시 계획한다. |
| 2026-07-29 | `EXT-010`은 해결로 정정한다. 이미지 비율·crop 결과는 backend가 검증하지 않고 client가 보장하며, 파일 용량·MIME 등 서버에서 확인 가능한 항목은 backend가 `FILE-001~015`와 동일하게 검증한다. |
| 2026-07-29 | OpenAPI 2.3.0에 추가된 원작 검색, 활성 장르 목록, Audio·Community 댓글, Community pagination, FanTalk 팬 원글 DELETE와 답변 PUT을 후속 구현 계약으로 채택한다. 목록 API는 서버가 `isActive=true` 항목만 반환하므로 client 활성 filter를 추가하지 않는다. |
| 2026-07-29 | 오디오 예약 공개는 Asia/Seoul 입력을 client에서 ISO-8601 UTC `Z`로 변환해 `releaseDate`에 보내고 `timezone` field/query를 제거한다. 즉시 공개는 `releaseDate=null`을 유지한다. |
| 2026-07-29 | FanTalk 답변 여부는 `creatorReplies`의 빈 배열 여부로 판단하고 수정 path의 `replyId`에는 `creatorReplies[].fanTalkId`를 사용한다. 댓글은 `writerId === creatorId`일 때 AI 캐릭터 작성으로 판단하며 팬 작성 FanTalk 원글 soft delete를 이번 후속 구현 범위에 포함한다. |
| 2026-07-29 | 백엔드에서 FanTalk 답변 수정 API가 이미 구현됐고 OpenAPI status만 누락됐음을 확인했다. 해당 operation을 `implemented`로 정정하고 Phase 10에서 mock/client뿐 아니라 실제 server integration까지 구현·검증한다. |
| 2026-07-29 | `EXT-009`의 가격 범위 `0..99999`를 Audio 생성·수정과 Community 생성 OpenAPI request schema, 요구사항, Phase 10 경계 test에 동일하게 적용한다. |
| 2026-07-29 | 현재 FanTalk UI는 목록 item 기반 Sheet와 backend 반환 순서만 사용하므로 별도 상세 GET·답변 상태 filter·sort query가 필요하지 않다고 확정했다. 중복 생성 전용 오류 key 분기도 제외하고 일반 오류 후 목록을 재조회한다. 답변 1개 불변식은 `FANTALK-003`의 backend 수용 기준으로 server integration에서 검증하므로 `EXT-004`를 해결로 종결한다. |
| 2026-07-31 | 사용자 직접 지시에 따라 로컬 자동 Gate와 지원 browser 범위는 데스크톱 Chrome·모바일 Chrome의 Chrome 2종으로 한정한다. Playwright는 Chromium/mobile Chrome project만 유지하고, 현재 제품 지원 대상이 아닌 WebKit·Mobile Safari 자동 실행과 수동 QA는 테스트 시간을 크게 늘리므로 현재 릴리스 범위에서 제외한다. |

View File

@@ -0,0 +1,241 @@
# Phase 0 프로젝트 기반 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 0 / 프로젝트 기반, 환경 설정, 기본 Gate |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Phase 0의 환경별 API origin, 실행 명령, 기본 품질 Gate가 현재 구현과 일치하는지 확인한다.
- 완료 기록과 현재 working tree의 코드·테스트·문서가 같은 기준을 사용하는지 검증한다.
### 포함 범위
- 코드·설정: `.env*`, `src/shared/config`, Vite·Vitest·Playwright 설정
- 테스트: 전체 unit, mock/server Playwright Gate, typecheck, lint, build
- 문서: PRD 공통 API 원칙, `P0-*`, README와 환경 가이드
- 수동 검증: 환경 파일과 실제 요청 intercept origin 정적 대조
### 제외 범위
- 외부 개발 API에 대한 실제 ADMIN 계정 로그인과 운영 데이터 검증
- 발견 사항의 코드·테스트 수정
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 |
| Medium | 제한된 조건의 기능·접근성·복구 문제 |
| Low | 비핵심 UX 또는 문서 정합성 문제 |
상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: PRD §4, §6, §13, §14
- 계획: `P0-T1`~`P0-GATE`
- 설정: `.env.development:1`, `.env.production:1`
- 문서: `README.md:21-22`, `docs/agent-guide/environment.md:3-4`
- 테스트: `tests/e2e/auth.spec.ts:3`, `accessibility-shell.spec.ts:4`, `server-mode-boundary.spec.ts:4`, `error-mapping.spec.ts:12`, `comments-test-support.ts:3`
### 실행 환경
```text
OS: macOS 26.0 (Build 25A354)
Node: v24.12.0
npm: 11.7.0
Browser: Playwright Chromium, Mobile Chrome
환경 변수: VITE_API_MODE=mock/server, VITE_API_BASE_URL(값은 저장소 공개 설정만 대조)
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` | 성공 | exit 0 |
| `npm run lint` | 성공 | exit 0 |
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run build` | 성공 | exit 0, 253 modules transformed |
| `npm run e2e:mock` | 실패 | 201 passed, 22 skipped, 5 failed; 4개 프로젝트의 error-mapping이 과거 origin으로 요청해 500 수신 |
| `npm run e2e:mock -- tests/e2e/series.spec.ts --project=webkit --grep "desktop and tablet Series management flow remains available at 768px"` | 성공 | 1 passed; 전체 실행의 단일 timeout은 재현되지 않음 |
| `npm run e2e` | 실패 | 12 passed, 24 failed; auth·shell·server boundary가 현재 앱 origin을 intercept하지 못함 |
| `jq` operation/status 집계 및 component schema `$ref` 차집합 검증 | 성공 | 두 명령 모두 exit 0; 25 paths, 37 operations, status 전부 `implemented`, 누락 schema ref 없음 |
OpenAPI 검증은 다음 명령으로 실행했다.
```bash
jq '[.paths[] | to_entries[] | select(.key | IN("get", "put", "post", "delete", "patch", "head", "options", "trace")) | .value] as $operations | {openapi, version: .info.version, paths: (.paths | length), operations: ($operations | length), statuses: ([$operations[]."x-implementation-status"] | unique)}' docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
jq -e '((([.. | objects | .["$ref"]? // empty | select(startswith("#/components/schemas/")) | split("/")[-1]] | unique) - (.components.schemas | keys)) | length) == 0' docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
```
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P0-004` | High | 수정 완료 | API origin 변경이 문서와 Playwright route에 반영되지 않아 브라우저 Gate가 실패한다 | `P0-R2` | `P0-R2` |
## 6. 발견 사항 상세
### REV-P0-004 — API origin 변경이 문서와 Playwright route에 반영되지 않아 브라우저 Gate가 실패한다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** PRD §4, §6.2, §13, §14
- **관련 계약:** 모든 OpenAPI operation의 server-mode 요청 경계
- **소유 Task:** 신규 `P0-R2`
**관찰 내용**
현재 개발·운영 환경 파일은 각각 `https://test-api.sodalive.net`, `https://api.sodalive.net`을 사용한다. README, 환경 가이드와 다섯 Playwright 지원 파일은 과거 `test-character-admin`/`character-admin` origin을 사용한다. 앱 요청과 테스트 route가 서로 다른 origin을 바라보므로 mock 오류 매핑과 server auth·shell·retry 검증이 실제 요청을 가로채지 못한다.
**근거**
- 코드: `.env.development:1`, `.env.production:1`
- 테스트: `tests/e2e/auth.spec.ts:3`, `accessibility-shell.spec.ts:4`, `server-mode-boundary.spec.ts:4`, `error-mapping.spec.ts:12`, `comments-test-support.ts:3`
- 문서: `README.md:21-22`, `docs/agent-guide/environment.md:3-4`
- 자동화: mock Gate 4개 동일 error-mapping 실패, server Gate 24개 실패
**재현 또는 검증 절차**
1. 현재 `.env.development`를 유지한다.
2. `npm run e2e:mock`을 실행한다.
3. 과거 origin 요청이 MSW handler와 일치하지 않아 기대한 401 대신 500이 반환되는 것을 확인한다.
4. `npm run e2e`를 실행해 로그인·shell·server retry route가 현재 요청을 intercept해야 하지만 24개 test가 실패하는 것을 확인한다.
**영향**
브라우저 Gate가 제품 회귀와 무관하게 실패하고, server-mode 인증·오류 경계 및 Comments 연동을 올바른 origin에서 검증하지 못한다. README를 따르는 개발자도 잘못된 서버 주소를 사용하게 된다.
**권장 조치**
공개 환경 파일을 단일 기준으로 README·환경 가이드·Playwright route/probe를 동기화하고, 과거 host 검색과 mock/server focused test를 회귀 증거로 추가한다.
**판정 기록**
- 2026-07-30 — 설정·문서·테스트 문자열 대조와 두 브라우저 Gate의 반복 실패로 확정.
- 2026-07-30 — `P0-R2`에서 E2E API origin helper와 문서 현재값을 정렬하고 focused/full Gate 분할 검증으로 수정 완료 판정.
## 7. 확정 항목의 plan·goal 전환
- `REV-P0-004``plan-task.md` 신규 `P0-R2`
- goal objective: `[P0-R2] API origin 단일 기준을 문서와 모든 Playwright route/probe에 동기화하고 mock/server Gate 회귀를 방지한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 설정·문서·정적·자동 Gate 확인 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P0-R2` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4에 실제 결과 기록 |
**최종 결론:** 확정 발견 사항 수정 완료.
**남은 항목:** 없음.
## 9. 수정 후 검증 기록
- 2026-07-30 — `P0-R2` RED: `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts`는 4 failed / 4 passed로 과거 origin unhandled 500을 재현했고, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium`은 2 failed / 2 passed로 stale route/probe 실패를 재현했다.
- 2026-07-30 — `tests/e2e/api-base-url.ts`를 추가해 E2E route/probe가 `.env.development``VITE_API_BASE_URL`을 읽게 하고, README와 환경 가이드를 `https://test-api.sodalive.net` / `https://api.sodalive.net`로 정렬했다.
- 2026-07-30 — GREEN focused: `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts` 8 passed, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium` 4 passed, `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium` 3 passed.
- 2026-07-30 — Gate: `npm run e2e` 36 passed. `npm run e2e:mock` 단일 실행은 900초 제한으로 188/228 진행 중 timeout됐으나, 같은 allowlist를 project별로 분할해 `chromium` 57 passed, `webkit` 49 passed / 8 skipped, `mobile-chrome` 52 passed / 5 skipped, `mobile-safari` 48 passed / 9 skipped로 전체 mock matrix를 확인했다.
- 2026-07-30 — 정적 검증: `rg -n 'test-character-admin|character-admin\.sodalive\.net' README.md docs/agent-guide tests/e2e` no matches, `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0, `tests/e2e` LSP diagnostics 오류 0건.
## 10. 2차 점검 결과 — 2026-07-30
- `npm ci`, `npm run typecheck`, `npm run lint`, `npm run build`가 모두 exit 0이었다.
- 전체 unit은 72 files / 358 tests, server allowlist E2E는 36 tests, mock Chromium matrix는 57 tests가 통과했다.
- Phase 0 신규 발견은 없으며 `REV-P0-004` 수정 완료 상태가 유지된다.
## 11. 2026-07-31 재점검
- **기준:** commit `dd30e36323543e8f60e9983326503653e8001f12`, 재점검 시작 시 working tree 변경 220개. 기존 사용자 변경은 수정하지 않고 현재 tree를 검토했다.
- **검증:** `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0, `npm run test:run`은 72 files / 360 tests passed, `npm run e2e`는 36 passed였다.
- **계약·구성 판정:** package scripts, 환경 mode 경계, build와 server allowlist에서 Phase 0 신규 결함은 확인되지 않았다. `REV-P0-004`는 수정 완료 상태를 유지한다.
- **교차 Phase:** 당시 Mobile Safari 간헐 실패 후보는 이후 지원 project 축소로 현재 Task에서 제외했다.
- **신규 Task:** 없음.
## 12. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** package scripts, TypeScript·ESLint·production build, server/mock mode 실행 경계, 현재 Playwright project 구성을 `P0` Gate와 재대조했다.
- **실행 증거:** `npm run typecheck`, `npm run lint`, `npm run build` exit 0. server E2E는 샌드박스의 최초 `listen EPERM`을 제품 실패와 분리한 뒤 승인된 로컬 실행에서 18/18 통과했다.
- **판정:** 현재 지원 project인 Chromium·Mobile Chrome 기준 기반 설정과 server allowlist에서 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API의 가용성·운영 배포 환경은 이 Phase 자동 검증 범위 밖이다.
- **신규 Task:** 없음.
## 13. 종합 재점검 — 2026-07-31
### `REV-P0-005` — 문서 디렉터리의 `.DS_Store`가 version control index에 포함됨
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | Phase 0 재현 가능한 프로젝트 기반과 저장소 문서 유지보수; API 계약 영향 없음 |
| 소유 Task | `P0-R3` |
| 근거 | `git ls-files 'docs/20260725_AI캐릭터관리자웹/.DS_Store'`가 해당 binary entry를 반환하고 `.gitignore`에는 `.DS_Store` 규칙이 없다. |
**재현 또는 검증 절차**
1. `git ls-files | rg '(^|/)\.DS_Store$'`를 실행한다.
2. `docs/20260725_AI캐릭터관리자웹/.DS_Store`가 출력되는지 확인한다.
3. `rg -n '^\.DS_Store$' .gitignore`가 no matches인지 확인한다.
**영향과 권장 조치**
운영체제별 binary metadata가 문서 변경에 섞여 불필요한 diff와 충돌을 만들 수 있다. 해당 entry만 제거하고 저장소 전역 ignore 규칙을 추가한다. 다른 사용자 파일이나 문서 내용은 정리하지 않는다.
**판정 기록**
- 2026-07-31 — 현재 index와 ignore 규칙을 정적으로 대조해 확정.
- 2026-07-31 — 애플리케이션 코드는 수정하지 않고 `plan-task.md` 신규 `P0-R3`로 전환.
- 2026-07-31 — `P0-R3`에서 `.DS_Store`를 index와 working tree에서 제거하고 저장소 ignore 규칙을 추가해 수정 완료 판정.
### Phase 0 결론
- **자동 검증:** `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` 모두 exit 0. 두 build는 기존 500kB 초과 chunk warning만 출력했다.
- **판정:** runtime·환경 mode·script의 신규 결함은 없다. Low 1건은 `P0-R3`에서 수정 완료됐다.
- **남은 위험:** 실제 배포 환경과 개발 API 가용성은 자동 검증 범위 밖이다.
### P0-R3 수정 후 검증 — 2026-07-31
- 무엇을: 문서 디렉터리의 `.DS_Store` staged entry를 제거하고 저장소 ignore 규칙을 추가했다.
- 왜: 운영체제별 binary metadata가 문서 변경과 충돌에 섞이지 않게 하기 위해서다.
- 검증: `git ls-files | rg '(^|/)\.DS_Store$'``git status --short --untracked-files=all | rg '\.DS_Store'`는 no matches, `rg -n '^\.DS_Store$' .gitignore``7:.DS_Store`, `git diff --check -- .gitignore docs/20260725_AI캐릭터관리자웹`은 exit 0이었다.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **기준:** commit `dd30e36323543e8f60e9983326503653e8001f12`와 사용자 변경을 포함한 current working tree.
- **검증:** `typecheck`, `lint`, `build:dev`, `build:prod` exit 0, server-mode Chromium·WebKit·Mobile Chrome·Mobile Safari smoke/auth/boundary 36 passed. 두 build는 기존 500kB 초과 chunk warning만 표시했다.
- **판정:** Phase 0 runtime·환경·build 소유의 신규 기능 결함은 없다. 전체 unit 실패는 `P9-R7`의 mock E2E script 문서·test 정합성으로 `REV-P9-008`에 귀속한다.
- **문서 교차 항목:** 상단 리뷰 상태가 완료된 `P0-R3`와 어긋나는 문제는 `REV-P10-008`/`P10-R7`에서 과거 기록을 보존한 채 정리한다.
- **신규 Phase 0 Task:** 없음.
## 15. 최종 재검증 — 2026-07-31
- **검토 범위:** package script, server/mock mode, 환경 변수, 개발·운영 build와 browser project 구성을 다시 확인했다.
- **실행 증거:** `npm run test:run` 78 files / 394 tests, `npm run e2e` 36 tests, `npm run e2e:mock` 186 passed / 22 skipped가 통과했다. `typecheck`, `lint`, `build:dev`, `build:prod`, staged/unstaged diff check도 exit 0이었다.
- **판정:** Phase 0 소유의 확정 신규 발견 사항 없음. build의 502.94kB chunk warning은 PRD·plan에 hard limit가 없어 이번 Task로 전환하지 않는다.
- **남은 위험:** 실제 배포 환경과 개발 API 가용성은 자동 검증 범위 밖이다.
- **신규 Phase 0 Task:** 없음.
## 16. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** `package.json`, Vite/Vitest/Playwright 설정, server/mock 명령, 환경 mode 경계, build 산출물과 `P0`·`P9` Gate 기록을 PRD·plan에 대조했다.
- **실행 증거:** `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` exit 0, server E2E 4 projects / 36 passed. build는 기존 503.04kB chunk warning만 표시했다. 전체 unit은 79 files / 397 tests 중 5 failed / 392 passed였고, 기반 코드가 아닌 integration harness 비결정성 `REV-P9-009`/`P9-R9`로 분리했다.
- **판정:** Phase 0 runtime·실행 스크립트·환경 mode 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 0 Task 없음.
- **남은 위험:** 503.04kB warning은 문서 hard limit가 없어 Task로 전환하지 않았다. 실제 배포 환경과 개발 API 가용성은 자동 범위 밖이다.

View File

@@ -0,0 +1,504 @@
# Phase 1 플랫폼·인증·공통 UI 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / API client, 인증·인가, shell, 공통 UI |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 재리뷰 판정 완료, `REV-P1-023`/`P1-R17` 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 공통 API client, 인증 session, ADMIN role, shell과 공유 UI가 요구사항·계약을 충족하는지 확인한다.
- 이전 Phase 1 리뷰의 수정 완료 항목이 현재 코드와 test에서 유지되는지 점검한다.
### 포함 범위
- 코드: `src/shared/api`, `src/features/auth`, `src/layouts`, `src/shared/ui`
- 테스트: 관련 unit/integration, 전체 정적·브라우저 Gate
- 문서: `AUTH-*`, `P1-*`, OpenAPI 인증 operation
- 수동 검증: session storage·header·401 처리·keyboard/ARIA 정적 대조
### 제외 범위
- 실제 개발 서버 계정과 role fixture를 이용한 외부 로그인
- Phase 0의 API origin 정합성 결함 수정
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 요구사항·계약 위반, 보안·데이터 위험, 회귀와 test 누락을 우선한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUTH-001`~`AUTH-013`, PRD §12~§14
- 계약: login/logout, bearer 인증, 공통 오류 envelope
- 계획: `P1-T1`~`P1-GATE`, 이전 회귀 Task
- 코드·테스트: `src/shared/api/**`, `src/features/auth/**`, `src/layouts/**`, `src/shared/ui/**`, 관련 `tests/e2e`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright: Chromium, Mobile Chrome
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` / `npm run lint` | 성공 | 모두 exit 0 |
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run build` | 성공 | 253 modules transformed |
| `npm run e2e:mock` | 실패 | Phase 0 소유 origin 불일치 4건과 비재현 timeout 1건; Phase 1 신규 결함으로 중복 등록하지 않음 |
| `npm run e2e` | 실패 | Phase 0 소유 origin 불일치로 12 passed, 24 failed |
| 인증·공통 UI 정적 대조 | 성공 | 신규 확정 위반 없음 |
| 공통 pagination helper 2차 계약 대조 | 실패 | production 사용처가 없는 `createPageParams`가 공통 size를 `20..50`으로 clamp하고 음수 page를 허용 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-014` | Low | 수정 완료 | 사용되지 않는 공통 pagination helper가 FanTalk 전용 size clamp를 전역 규칙처럼 고정한다 | `P1-R8` | `P1-R8` |
## 6. 발견 사항 상세
### REV-P1-014 — 사용되지 않는 공통 pagination helper가 FanTalk 전용 size clamp를 전역 규칙처럼 고정한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** PRD §11.1 공통 pagination
- **관련 계약:** OpenAPI 공통 `Page`, `Size`; FanTalk만 별도 `size=20..50`
- **소유 Task:** 신규 `P1-R8`
**관찰 내용**
`createPageParams``size=1`을 20으로, `size=51`을 50으로 바꾸고 음수 page를 그대로 둔다. 이는 공통 `page >= 0`, `size >= 1`, 전역 maximum 없음과 다르다. production 사용처는 없고 단위 test만 이 동작을 “documented”로 고정해 미래 소비자가 잘못된 전역 규칙을 재사용할 위험이 있다.
**근거**
- 코드: `src/shared/api/pagination.ts:14-23`
- 테스트: `src/shared/api/__tests__/pagination.test.ts:17-37`
- 사용처 검색: 위 두 파일 외 `createPageParams` 참조 0건
- 문서: `prd.md:636`
- 계약: OpenAPI 공통 `Size`는 default 20, minimum 1이며 maximum이 없음
**재현 또는 검증 절차**
1. `createPageParams({ page: -1, size: 1 })`을 호출하면 `{ page: -1, size: 20 }`이 된다.
2. `createPageParams({ size: 51 })`을 호출하면 size가 50이 된다.
3. `rg -n 'createPageParams' src`로 production 소비자가 없고 helper test만 남아 있음을 확인한다.
**영향**
현재 화면에는 직접 영향이 없지만, 공통 API로 보이는 미사용 helper와 test가 계약과 반대인 규칙을 문서화해 다음 pagination 구현의 회귀 원인이 된다.
**권장 조치**
사용처가 없으므로 새 abstraction을 만들지 말고 `PageParams`, helper와 잘못된 동작 test를 제거한다. 도메인 adapter의 계약별 normalization과 실제 소비 중인 `PageData` type은 유지한다.
**판정 기록**
- 2026-07-30 — PRD·OpenAPI, helper 동작과 production 사용처 검색을 대조해 확정.
- 2026-07-30 — `P1-R8`에서 미사용 `PageParams`/`createPageParams`와 잘못된 clamp test를 삭제하고, `PageData` 및 도메인별 pagination contract 단위 회귀로 수정 완료를 확인했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P1-014``plan-task.md` 신규 `P1-R8`
- goal objective: `[P1-R8] 사용되지 않는 공통 pagination helper의 잘못된 전역 clamp를 제거한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 코드·계약·test 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P1-R8` 추가 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료.
**남은 항목:** 없음. 기존 인증·공통 UI 회귀는 유지됐고 공통 browser Gate는 `P0-R2`에서 이미 복구됐다.
## 9. 수정 후 검증 기록
2026-07-30 `P1-R8`에서 애플리케이션 helper를 수정했다.
- RED 대체: `rg -n 'createPageParams|PageParams|PageData|normalizeSize' src``createPageParams` production 사용처 0건과 test-only 사용을 확인했고, 기존 `npm run test:run -- src/shared/api/__tests__/pagination.test.ts`는 1 file / 5 tests passed로 stale clamp test가 통과했다.
- GREEN/회귀: 미사용 `PageParams`/`createPageParams`와 해당 test를 삭제했다. `rg -n 'createPageParams|type PageParams|import .*PageParams' src` — no matches. `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks` — 35 files / 201 tests passed. `npm run typecheck`, `npm run lint` — exit 0.
## 10. 2026-07-31 재점검
### 실행·판정 요약
- **기준:** commit `dd30e36323543e8f60e9983326503653e8001f12`, 재점검 시작 시 working tree 변경 220개.
- **검증:** `npm run test:run` 72 files / 360 tests passed, `npm run typecheck`, `npm run lint`, `npm run build` exit 0, `npm run e2e` 36 passed.
- **신규 발견:** 1건. 기존 `REV-P1-014`는 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-015` | Medium | 수정 완료 | 여러 pagination 인스턴스가 같은 select ID를 사용한다 | `P1-R9` | `P1-R9` |
### REV-P1-015 — 여러 pagination 인스턴스가 같은 select ID를 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.7 visible label·control 연결, keyboard 접근성
- **관련 계약:** 없음. 공통 UI DOM 식별자 경계
- **소유 Task:** 신규 `P1-R9`
**관찰 내용**
수정 전 `ResourcePagination`은 모든 인스턴스에 `id="resource-page-size"`와 같은 `htmlFor`를 고정했다. Comments는 root pagination과 하나 이상의 reply pagination을 동시에 렌더링할 수 있고 Community 목록 pagination도 열린 Sheet 뒤 DOM에 남으므로 한 문서에 같은 ID가 여러 개 생길 수 있었다.
**근거**
- 코드: `src/shared/ui/resource-pagination.tsx:15-18`
- 동시 소비: `src/features/comments/components/CommentThread.tsx:140,168`, `src/features/community-posts/pages/CommunityPostListPage.tsx:100`
- 테스트 공백: `src/shared/ui/__tests__/resource-pagination.test.tsx`는 단일 인스턴스만 렌더링한다.
**재현 또는 검증 절차**
1. `ResourcePagination` 두 개를 같은 container에 렌더링한다.
2. `document.querySelectorAll('#resource-page-size')`가 2개인지 확인한다.
3.`페이지 크기` label의 `control`이 각 인스턴스가 아니라 첫 번째 동일 ID 해석에 의존하는지 확인한다.
**영향**
DOM ID 유일성이 깨지고 label 클릭·보조기기 탐색이 다른 pagination select를 가리킬 수 있다. root와 reply page를 함께 관리할 때 사용자가 잘못된 목록의 크기를 바꿀 위험이 있다.
**권장 수정 방향**
공개 prop를 추가하지 않고 React `useId` 등으로 인스턴스별 select ID를 만들며, 두 인스턴스 label/control 연결 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — 공통 컴포넌트와 실제 다중 소비 구조를 정적 대조해 확정.
- 2026-07-31 — `P1-R9`에서 `ResourcePagination` 내부 ID를 React `useId`로 격리하고 multi-instance label/control 회귀 test와 Comments focused unit으로 수정 완료를 확인했다.
### plan·goal 전환 및 종료 판정
- `REV-P1-015``plan-task.md` 신규 `P1-R9`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **남은 위험:** 개발 중 E2E 반복 실행은 사용자 지시에 따라 생략했으며, Comments mock E2E는 최종 회귀 단계에서 필요 시 실행한다.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** 인증/session/logout, 보호 route, API 오류 mapping, 공통 file·crop·pagination·modal UI를 `P1`과 PRD 공통 정책에 재대조했다.
- **실행 증거:** `src/app src/features/auth src/layouts src/styles` 10 files / 64 tests, `src/shared` 31 files / 117 tests 통과. `typecheck`·`lint`·`build`, server E2E 18 tests, mock Chromium E2E 57 tests도 통과했다.
- **판정:** `REV-P1-015`까지 수정 완료 상태가 유지되며 Phase 1 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 외부 인증 서버 credential 기반 수동 QA는 자동 검증 범위 밖이다.
- **신규 Task:** 없음.
## 12. 종합 재점검 — 2026-07-31
### `REV-P1-016` — crop 미리보기와 저장 결과가 서로 다른 좌표계를 사용함
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD `FILE-008`, Image crop UI 흐름 4, 수용 기준의 crop 미리보기·적용 일치 |
| 소유 Task | `P1-R10` |
| 코드 근거 | `src/shared/ui/image-crop-dialog.tsx:155`, `:166~178`, `:189~190`; `src/shared/lib/crop-image.ts:48~65` |
| test 근거 | `src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 raw offset 전달만 확인하고 표시 px→원본 px 변환과 실제 aspect frame을 검증하지 않는다. |
**재현 또는 검증 절차**
1. 4,000×3,000 image를 1:1 crop dialog에 연다.
2. 현재 preview는 `max-h-64`에 의해 약 341×256 CSS px로 축소되지만 1:1 crop frame 없이 원본 4:3 전체를 표시한다.
3. preview를 10 CSS px 이동한다. 화면상 같은 이동은 원본 약 117px에 해당한다.
4. `calculateCropSourceRect``offsetX=10`을 원본 10px로 직접 차감해 저장 영역이 preview와 다르게 이동하는 것을 확인한다.
**영향과 권장 조치**
운영자가 선택한 영역과 업로드되는 실제 image가 달라질 수 있고, Character·Audio의 1:1 및 Series 210:297에서는 frame 밖 영역까지 preview에 보인다. 표시 frame 크기와 image scale을 source 좌표로 환산하는 단일 계산을 사용하고, 고정/free aspect의 frame·canvas 결과를 pixel 회귀 test로 고정한다.
**판정 기록**
- 2026-07-31 — PRD의 “현재 crop 영역·결과 미리보기”와 CSS transform·canvas source rectangle 계산을 대조해 확정.
- 2026-07-31 — 공통 UI 소유 신규 `P1-R10`으로 전환. Character·Audio·Series·Community에는 중복 Task를 만들지 않는다.
- 2026-07-31 — `P1-R10`에서 preview frame 크기 기반 source offset 환산을 추가하고 shared·도메인 단위 회귀로 수정 완료 판정.
### `REV-P1-017` — crop 적용 재진입·오류 복구와 Blob URL 해제가 없음
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD §10.5 저장 중·오류/재시도, §13 일반 mutation 중복 차단; 공통 crop lifecycle |
| 소유 Task | `P1-R11` |
| 코드 근거 | `src/shared/ui/image-crop-dialog.tsx:166~180`, `:203~206`; Character create/edit, Audio, Series, Community crop source의 `URL.createObjectURL`/error-only revoke |
| test 근거 | shared crop dialog와 도메인 crop test에 renderer reject·적용 연타·성공/취소/교체/unmount의 `revokeObjectURL` assertion이 없다. |
**재현 또는 검증 절차**
1. `renderCrop`이 pending인 상태에서 적용을 연속 클릭하면 현재 `applyCrop`이 호출마다 새 Promise를 시작하는지 확인한다.
2. renderer를 reject하면 catch와 visible error가 없어 dialog에서 복구 안내를 제공하지 못하는지 확인한다.
3. 각 crop source helper에서 image load 성공 후 적용·취소·새 선택·unmount를 반복한다.
4. 성공 경로에서 `URL.revokeObjectURL` 호출이 없는 것을 확인한다.
**영향과 권장 조치**
중복 canvas 작업과 예외의 unhandled rejection이 발생할 수 있고, 최대 10MB image를 반복 선택하는 관리자 세션에서 Blob URL이 문서 수명까지 유지된다. 적용을 single-flight로 만들고 pending/error를 표시하며, 생성 주체가 idempotent release contract로 URL을 수명 종료 시 해제하도록 한다.
**판정 기록**
- 2026-07-31 — 공통 dialog와 다섯 소비 경로의 비동기·resource ownership을 정적으로 대조해 확정.
- 2026-07-31 — `P1-R10` 이후 실행할 신규 `P1-R11`로 전환.
- 2026-07-31 — `P1-R11`에서 공통 crop source `release` contract, dialog single-flight/error 상태, 다섯 소비 경로의 cleanup을 추가하고 focused shared·도메인 단위 및 정적 Gate로 수정 완료를 확인했다.
### Phase 1 결론
- **자동 검증:** app/auth/shared/layout/style 묶음 41 files / 181 tests passed, 전체 `typecheck`·`lint`·개발/운영 build exit 0.
- **판정:** 인증·API client·기존 shared UI 회귀는 통과했고 `REV-P1-016~017``P1-R10~R11`에서 수정 완료됐다.
- **남은 위험:** 실제 인증 server credential 검증은 외부 수동 QA에서 추적한다. Safari/WebKit은 현재 지원 범위에서 제외한다.
## 13. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** 인증/session/logout, 보호 route, API client, shared file·crop·pagination·modal UI와 관련 unit/server E2E.
- **검증:** 전체 394 unit 중 Phase 1 app/auth/shared 기능 test는 통과했고, `typecheck`·`lint`·개발/운영 build와 4-project server E2E 36 tests도 통과했다. 실패 2건은 `src/shared/mocks`의 script/document contract로 한정됐다.
- **판정:** `P1-R10~R11`을 포함한 Phase 1 기능 소유의 신규 결함은 없다. 실제 인증 server credential 수동 QA는 계속 별도다.
- **문서 교차 항목:** 상단 리뷰 상태와 최신 수정 완료 결론의 불일치는 `REV-P10-008`/`P10-R7`이 소유한다.
- **신규 Phase 1 Task:** 없음.
## 14. 최종 재검증 및 신규 판정 — 2026-07-31
### 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-018` | Medium | 수정 완료 | fetch와 XHR의 독립 401 latch가 같은 session 만료 전환을 두 번 실행한다 | `P1-R12` | `P1-R12` |
### REV-P1-018 — fetch와 XHR의 독립 401 latch가 같은 session 만료 전환을 두 번 실행한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-005`, PRD §13 “여러 요청이 동시에 실패해도 로그인 이동과 알림을 한 번만 수행”
- **관련 계약:** HTTP 401 공통 처리. OpenAPI endpoint·DTO 변경 없음.
- **소유 Task:** 신규 `P1-R12`
**관찰 내용**
공통 fetch client와 Audio XHR upload는 각각 별도 `hasHandledAuthenticationExpiry`를 갖는다. 같은 session에서 두 transport가 동시에 401을 받으면 각 latch가 상대 transport의 처리를 보지 못해 `clearSession``onAuthExpired`를 각각 실행한다.
**근거**
- 코드: `src/shared/api/client.ts:55-57,99-103``src/features/audio-contents/api/upload-audio-content.ts:24-25,101-105`가 서로 독립된 count/latch를 사용한다.
- 조립: `src/app/App.tsx:195-201`의 fetch 만료 callback과 `src/app/protected-admin-shell.tsx:45-49`의 upload 만료 callback은 각각 `replaceWith``navigateTo`를 호출한다.
- 테스트 누락: `client-auth.test.ts``audio-upload.test.ts`는 transport 내부 burst만 검증하고 fetch+XHR 교차 burst는 검증하지 않는다.
- 문서: PRD `AUTH-005`, §13과 기존 `P1-R6`, `P4-R2`는 인증 제거·login 이동의 단일 실행을 요구한다.
**재현 또는 검증 절차**
1. 같은 mutable token과 `clearSession`/`onAuthExpired` spy를 `createApiClient``uploadAudioContent`에 주입한다.
2. 두 요청을 동시에 시작하고 fetch response와 XHR response를 모두 401로 종료한다.
3. 임시 진단 test를 `npm run test:run -- src/features/audio-contents/tests/auth-expiry-coordination-repro.test.ts`로 실행했다.
4. 실제 결과는 exit 1, 1 failed였고 `clearSession` 기대 1회 대비 실제 2회였다. 진단 파일은 판정 후 제거해 제품 test 변경을 남기지 않았다.
**영향**
제한된 동시 만료 조건에서 session 제거·login history 변경·만료 안내가 중복될 수 있다. 데이터 손실이나 인증 우회는 확인되지 않았지만 PRD의 복구 흐름과 browser history 일관성을 위반한다.
**권장 조치**
새 전역 event bus를 만들지 않고 두 transport가 callback 직전 같은 현재 session 존재 여부를 확인하게 한다. 첫 handler가 session을 동기 제거하면 나머지는 만료 전환을 건너뛰고, 새 로그인 token에서는 다음 401을 다시 처리하는 교차 transport 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — 정적 latch 대조 후 교차 transport 진단 test에서 callback 2회를 재현해 Medium 확정.
- 2026-07-31 — `plan-task.md` 신규 `P1-R12`로 전환. 애플리케이션 코드는 수정하지 않음.
- 2026-07-31 — `P1-R12`에서 callback 직전 현재 token guard를 추가해 수정 완료. reviewer blocker로 fetch-first 순서와 주입 auth authoritative token 처리를 보강했다. focused 3 files / 22 tests, auth/app/shared 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, 개발/운영 build, LSP diagnostics가 통과했다.
### 종료 판정
- **자동 검증:** `P1-R12` focused 3 files / 22 tests, `src/app src/features/auth src/shared/api` 포함 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check`, 변경 파일 LSP diagnostics가 통과했다. E2E는 사용자 지시에 따라 반복 실행하지 않았다.
- **최종 결론:** `REV-P1-018` 수정 완료. Blocker/High 없음.
- **남은 항목:** 실제 인증 server credential과 실기기 browser 수동 QA.
## 15. 2026-07-31 문서 기준 재리뷰
### 검토 범위와 제외
- **검토:** PRD `AUTH-*`, `FILE-*`, OpenAPI 2.3.0, `P1`·`P10` 후속 기록, `App` 보호 route probe, browser location parser, 공통 crop UI/source 계산과 관련 test를 current working tree에서 대조했다.
- **제외:** 실제 개발 API credential·stale role fixture가 필요한 server 수동 QA와 실제 image pixel 수동 비교는 자동 검증 범위에서 제외했다.
### 확정 발견 사항
#### `REV-P1-019` 고정 aspect crop frame과 저장 source rectangle 불일치
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `FILE-005~008`, `FILE-010`, `FILE-012` |
| 소유 Task | `P1-R13` |
**근거**
- `src/shared/ui/image-crop-dialog.tsx:176-186`은 crop viewport가 아니라 CSS transform이 적용된 `<img>``getBoundingClientRect()``previewFrameWidth/Height`로 보낸다.
- 같은 파일 `:210-211`의 preview는 `overflow-hidden` container 안에 원본 aspect `<img>`만 렌더하며, 1:1 또는 `210:297` crop 경계를 보여 주는 고정 aspect frame이 없다.
- `image-crop-dialog.test.tsx:14-18,56-66``HTMLImageElement.getBoundingClientRect()`를 인위적인 256×256로 만들어 request field만 검증한다. 실제 DOM frame aspect과 원본 4:3 image가 1:1 frame에 cover되는 동작은 검증하지 않는다.
- `crop-image.ts:52-57`은 전달된 rect에 다시 `zoom`을 나눈다. transformed image rect의 width/height에 이미 zoom이 반영되므로 offset scale이 zoom을 중복 반영할 수 있다.
**재현 및 영향**
1. 4,000×3,000 원본에 Character/Audio 1:1 policy를 적용하면 preview는 4:3 image 자체를 보이지만 `calculateCropSourceRect()`는 중앙 3,000×3,000을 저장 대상으로 선택한다.
2. 이동·확대 후 운영자가 확인한 구도와 실제 upload File의 pixel 영역이 달라질 수 있어 고정 aspect media 등록 결과를 신뢰할 수 없다.
**권장 조치·판정 기록**
- 실제 crop viewport를 고정 aspect로 렌더하고 비변환 viewport rect, image cover scale, offset/zoom을 한 좌표계에서 계산한다. 새 library는 필요하지 않다.
- 2026-07-31 — 시각 frame DOM과 source 계산 data flow를 대조해 High 확정. 완료된 `P1-R10`을 열지 않고 신규 `P1-R13`으로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R13`에서 고정 aspect viewport를 렌더하고 해당 viewport rect를 source 계산에 전달하도록 수정했다. reviewer blocker였던 image cover geometry와 zero-overhang offset clamp를 보완한 뒤 shared crop focused 2 files / 20 tests, Character·Audio·Series·Community 단위 회귀 30 files / 183 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer delta review `APPROVED`로 수정 완료 판정했다. E2E와 수동 pixel 비교는 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다.
#### `REV-P1-020` Character 생성·수정 route가 보호 인가 probe를 건너뜀
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005`, `AUTH-006`, PRD §13 401/403 복구 |
| 소유 Task | `P1-R14` |
**근거**
- `src/app/App.tsx:86-91,163-170``isAiCharactersRoute(location.path)`가 true인 경우에만 관리자 권한 probe를 실행하고 결과 전까지 protected shell을 숨긴다.
- `src/app/browser-location.ts:264-266``isAiCharactersRoute()``routePaths.aiCharacterCreate``getCharacterEditIdFromPath()`를 포함하지 않는다. 따라서 `/ai-characters/new``/ai-characters/:id/edit`는 probe 없이 바로 shell을 렌더한다.
- `App.protected-errors.test.tsx`의 401/403 test는 `/ai-characters`만 검증하고, `App.test.tsx`의 create/edit route test는 403 없이 form 렌더만 검증한다.
**재현 및 영향**
1. ADMIN session이 로컬에 남아 있지만 server가 403을 반환하는 상태에서 두 URL을 직접 열면 공통 접근 거부 흐름을 건너뛴 수 있다.
2. create는 초기 하위 request 없이 form을 노출하고, edit은 detail 403을 공통 access-denied 전환이 아닌 화면 단위 오류로 보일 수 있어 `AUTH-006`의 권한 작업 미실행 보장을 깨뜨린다.
**권장 조치·판정 기록**
- 두 route를 기존 `isAiCharactersRoute` 보호 범위에 포함하고 create/edit 직접 URL의 401/403·probe pending 회귀 test를 추가한다.
- 2026-07-31 — route matcher→App probe→child render 경로를 정적 추적해 High 확정, 신규 `P1-R14`로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R14`에서 `isAiCharactersRoute``/ai-characters/new``/ai-characters/:id/edit`를 포함하게 수정해 기존 보호 route probe를 재사용했다. RED 5 failures 재현 후 focused 3 files / 23 tests, app/auth/api 회귀 13 files / 84 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer gate `APPROVED`로 수정 완료 판정했다. stale ADMIN 개발 API 수동 확인은 외부 credential 범위로 남는다.
#### `REV-P1-021` malformed percent-encoding route가 `URIError`로 SPA를 중단함
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD §10.6 오류 복구, 공통 route 경계 |
| 소유 Task | `P1-R15` |
**근거·재현**
- `browser-location.ts:121-261`의 모든 param parser는 regex에 일치한 segment를 `decodeURIComponent()`로 바로 decode하며 예외을 처리하지 않는다. regex `[^/]+``%`를 허용한다.
- `node -e "decodeURIComponent('%')"` 실행 결과는 exit 1, `URIError: URI malformed`였다. 동일 함수가 App render 중 route 판정에서 호출되므로 `/ai-characters/%`와 같은 직접 URL은 공통 오류 화면을 거치지 않고 render를 중단할 수 있다.
**영향·권장 조치·판정 기록**
- 외부 link·수동 URL에서 화면 복구가 불가능하지만 정상 URL·인증 우회는 확인되지 않아 Medium으로 판정했다.
- decode 실패를 `null`로 끝내는 작은 helper와 `/ai-characters` 안전 fallback, 정상 한글/ASCII ID 회귀 test를 추가한다.
- 2026-07-31 — 명령 재현과 route data flow를 대조해 확정, 신규 `P1-R15`로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R15`에서 모든 route param decode를 `decodeRouteSegment` helper로 통일해 malformed percent-encoding을 `null`로 처리하고 character list fallback을 렌더하게 했다. RED 3 failures 재현 후 focused 2 files / 12 tests, app 회귀 5 files / 37 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer gate `APPROVED`로 수정 완료 판정했다.
### 재리뷰 검증·종료 판정
- **자동 증거:** `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`는 exit 0이고 server E2E는 4 projects / 36 passed였다. build는 503.04kB chunk warning만 표시했다. 전체 unit은 79 files 중 5 failed / 74 passed, 397 tests 중 5 failed / 392 passed였고 5개 관련 spec focused는 5 files / 25 tests passed였다. 전체 unit 비결정성은 `REV-P9-009`/`P9-R9`로 분리했다.
- **판정:** High 2건, Medium 1건을 확정해 `P1-R13~R15`로 전환했고, `REV-P1-019~021`은 수정 완료됐다.
- **남은 위험:** 실제 image frame→upload pixel 수동 비교, 실제 개발 API stale ADMIN 403, 실기기 browser QA는 신규 Task·기존 수동 QA 범위로 남는다.
## 16. 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- `P1-R13~R15`의 crop viewport/source 계산, Character create/edit 보호 probe, malformed route parser와 관련 test를 current staged working tree에서 다시 대조했다.
- `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — exit 0, 5 files / 46 tests passed.
- 전체 `npm run test:run` — 두 차례 연속 각각 81 files / 409 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --cached --check`도 exit 0이었다.
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium` — exit 0, 31 tests passed. 지원 범위 밖 WebKit·Mobile Safari는 실행하지 않았다.
- 실제 crop pixel 수동 비교와 실제 개발 API stale ADMIN credential QA는 실행하지 않았다.
### `REV-P1-022` — malformed route fallback이 보호 route probe를 우회함
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005~006`, `P1-R15` canonical fallback interface |
| 소유 Task | 신규 `P1-R16` |
**근거·재현**
- `src/app/browser-location.ts:42~53`은 route regex 중 하나가 맞으면 decode 성공 여부를 확인하지 않고 raw pathname을 snapshot으로 반환한다.
- malformed `/ai-characters/%``isCharacterRoutePath()`에는 맞지만 `getCharacterIdFromPath()``decodeRouteSegment()``null`을 반환한다. 따라서 `isAiCharactersRoute()`는 false가 된다(`browser-location.ts:65~67,121~149,300~301`).
- `App.tsx:86~128,163~168`의 ADMIN 보호 probe와 pending shell 차단은 `isAiCharactersRoute()`가 true일 때만 동작한다.
- `ProtectedAdminShell`은 모든 parser가 `null`이면 Character 목록을 fallback으로 렌더한다(`protected-admin-shell.tsx:199~202`). 기존 malformed test는 목록 heading만 확인하고 401/403 probe와 shell 선노출을 검증하지 않는다(`App.test.tsx:110~118`).
**영향·권장 조치**
stale ADMIN session에서 malformed 직접 URL이 공통 403 access-denied 전환과 probe pending 경계를 건너뛰고 protected shell/list를 먼저 렌더할 수 있다. backend 권한 검사를 우회해 데이터를 성공 조회하는 증거는 없지만 `P1-R14`와 같은 fail-closed 요구사항을 위반하므로 High로 판정한다. route 후보의 필수 segment decode가 실패하면 `readRoutePath()` 단계에서 `routePaths.aiCharacters` snapshot으로 귀결시키고 malformed 401/403 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — route snapshot → `isAiCharactersRoute` → App probe → shell fallback을 정적 추적하고 기존 test 누락을 대조해 High 확정.
- 2026-07-31 — 완료된 `P1-R15`를 다시 열지 않고 신규 `P1-R16`으로 전환. 애플리케이션 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R16`에서 malformed route를 canonical Character 목록 snapshot으로 정규화해 기존 ADMIN 보호 probe를 재사용하도록 수정 완료.
### 종료 판정
- `REV-P1-019~020` 수정은 focused·full·정적/build 검증에서 유지됐다. `REV-P1-021`은 URIError 중단은 막았지만 fail-closed fallback이 불완전해 신규 `REV-P1-022`로 분리했고, `P1-R16`에서 수정 완료했다.
- **최종 결론:** `REV-P1-022` 수정 완료. 실제 crop pixel·stale ADMIN server·실기기 QA는 별도 수동 범위다.
## 17. P1-R16 수정 후 검증 기록 — 2026-07-31
- RED: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 failed / 7 passed. malformed `/ai-characters/%` route에서 보호 probe pending 대신 `AI 캐릭터 목록을 불러오는 중`이 보여 shell/list fallback 선노출을 재현했다.
- GREEN/REFACTOR: `src/app/browser-location.ts`의 route 후보 반환 전 `isAiCharactersRoute()` 검사를 추가해 decode 실패 route를 `/ai-characters` snapshot으로 정규화했다. 별도 router dependency나 auth abstraction은 만들지 않았다.
- Focused 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/browser-location.test.ts` — 2 files / 9 tests passed. 개발 중 E2E 반복 실행은 사용자 지시에 따라 생략했다.
- 통합 검증: `npm run test:run` — 81 files / 411 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
## 18. P1-R16 수정 결과 재점검 — 2026-07-31
### `REV-P1-023` — malformed route 인가 matrix와 focused 명령이 불완전함
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005~006`, `REV-P1-022`, `P1-R16` 완료 증거·Phase 1 Gate |
| 소유 Task | 신규 `P1-R17` |
**근거·영향**
- `App.protected-shell.test.tsx`의 신규 회귀는 `/ai-characters/%`와 403 한 조합만 확인한다. 기존 `App.test.tsx`의 Audio/Series 복합 malformed 경로는 목록 heading만 확인해 보호 probe 우회가 재발해도 통과할 수 있다.
- malformed 401의 login 전환·session 제거를 직접 고정한 test가 없고 `P1-R16` 실행 명령은 신규 regression spec인 `App.protected-shell.test.tsx`를 포함하지 않는다.
- 현재 root-cause guard는 focused 42 tests와 full 411 tests에서 동작하지만 fail-closed 인가 경계의 완료 증거가 Task가 요구한 단일·복합 401/403보다 좁으므로 Medium test/Gate 회귀로 판정한다.
**권장 조치·판정 기록**
- 단일·복합 malformed path × 401/403을 data-driven test로 묶어 pending shell 미노출과 최종 auth 전환을 확인하고 focused 명령에 실제 spec을 포함한다.
- 2026-07-31 — 제품 코드의 현재 동작과 test/Task 완료 증거를 분리해 확정. 완료된 `P1-R16`을 다시 열지 않고 신규 `P1-R17`로 전환했다.
### 종료 판정
- 제품 route guard의 신규 runtime 실패는 확인되지 않았고 `P1-R17`에서 누락된 malformed route 인가 matrix와 focused Gate 증거를 보강했다.
- 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 file / 14 tests passed. `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts` — 4 files / 40 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 94 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
- **최종 결론:** `REV-P1-023`/`P1-R17` 수정 완료. 실제 stale ADMIN 개발 API 수동 QA는 별도다.
## 19. P1-R17 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- `App.protected-shell.test.tsx`, `App.protected-errors.test.tsx`, `App.test.tsx`, `browser-location.test.ts``P1-R17` 완료 기록을 대조했다.
- focused 4 files / 40 tests와 Phase 1을 포함한 전체 81 files / 417 tests가 통과했다. `typecheck`, `lint`, 개발/운영 build도 exit 0이었다.
- malformed 단일·Audio 복합·Series 복합 route × 401/403에서 probe pending 중 shell·logout 미노출, 401 session 제거·login 이동, 403 session 유지·access-denied 이동을 직접 확인했다.
### 발견 사항과 종료 판정
- 확정 발견 사항 없음. `P1-R16`의 canonical fallback 제품 코드와 `P1-R17`의 인가 회귀 matrix·focused Gate가 일치한다.
- 실제 crop pixel 비교와 stale ADMIN 개발 API 확인은 기존 수동 QA 대기로 유지하며 자동 test 통과로 대체하지 않는다.
- **최종 결론:** Phase 1 수정 검증 완료. 신규 회귀 Task 전환 없음.

View File

@@ -0,0 +1,852 @@
# Phase 10 OpenAPI Follow-up 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 10 / OpenAPI 2.3.0 후속 구현, 전체 통합 Gate와 현재 상태 문서 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-08-01 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md`, 이전 Phase 10 리뷰 |
| 리뷰 상태 | `REV-P10-018`/`P10-R17` 수정 완료, 실제 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- OpenAPI 2.3.0의 implemented operation, 후속 도메인 변경, 전체 자동 Gate와 수동 QA 상태를 종합 점검한다.
- 계획 상단·상태 절·과거 리뷰의 현재형 결론이 최신 완료 이력과 이번 신규 발견을 정확히 반영하는지 확인한다.
### 포함 범위
- 코드·테스트: 전 도메인 통합 경계와 전체 자동 Gate
- 계약: OpenAPI paths/operations/status/schema refs
- 문서: `P10-*`, plan 현재 상태·검증 기록, `review-phase-10-20260729.md`
- 수동 검증: 문서 상태 문장과 실제 task/Gate 기록 대조
### 제외 범위
- 실제 개발 서버 credential과 고정 fixture가 필요한 Series/FanTalk/Comments 수동 QA
- 이번 리뷰에서 발견한 코드 문제의 구현
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 계약 누락·통합 회귀와 현재 상태 문서의 의사결정 오류를 확인한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: PRD 전체와 §14 성공 기준
- 계약: `api-contract.openapi.json` 2.3.0 전체
- 계획: `P10-T1`~`P10-GATE`, 후속 회귀 Task, 상단 상태와 §6
- 이전 리뷰: `reviews/review-phase-10-20260729.md:134-158`
- 현재 신규 발견: `REV-P0-004`, `REV-P3-004/005`, `REV-P4-007`, `REV-P5-004`, `REV-P6-002`, `REV-P7-001`, `REV-P8-003`, `REV-P9-003`
### 실행 환경
```text
macOS 26.0 (Build 25A354)
Node v24.12.0 / npm 11.7.0
Playwright Chromium, Mobile Chrome
OpenAPI: 3.1.0 / info.version 2.3.0
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` / `npm run lint` | 성공 | 모두 exit 0 |
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run build` | 성공 | exit 0, 253 modules transformed |
| `npm run e2e:mock` | 실패 | 201 passed, 22 skipped, 5 failed; origin 4건과 비재현 timeout 1건 |
| `npm run e2e:mock -- tests/e2e/series.spec.ts --project=webkit --grep "desktop and tablet Series management flow remains available at 768px"` | 성공 | 1 passed |
| `npm run e2e` | 실패 | 12 passed, 24 failed; 현재 API origin과 과거 route 불일치 |
| `jq` operation/status 집계 및 component schema `$ref` 차집합 검증 | 성공 | 두 명령 모두 exit 0; 25 paths, 37 operations, 전부 `implemented`, 누락 component schema ref 없음 |
| 실제 서버 수동 QA | 불가 | ADMIN credential과 제어 가능한 fixture가 제공되지 않음 |
| plan·이전 리뷰 상태 대조 | 성공 | plan 상단·§6·이전 리뷰가 자동 Gate 완료와 수동 QA 대기로 일치 |
| FanTalk PUT schema 실행 대조 | 실패 | OpenAPI `FanTalkListItem` data는 현재 schema가 거부하고 POST 응답 shape만 허용 |
| FanTalk POST→목록 reply ID 대조 | 실패 | mock `creatorReplies[].fanTalkId`에 POST `replyId`가 아니라 root `fanTalkId` 저장 |
| 현재형 문서·dead scaffold 검색 | 실패 | README가 implemented operation을 대기/구현 대상으로 설명하고 미사용 Phase 3 placeholder export가 남음 |
OpenAPI 검증은 다음 명령으로 실행했다.
```bash
jq '[.paths[] | to_entries[] | select(.key | IN("get", "put", "post", "delete", "patch", "head", "options", "trace")) | .value] as $operations | {openapi, version: .info.version, paths: (.paths | length), operations: ($operations | length), statuses: ([$operations[]."x-implementation-status"] | unique)}' docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
jq -e '((([.. | objects | .["$ref"]? // empty | select(startswith("#/components/schemas/")) | split("/")[-1]] | unique) - (.components.schemas | keys)) | length) == 0' docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
```
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P10-003` | Low | 수정 완료 | plan과 이전 Phase 10 리뷰의 현재 상태가 완료 이력과 신규 회귀 목록을 반영하지 않는다 | `P10-R3` | `P10-R3` |
| `REV-P10-004` | High | 수정 완료 | FanTalk PUT client가 OpenAPI 응답 대신 POST 응답 schema를 사용해 성공 응답 parsing에 실패한다 | `P10-R4` | `P10-R4` |
| `REV-P10-005` | Medium | 수정 완료 | FanTalk mock이 새 답변의 reply row ID 대신 root ID를 수정 path에 사용한다 | `P10-R4` | `P10-R4` |
| `REV-P10-006` | Low | 수정 완료 | 현재 문서와 dead scaffold가 완료된 Phase 3·10 기능을 아직 대기 상태로 설명한다 | `P10-R5` | `P10-R5` |
## 6. 발견 사항 상세
### REV-P10-003 — plan과 이전 Phase 10 리뷰의 현재 상태가 완료 이력과 신규 회귀 목록을 반영하지 않는다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 구현 계획·리뷰 가이드의 현재 상태 및 검증 기록 유지 규칙
- **관련 계약:** 없음
- **소유 Task:** 신규 `P10-R3`
**관찰 내용**
plan 상단 상태는 2026-07-29 신규 회귀 Task와 `P10-GATE`가 미완료라고 적는다. 중간 §6은 Series/FanTalk server E2E가 최종 완료를 막는다고 서술하지만, plan 후반에는 자동 Gate 완료와 수동 QA 대기가 기록돼 있다. 이전 Phase 10 리뷰도 이미 완료된 `P10-R1`·`P10-R2` 및 Gate를 대기 상태로 유지한다. 이번 리뷰에서 새로 확정한 Task 목록도 현재 상태에 아직 반영되지 않는다.
**근거**
- 계획: `plan-task.md:13`, §6의 server E2E 상태 문장, 후반 `P10-R1`·`P10-R2`·Gate 완료 기록
- 이전 리뷰: `reviews/review-phase-10-20260729.md:134-151`
- 이번 리뷰: Phase 0~10 신규 report와 새 회귀 Task
- 가이드: 완료 체크를 되돌리지 않고 현재 상태와 수정 후 검증을 누적해야 함
**재현 또는 검증 절차**
1. plan 상단 상태와 §6의 blocker 문장을 읽는다.
2. 문서 후반의 `P10-R1`, `P10-R2`, 자동 Gate 완료 기록을 읽는다.
3. 이전 Phase 10 리뷰의 최종 결론과 비교한다.
4. 동일 작업이 “미완료”, “자동 Gate 완료”, “수동 QA 대기”로 동시에 표현되는 것을 확인한다.
**영향**
다음 실행자가 현재 자동 Gate, 수동 QA, 신규 회귀 Task 중 무엇이 남았는지 잘못 판단할 수 있다. 완료된 Task를 재개하거나 아직 수정하지 않은 신규 결함을 완료로 오인할 위험이 있다.
**권장 조치**
기존 실행 이력은 보존하고 현재 상태 요약만 2026-07-30 기준으로 갱신한다. 과거 Phase 10 리뷰에는 완료된 항목의 `수정 완료`·검증 기록을 누적하고, 새 회귀 Task와 실제 서버 수동 QA 대기를 구분한다.
**판정 기록**
- 2026-07-30 — plan의 세 상태 위치, 이전 리뷰와 이번 신규 Task를 대조해 확정.
- 2026-07-30 — plan 상단·§6과 이전 Phase 10 리뷰 결론을 자동 Gate 완료·실제 개발 API 수동 QA 대기 기준으로 정렬해 완료.
### REV-P10-004 — FanTalk PUT client가 OpenAPI 응답 대신 POST 응답 schema를 사용해 성공 응답 parsing에 실패한다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `FANTALK-004`, `FANTALK-009`
- **관련 계약:** `PUT .../fan-talks/{fanTalkId}/replies/{replyId}``FanTalkReplyUpdateApiResponse`
- **소유 Task:** 신규 `P10-R4`
**관찰 내용**
OpenAPI PUT 성공 data는 레거시 `CreatorChannelFanTalkResponse`에 대응하는 `FanTalkListItem` shape다. 그러나 `updateFanTalkReply`는 POST 성공용 `fanTalkReplyResponseSchema``FanTalkReplyResponse`를 재사용한다. 실제 계약 shape의 성공 응답을 받으면 strict schema parsing이 실패해 서버 mutation이 성공했어도 UI는 수정 실패로 처리한다. 현재 contract/UI test fixture도 PUT에 POST shape를 반환해 문제를 고정한다.
**근거**
- client: `src/features/fan-talks/api/fan-talk-api.ts:72-80`
- POST schema: `src/features/fan-talks/schemas/fan-talk-reply-schema.ts:9-19`
- 잘못된 fixture: `src/features/fan-talks/tests/fan-talk-contract.test.ts:58-60,106-117`, `fan-talk-reply.test.tsx:65-71,104-117`
- 계약: OpenAPI `FanTalkReplyUpdateApiResponse.data → FanTalkListItem`; data의 `fanTalkId`는 수정 reply row ID, `creatorReplies`는 빈 배열
- 실행 재현: OpenAPI PUT data shape parse `false`, POST data shape parse `true`
**재현 또는 검증 절차**
1. PUT handler가 `{fanTalkId,writerId,writerNickname,writerProfileImageUrl,content,createdAtUtc,creatorReplies:[]}`를 성공 data로 반환하게 한다.
2. 기존 `updateFanTalkReply`를 호출한다.
3. HTTP 200 이후 `fanTalkReplyResponseSchema``replyId`, `creatorMemberId` 누락과 extra fields 때문에 parsing 오류를 내는 것을 확인한다.
**영향**
실제 개발 API에서 답변 수정이 서버에는 반영되지만 관리자는 실패 안내를 보고 재시도할 수 있다. 이는 중복 PUT과 상태 혼동을 유발하며 `P10-T5`의 완료 증거를 무효화하는 계약 위반이다.
**권장 조치**
PUT 전용 response schema/type을 `FanTalkListItem` 기반으로 분리하고 contract/MSW/mock fixture를 OpenAPI shape로 바꾼다. UI가 응답을 직접 표시하지 않더라도 성공 data validation은 유지한다.
**판정 기록**
- 2026-07-30 — OpenAPI schema와 client strict parser를 실행 대조해 확정.
- 2026-07-30 — PUT 전용 response schema/type과 OpenAPI shape fixture를 추가하고 focused FanTalk unit·mock E2E로 수정 완료.
### REV-P10-005 — FanTalk mock이 새 답변의 reply row ID 대신 root ID를 수정 path에 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FANTALK-004`, `FANTALK-009`, `FANTALK-010`, `MOCK-005`
- **관련 계약:** POST `FanTalkReplyResponse.replyId`, 목록 `creatorReplies[].fanTalkId`, PUT path `replyId`
- **소유 Task:** 신규 `P10-R4`
**관찰 내용**
mock POST는 새 `replyId=9001`을 반환하지만 목록 갱신 시 `creatorReplies[].fanTalkId`에 root FanTalk ID를 저장한다. mock PUT도 그 root ID를 reply ID로 기대해 잘못된 mapping이 자체적으로 성공한다. 기존 E2E는 답변 생성 후 같은 item을 수정하지 않고 별도 pre-existing item만 수정해 이 차이를 찾지 못한다.
**근거**
- mock create mapping: `src/shared/mocks/fan-talk-mock-store.ts:38-49`
- mock PUT ID 비교/응답: `src/shared/mocks/fan-talk-mock-store.ts:54-73`
- UI fixture 동일 오류: `src/features/fan-talks/tests/fan-talk-reply.test.tsx:95-102`
- 문서: `prd.md:323,328-329`
- 현재 E2E: create flow와 pre-existing reply edit flow가 분리돼 POST→같은 item PUT path를 검증하지 않음
**재현 또는 검증 절차**
1. 미답변 root `fanTalkId=7001`에 POST하고 응답 `replyId=9001`을 받는다.
2. 목록을 다시 읽으면 mock의 `creatorReplies[0].fanTalkId`가 9001이 아니라 7001이다.
3. 같은 item을 수정하면 `/replies/7001`을 호출한다.
4. 요구 결과는 `/replies/9001` 호출이다.
**영향**
mock preview가 새 답변의 후속 수정 path를 실제 server와 다르게 검증해 integration 결함을 숨긴다. 새 답변 작성 직후 수정하는 운영 흐름이 실제 API에서 404 또는 다른 row 오조회를 일으킬 수 있다.
**권장 조치**
목록 creator reply에는 POST의 `replyId`를 저장하고 mock PUT lookup도 그 ID를 사용한다. create→재조회→같은 item edit E2E를 추가해 path를 고정한다.
**판정 기록**
- 2026-07-30 — PRD ID mapping과 mock create/update branch 및 E2E journey를 대조해 확정.
- 2026-07-30 — mock 목록 mapping이 POST `replyId`를 보존하도록 수정하고 POST→동일 item PUT path E2E로 수정 완료.
### REV-P10-006 — 현재 문서와 dead scaffold가 완료된 Phase 3·10 기능을 아직 대기 상태로 설명한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 구현 계획·리뷰 가이드의 현재 상태 유지 규칙
- **관련 계약:** OpenAPI 2.3.0 implemented operation 상태
- **소유 Task:** 신규 `P10-R5`
**관찰 내용**
README는 v2 원작·장르 lookup을 아직 대기라고 설명하고 FanTalk 수정·삭제, Comments CRUD, Community pagination을 “Phase 10 구현 대상”이라고 적는다. 이 기능들은 OpenAPI 2.3.0과 `P10-T1~T7`에서 implemented/완료다. 또한 사용처 없는 `AiCharactersPage` export가 “Phase 3에서 연결” placeholder UI를 보존한다.
**근거**
- 문서: `README.md:54-57`
- dead scaffold: `src/app/admin-pages.tsx:19-36`
- 사용처 검색: `AiCharactersPage`는 정의 외 참조 0건
- 계약·계획: OpenAPI 25 paths/37 operations 전부 implemented, `P10-T1~T7` 체크 완료
**재현 또는 검증 절차**
1. README Known Backend Constraints를 현재 OpenAPI·Phase 10 완료 기록과 비교한다.
2. `rg -n 'AiCharactersPage' src`로 placeholder가 routing되지 않는 dead export임을 확인한다.
3. 현재 인수자가 문서만 읽으면 이미 구현된 기능을 미구현으로 판단하게 되는 것을 확인한다.
**영향**
다음 작업자가 완료된 lookup·FanTalk·Comments 범위를 다시 계획하거나 실제 남은 수동 QA와 제품 제외 범위를 잘못 구분할 수 있다. dead placeholder는 오래된 문구가 재사용될 가능성을 남긴다.
**권장 조치**
README 현재 상태만 implemented 범위와 실제 개발 API 수동 QA 대기로 갱신하고, 사용처 없는 placeholder component는 삭제한다. 날짜별 과거 Decision/Progress는 보존한다.
**판정 기록**
- 2026-07-30 — README·dead export 검색과 OpenAPI/plan 완료 상태를 대조해 확정.
- 2026-07-30 — README 현재형 stale 문구를 갱신하고 미사용 `AiCharactersPage` export를 삭제해 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P10-003``plan-task.md` 신규 `P10-R3`
- goal objective: `[P10-R3] 계획과 이전 Phase 10 리뷰의 현재 상태를 완료 이력·신규 회귀 Task·수동 QA 대기 기준으로 동기화한다.`
- `REV-P10-004`, `REV-P10-005``plan-task.md` 신규 `P10-R4`
- goal objective: `[P10-R4] FanTalk PUT 응답 schema와 POST→목록 reply ID mapping을 실제 계약에 맞춘다.`
- `REV-P10-006``plan-task.md` 신규 `P10-R5`
- goal objective: `[P10-R5] 완료된 후속 계약의 현재 문서와 dead Phase scaffold를 정리한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 전 자동 Gate·OpenAPI·상태 문서 대조 |
| 후보 항목 판정 완료 | 충족 | 기존 1건과 신규 3건 모두 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P10-R3`~`P10-R5` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 충족 | 실제 server 수동 QA는 운영 담당자와 credential·fixture 준비 후 재개 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** `REV-P10-003`~`REV-P10-006` 수정 완료, 외부 수동 QA 대기
**남은 항목:** 실제 개발 서버 ADMIN credential·고정 fixture 확보 후 Series/FanTalk/파일 정책 수동 QA.
## 9. 수정 후 검증 기록
### P10-R3 수정 후 검증 — 2026-07-30
- 무엇을: plan 상단 상태, §6 구현 완료 정의, 과거 Phase 10 리뷰의 최종 결론과 수정 후 검증 기록을 최신 Gate 정책에 맞춰 갱신했다.
- 왜: 현재 자동 Gate 완료, 실제 개발 API 수동 QA 대기, 신규 회귀 Task 완료 상태가 서로 다른 위치에서 다르게 읽히는 문제를 닫기 위해서다.
- 검증: stale 현재 상태 검색과 `git diff --check -- docs/20260725_AI캐릭터관리자웹`를 통과했다.
### 2차 계약 점검 기록 — 2026-07-30
- 의존성·정적 Gate: `npm ci`, `npm run typecheck`, `npm run lint`, `npm run build`가 모두 exit 0이었고 build는 255 modules를 변환했다.
- 자동 Gate: `npm run test:run` 72 files / 358 tests, `npm run e2e` server allowlist 36 tests, `npm run e2e:mock -- --project=chromium` 57 tests가 통과했다.
- OpenAPI 정적 검증: 3.1.0 / document 2.3.0, 25 paths / 37 operations, status 전부 `implemented`, 누락 component schema ref 0건을 재확인했다.
- `fanTalkReplyResponseSchema.safeParse(OpenAPI PUT data)``false`, 동일 schema의 POST shape parse는 `true`로 재현했다.
- mock POST `replyId`와 refetch 목록 `creatorReplies[].fanTalkId` mapping을 정적 추적해 root ID 사용을 확인했다.
- README의 legacy/Phase 10 대기 문구와 사용처 없는 `AiCharactersPage`를 검색해 현재 상태 불일치를 확인했다.
- 애플리케이션 코드는 수정하지 않고 `P10-R4~R5`로 전환했다.
### P10-R4 수정 후 검증 — 2026-07-30
- 무엇을: FanTalk reply PUT success parser를 OpenAPI `FanTalkListItem` shape로 분리하고, mock POST 뒤 목록 reply ID mapping과 PUT 응답 shape를 실제 계약에 맞췄다.
- 왜: 실제 개발 API의 PUT 성공 응답을 POST response schema로 parsing해 실패 처리하거나, mock preview가 새 답변의 후속 수정 path를 root ID로 잘못 검증하는 문제를 닫기 위해서다.
- 검증: focused FanTalk contract/UI test, FanTalk mock Chromium E2E, FanTalk/shared mock 회귀, typecheck를 통과했다.
### P10-R5 수정 후 검증 — 2026-07-30
- 무엇을: README Known Backend Constraints의 현재형 stale 문구를 갱신하고 미사용 `AiCharactersPage` placeholder export를 삭제했다.
- 왜: 완료된 v2 lookup·Phase 10 구현 범위를 대기 상태로 오해하지 않고, 실제 남은 작업이 개발 API 수동 QA임을 구분하기 위해서다.
- 검증: `rg -n 'legacy 후보|Phase 10 구현 대상|Phase 3에서|AiCharactersPage' README.md src/app`는 no matches, `npm run test:run -- src/app/App.test.tsx src/app/App.protected-errors.test.tsx`는 2 files / 18 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `src/app/admin-pages.tsx` LSP diagnostics는 오류 0건, 대상 문서·파일 `git diff --check`는 no output이었다.
## 10. 2026-07-31 재점검
### 실행·계약 판정 요약
- OpenAPI 문서 version `2.3.0`, 25 paths / 37 implemented operations와 PRD 소비 표를 다시 대조했다.
- Character/Audio/Series/Community/FanTalk/Comments DTO·required·mutation `data` shape에서 신규 OpenAPI JSON 자체 결함은 확인되지 않았다.
- 전체 unit 72 files / 360 tests, typecheck·lint·build와 server allowlist 36 tests가 통과했다. 당시 전체 Mock matrix 비결정성 후보는 지원 project 축소로 현재 Task에서 제외했다.
- 신규 문서 상태 발견 1건을 `P10-R6`으로 전환했다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P10-007` | Low | 수정 완료 | PRD·plan·현재 리뷰가 완료된 구현을 미래 작업 또는 미해결 상태로 표시한다 | `P10-R6` | `P10-R6` |
### REV-P10-007 — PRD·plan·현재 리뷰가 완료된 구현을 미래 작업 또는 미해결 상태로 표시한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 문서 유지보수 가이드, PRD §11.5 현재 외부 의존 상태, plan 완료 증거 추적
- **관련 계약:** OpenAPI 2.3.0 implemented operation 상태
- **소유 Task:** 신규 `P10-R6`
**관찰 내용**
PRD Overview는 아직 “애플리케이션 코드를 수정하지 않는다”고 쓰고, 해결된 EXT 항목 여러 개는 완료된 Phase 10 기능을 “구현한다”는 미래형 영향으로 표시한다. plan §5.1도 v2 lookup·UTC·Community pagination·FanTalk PUT/DELETE 등 완료 항목을 미래 Task 표현으로 남긴다. 현재 Phase 1·5·8 리뷰 metadata는 수정 완료 상세와 달리 “신규 회귀 수정 대기”를 유지하고, Phase 8 §5의 `REV-P8-004`는 §6·§8·§9의 `수정 완료`와 다르게 `확정`으로 남아 있다.
**근거**
- `prd.md:35-42`, `prd.md:724-744`
- `plan-task.md` §5.1 P9 수용 기준 증거 요약
- `reviews/phase1-platform-auth-shared-ui.md:12`
- `reviews/phase5-series-management.md:12`
- `reviews/phase8-comments.md:12,71-76,122-125,178-181`
- 반대 완료 증거: `P10-T1~T7`, `P10-R3~R5` 체크와 각 수정 후 검증 기록
**재현 또는 검증 절차**
1. 위 현재형 문구를 읽고 각 항목의 실제 완료 Task와 비교한다.
2. Phase 8 요약 표의 `REV-P8-004` 상태를 같은 문서 상세·최종 결론과 비교한다.
3. 과거 날짜별 Decision/Progress가 아니라 현재 Overview·영향·요약에서 불일치가 발생하는지 구분한다.
**영향**
새 작업자가 완료 기능을 다시 계획하거나 실제 남은 개발 API 수동 QA와 코드 미구현을 혼동할 수 있다. 실행 자체에는 직접 영향이 없어 Low로 분류한다.
**권장 수정 방향**
과거 기록은 보존하고 현재 Overview, EXT 영향, plan 수용 기준, 현재 review metadata/summary만 최소 갱신한다. 2026-07-31 신규 회귀 Task와 실제 server QA 대기는 완료 기능과 분리해 표시한다.
**판정 기록:**
- 2026-07-31 — 현재형 섹션과 완료 이력을 교차 검색해 확정.
- 2026-07-31 — `P10-R6`에서 PRD Overview·EXT 영향, plan §3.1, Phase 1·8 리뷰 metadata와 Phase 8 요약 상태를 완료 상태로 정렬해 수정 완료.
### plan·goal 전환 및 종료 판정
- `REV-P10-007``plan-task.md` 신규 `P10-R6`
- **최종 결론:** 신규 Low 1건 수정 완료. OpenAPI JSON 변경 필요 없음.
- **남은 항목:** 기존 실제 개발 API 수동 QA.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** OpenAPI 2.3.0 전체 25 paths / 37 operations와 Character lookup, Audio UTC, Series genre·CRUD, Community pagination, FanTalk PUT/DELETE, Audio·Community Comments adapter/schema/mock 구현을 재대조했다.
- **실행 증거:** `jq -e` parse와 operation 집계 성공, Phase별 focused unit 전부 통과, server E2E 18 tests와 mock Chromium E2E 57 tests 통과, `typecheck`·`lint`·`build` exit 0.
- **판정:** OpenAPI JSON이나 Phase 10 endpoint·DTO에 확정 신규 발견 사항 없음. `REV-P4-010`은 PRD의 client file 처리 순서, `REV-P9-006`은 공통 UI mutation 상태 문제이므로 OpenAPI 변경 대상이 아니다.
- **남은 위험:** 기존 계획에 기록된 실제 개발 API 수동 QA는 여전히 필요하다.
- **신규 Phase 10 Task:** 없음.
## 13. 요청 기준 재리뷰 — 2026-07-31
### OpenAPI·구현 판정
- `jq` parse 결과 OpenAPI `3.1.0`, document version `2.3.0`, 25 paths / 37 operations이며 모든 operation은 `implemented`다.
- component schema `$ref` 누락은 0건이고 Character/Audio/Series/Community/FanTalk/Comments contract test는 전체 unit에서 통과했다.
- OpenAPI JSON·endpoint·DTO 소유의 신규 결함과 계약 변경 필요는 확인되지 않았다.
### `REV-P10-008` — 일부 Phase 리뷰의 현재 상태가 같은 문서의 수정 완료 이력과 다시 어긋남
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 문서 유지보수 가이드, review 종료 조건, `P10-R6`의 current metadata 동기화 계약
- **관련 계약:** OpenAPI 변경 없음
- **소유 Task:** 신규 `P10-R7`
**관찰 내용**
Phase 0·1·8 리뷰 상단은 각각 최신 `P0-R3`, `P1-R10~R11`, `P8-R4`가 수정 완료된 뒤에도 “신규 회귀 수정 필요”로 남아 있다. Phase 4는 공통 crop 회귀가 `P1-R10~R11`에서 완료됐지만 “교차 회귀 대기”를 유지한다. Phase 9도 `P9-R7` 완료 뒤 같은 상태였고, 이번에는 별도 신규 `P9-R8`이 생겼으므로 과거 완료와 현재 미완료를 구분해 다시 동기화해야 한다.
**근거**
- current metadata: `phase0-project-foundation.md:12`, `phase1-platform-auth-shared-ui.md:12`, `phase4-audio-content.md:12`, `phase8-comments.md:12`, `phase9-cross-cutting-quality.md:12`
- 같은 문서의 반대 완료 근거: 각 최신 `Phase 결론``P0-R3`, `P1-R10~R11`, `P4-R7`, `P8-R4`, `P9-R7` 수정 검증 기록
- 기존 정합성 Task: `plan-task.md`의 완료된 `P10-R6`
**재현 또는 검증 절차**
1.`reviews/phase*.md`의 1~16행에서 current `리뷰 상태`를 읽는다.
2. 같은 파일 마지막 40행의 최신 결론·수정 검증과 비교한다.
3. 과거 날짜별 기록이 아니라 current metadata가 최신 완료/미완료 상태와 모순되는 파일을 목록화한다.
4. 이번 신규 `P9-R8`과 실제 개발 API 수동 QA를 기존 완료 이력과 구분한다.
**영향**
후속 작업자가 이미 끝난 회귀를 다시 구현하거나, 새 `P9-R8`과 실제 server 수동 QA를 과거 미완료 항목으로 혼동할 수 있다. runtime 영향은 없어 Low다.
**권장 조치**
`P9-R8` 완료 뒤 과거 검증 기록은 보존하고 각 current metadata·최신 요약·plan 상단 상태만 한 번에 동기화한다. OpenAPI와 애플리케이션 코드는 변경하지 않는다.
**판정 기록**
- 2026-07-31 — Phase별 상단 상태와 최신 결론을 정적 대조해 확정.
- 2026-07-31 — `plan-task.md` 신규 `P10-R7`로 전환하고 `P9-R8` 이후 실행하도록 의존성을 기록.
- 2026-07-31 — `P10-R7`에서 Phase 0·1·4·8·9 metadata와 plan 상단 상태를 `P9-R8` 완료 및 실제 개발 API 수동 QA 대기 기준으로 재동기화해 수정 완료.
### 종료 판정
- **최종 결론:** OpenAPI·구현 소유 신규 결함 없음. `REV-P10-008``P10-R7`에서 수정 완료.
- **남은 항목:** 기존 실제 개발 API 수동 QA.
## 12. 종합 재점검 — 2026-07-31
- **검토 범위:** OpenAPI 3.1 문서 version 2.3.0의 25 paths / 37 operations, schema/reference, Character lookup·Audio UTC·Series CRUD·Community pagination·FanTalk PUT/DELETE·Comments adapter와 mock contract를 재대조했다.
- **실행 증거:** `jq -e` parse·version·operation 집계 성공, 도메인 묶음 36 files / 206 tests와 app/shared 묶음 41 files / 181 tests 통과, `typecheck`·`lint`·build·server E2E 18/18 통과.
- **판정:** OpenAPI JSON과 Phase 10 endpoint/DTO 소유의 확정 신규 finding 없음. 계약 변경은 필요하지 않다.
- **교차 Phase:** crop 문제는 client UI 계약이므로 `P1-R10~R11`, 댓글 초안은 UI mutation recovery이므로 `P8-R4`, browser matrix는 `P9-R7`이 소유한다.
- **남은 위험:** 실제 개발 API 수동 QA는 mock/client 검증과 분리해 계속 대기 상태다.
- **신규 Phase 10 Task:** 없음.
## 14. P10-R7 수정 후 검증 — 2026-07-31
- 무엇을: Phase 0·1·4·8·9 현재 리뷰 metadata와 plan 상단 상태를 최신 수정 완료 이력에 맞췄다.
- 왜: 같은 문서 하단에는 `P0-R3`, `P1-R10~R11`, `P4-R7`, `P8-R4`, `P9-R8` 수정 완료가 기록됐지만 상단 current status는 과거 미완료 문구를 유지해 후속 작업자가 남은 범위를 오해할 수 있었기 때문이다.
- RED 대체: Phase별 상단 1~16행과 tail 최신 결론을 대조해 `신규 회귀 수정 필요`, `교차 회귀 대기`, `P9-R8 수정 필요`, `P10-R7 수정 필요` 현재형 불일치 후보를 확인했다.
- GREEN: 과거 판정·실패·수정 기록은 보존하고 상단 `리뷰 상태`, Phase 9·10 최신 종료 판정, plan 상단 상태만 현재 상태로 정정했다.
- 검증: stale current status 검색 0건, review 상대 링크 형식 점검, `git diff --check -- docs/20260725_AI캐릭터관리자웹` exit 0.
## 15. 최종 재검증 — 2026-07-31
- **검토 범위:** OpenAPI 3.1.0 문서 version 2.3.0의 25 paths / 37 operations, 모든 local schema `$ref`, 구현 adapter·mock contract와 요구사항 추적.
- **실행 증거:** `jq` parse·version·operation 집계와 `$ref` 존재 검사가 통과했고 37 operation 모두 `x-implementation-status=implemented`였다. 전체 unit·server/mock E2E·정적/build Gate도 위 최종 수치로 통과했다.
- **판정:** OpenAPI endpoint·DTO·문서 소유의 확정 신규 발견 사항 없음. `REV-P1-018`, `REV-P4-011`은 client lifecycle 문제이므로 계약 변경 대상이 아니다.
- **남은 위험:** 실제 개발 API 수동 QA와 신규 `P1-R12`, `P4-R8` 완료 후 Gate 재검증.
- **신규 Phase 10 Task:** 없음.
## 16. 2026-07-31 문서 기준 재리뷰
### 검토 범위와 제외
- **검토:** OpenAPI 3.1.0 / document 2.3.0의 path·operation·schema ref, `P10-T1~T7`·`P10-R1~R7` 완료 이력, plan의 현재형 설명·필수 section·dependency 표시를 current working tree에서 대조했다.
- **제외:** 실제 개발 API credential·fixture가 필요한 Series/FanTalk/Comments/file policy 수동 QA는 기존 대기 범위로 유지했다.
### `REV-P10-009` 완료된 Phase 10 범위가 plan·리뷰에 미래형으로 남음
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | 문서 가이드 현재 상태·이력 분리, `P10-T1~T7` |
| 소유 Task | `P10-R8` |
**근거**
- `plan-task.md:161`의 Phase 7 지도는 이미 구현된 reply edit를 여전히 “계약 대기”로 쓴다.
- 같은 문서 `:173-175`의 Phase 4~6 현재 상태는 Audio UTC migration, Series CRUD 후속 구현, Community pagination migration을 미완료로 표시하지만 `P10-T2~T4`는 완료됐다.
- §5.1의 Audio·Community·FanTalk·File 행은 여전히 “`P10-T2/T4/T5/T7`에서” 구현한다고 미래형으로 쓴다.
- plan Tech Stack은 React Router, React Hook Form, Axios, date-fns, dnd-kit, Lucide React 등을 현재 stack으로 나열하지만 `package.json`에는 이 dependency들이 없고 현재 구현은 native history, fetch/XHR 등을 사용한다.
- 본 리뷰의 직전 §15는 남은 항목에 이미 완료된 `P1-R12`, `P4-R8`을 포함한다.
**영향·권장 조치·판정 기록**
- 실행자가 완료된 기능을 중복 계획하거나 미설치 dependency를 필수로 추가할 수 있지만 runtime 장애는 아니므로 Low로 판정했다.
- 과거 실행 이력은 보존하고 Phase 지도·§3.1·§5.1·Tech Stack·최신 리뷰 결론만 current working tree에 맞게 최소 정정한다.
- 2026-07-31 — 완료 Task·dependency·현재형 문구를 대조해 확정, 신규 `P10-R8`로 전환했다. 현재 문구는 이 리뷰에서 바로 고치지 않았다.
- 2026-07-31 — `P10-R8`에서 plan Tech Stack, Phase 7 지도, §3.1 Phase 4~6 현재 상태를 완료 범위와 설치 dependency에 맞춰 정정하고 수정 완료로 전환했다. 과거 실행 이력과 실제 개발 API 수동 QA 대기는 보존했다.
### `REV-P10-010` plan이 Goal 실행형 필수 section을 명시적으로 제공하지 않음
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 규칙 | `docs/agent-guide/goal-plan.md` §3 |
| 소유 Task | `P10-R9` |
**근거**
- `rg -n '^## ' plan-task.md`로 확인한 최상위 구조는 전역 제약, Phase 운영 규칙, Phase 지도, 파일 책임 지도, Phase 0~10, 요구사항 추적표, 구현 완료 정의, 검증 기록으로 구성된다.
- 가이드가 필수로 정한 `목표`, `현재 상태`, `범위의 포함·제외`, `기술적 제약`, `실행 순서와 의존성`, `변경 금지 항목`, `의사결정 및 중단 규칙`, `Progress`, `Decision Log`, `발견된 문제`, `최종 보고 형식`은 일부 내용이 본문에 혼재되거나 Phase 10 하위에만 있고 명시적 필수 section으로 탐색할 수 없다.
- 신규 Task는 각 Phase에 추가했지만 전체 실행 순서, 현재 미완료 문제 표, 최종 보고 형식으로 한 번에 이동할 navigation이 없다.
**영향·권장 조치·판정 기록**
- 후속 goal 실행자가 금지 변경·중단 조건·최신 문제를 본문 전체에서 재조합해야 하므로 추적 품질 문제지만 기능 계약 위반은 아니어서 Low로 판정했다.
- 기존 5,000여 행의 Task·Progress·Decision 이력을 삭제·재작성하지 않고, 필수 section을 명시적으로 추가하여 현재 본문을 mapping한다.
- 2026-07-31 — 가이드 필수 section inventory와 현재 heading을 대조해 확정, `P10-R8` 후속 신규 `P10-R9`로 전환했다. 구조는 이 리뷰에서 바로 고치지 않았다.
- 2026-07-31 — `P10-R9`에서 Goal 실행형 필수 12 section을 plan 상단에 명시하고 기존 Phase·Progress·Decision 이력으로 연결해 수정 완료했다.
### OpenAPI·검증·종료 판정
- **OpenAPI:** `jq` parse/version/path/operation/status/ref 검사는 exit 0이었다. OpenAPI 3.1.0, document 2.3.0, 25 paths, 37 operations 전체 `implemented`, 누락 schema ref 0건이다. 계약 JSON 변경은 필요하지 않다.
- **자동 증거:** `typecheck`, `lint`, 개발/운영 build, server E2E 36 tests는 통과했다. 전체 unit 5 failed / 392 passed와 focused 25 passed는 `REV-P9-009`로, bare mock E2E WebKit main 2 failed·focused 2 passed는 `REV-P9-010`으로 분리했다.
- **판정:** OpenAPI endpoint·DTO 신규 결함은 없고 Low 문서 2건을 확정해 `P10-R8~R9`로 전환했으며, 둘 다 수정 완료했다. 오탐·보류로 남은 후보는 없다.
- **남은 위험:** 실제 개발 API 수동 QA와 최종 Gate 재검증이 필요하다.
## 18. 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- `P10-R8~R9`의 현재형 설명, 설치 dependency, Goal 필수 12 section과 local Markdown link를 current staged working tree에서 재검증했다.
- 필수 section Node 검사는 12/12, local Markdown link 검사는 26 files / broken 0, OpenAPI 검사는 3.1.0·문서 2.3.0·25 paths·37 operations·missing schema ref 0이었다.
- 전체 unit 두 차례 각각 81 files / 409 tests, typecheck·lint·개발/운영 build와 staged diff check가 통과했다. build는 기존 500kB chunk warning만 표시했다.
### `REV-P10-011` — §5.1 현행화가 남고 stale 검증 명령도 과거 이력을 다시 매치함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | review 가이드 §3·5 실제 명령·결과 증거, `P10-R8` 완료 증거 |
| 소유 Task | 신규 `P10-R10` |
**근거·재현**
- `plan-task.md``P10-R8` 실행 명령은 `detail/edit/filter는 계약 대기|migration 필요|후속 구현|parser/UI 갱신 필요|P10-T[2457]에서`를 문서 전체에서 검색한다.
- exact 검색은 과거 Phase 진행 기록뿐 아니라 `P10-R8`의 실행 명령과 RED 기록 자체를 다시 찾아 exit 0과 여러 match를 반환했다. 따라서 기록된 “current-state stale 검색 0건”을 해당 명령으로는 재현할 수 없다.
- §5.1의 Audio·Community·FanTalk·파일 정책 행은 완료된 UTC 전송, pagination object, 답변 수정·팬 원글 삭제, 가격·오류·파일 경계를 각각 `P10-T2/T4/T5/T7`의 미래 작업처럼 표현한다.
- top 상태·Phase 지도·§5.1만 추출한 section-aware 검색도 위 네 행을 실제로 반환했다. 이는 과거 이력 오탐과 별개인 현재 문구 누락이다.
- 현재 top 상태·Phase 지도와 설치 dependency 자체는 수정 내용과 일치하지만 §5.1 현행화와 검증 대상 범위 한정이 모두 남았다.
**영향·권장 조치**
후속 실행자가 완료된 Phase 10 기능을 다시 계획하거나 실행 기록을 재현해 완료 증거를 신뢰하지 못할 수 있다. 과거 finding·Progress는 보존하고 §5.1을 구현 완료와 실제 server·수동 QA 대기로 구분한 뒤 top current-state·Phase 지도·§5.1만 선택하는 section-aware negative search의 실제 exit/result를 누적한다.
**판정 기록**
- 2026-07-31 — §5.1 미래형 잔존과 `P10-R8` exact 명령의 self/history match를 대조해 Low 확정.
- 2026-07-31 — 완료된 `P10-R8`을 다시 열지 않고 신규 `P10-R10`으로 전환. 제품 코드·OpenAPI는 수정하지 않았다.
- 2026-07-31 — `P10-R10`에서 §5.1 현재형 설명과 section-aware 검증 기준을 정정해 수정 완료.
### 종료 판정
- `P10-R8`의 현재 상태 내용과 `P10-R9`의 필수 section·link 보완은 유지됐다.
- **최종 결론:** `REV-P10-011`/`P10-R10` 수정 완료. 실제 개발 API 수동 QA는 별도다.
## P10-R10 수정 후 검증 기록 — 2026-07-31
- RED 대체: 과거 `P10-R8` 문서 전체 stale 검색은 과거 이력과 명령 자체를 다시 매치했고, section-aware 검색도 §5.1의 완료 기능 미래형 표현을 검출했다.
- GREEN/REFACTOR: §5.1을 구현 완료와 실제 개발 API 수동 QA 대기로 정정하고, 현재 검증은 top current-state·Phase 지도·§5.1만 대상으로 한 section-aware negative search로 한정했다. 기존 `P10-R8` 완료 판정과 과거 stale 근거는 보존했다.
- 검증: section-aware stale search는 0 matches였고, dependency 대조 Node 명령에서 현재 설치 dependency 목록을 확인했다. 전체 unit 411 tests, typecheck, lint, build:dev, build:prod도 통과했다.
## 19. P10-R10 수정 결과 재점검 — 2026-07-31
### `REV-P10-012` — 원문 검증 명령과 current-state 완료 판정을 재현할 수 없음
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | review 가이드 §3·5 실제 명령 증거, Goal plan 현재 상태·Progress, `P10-R10` 완료 증거 |
| 소유 Task | 신규 `P10-R11` |
**근거·재현**
- `P10-R10` 원문의 AWK heading 정규식은 Markdown에 `1\\.`, `3\\.`처럼 이중 escape돼 있다. 그대로 실행하면 section 종료를 찾지 못하고 이후 과거 이력 10건을 다시 match하므로 negative command는 실패한다.
- 같은 실행 명령의 “dependency 대조 Node 명령”, “필수 12 section 검사”, “Markdown link 검사”는 copy/paste 가능한 실제 명령이 아니다. 반면 검증 기록은 0 matches와 검사 통과를 완료 증거로 쓴다.
- plan 상단 상태·실행 순서·발견된 문제와 Phase 10 리뷰 metadata는 체크·검증 기록상 완료된 `P1-R16/P9-R11/P10-R10`을 여전히 수정 필요로 표시한다.
**영향·권장 조치·판정 기록**
- 후속 실행자가 기록된 명령을 재현할 수 없고 완료 Task를 다시 열 수 있다. `[.]` 기반 AWK와 완전한 Node/link 명령을 기록하고 신규 회귀 Task 완료 뒤 current-state metadata를 실제 Gate/수동 QA 상태로 갱신한다.
- 2026-07-31 — 동작하도록 escape를 바로잡은 검색은 0 matches였지만 원문 exact 명령은 10 matches임을 재현해 Low 문서 증거 회귀로 확정. 완료된 `P10-R10`을 다시 열지 않고 신규 `P10-R11`로 전환했다.
### 종료 판정
- §5.1의 Phase 10 완료/수동 QA 현행화 자체는 유지되고, `P10-R11`에서 원문 명령 재현성과 current-state 종료 판정을 복구했다.
- 검증: section-aware negative search, dependency stale 검사, 필수 12 section 검사, Markdown local link 검사, `git diff --check`는 모두 exit 0 / no output이었다. `npm run test:run`은 81 files / 417 tests passed, `typecheck`·`lint`·`build:dev`·`build:prod`도 exit 0이었다. mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome에서만 수집됐다.
- **최종 결론:** `REV-P10-012`/`P10-R11` 수정 완료. 실제 개발 API 수동 QA는 별도다.
## 20. P10-R11 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- `P10-R11`의 exact section-aware search, dependency·필수 12 section·local link·diff 명령을 문서에서 그대로 실행해 모두 exit 0 / no output임을 확인했다.
- 전체 81 files / 417 tests, `typecheck`, `lint`, 개발/운영 build가 통과했다. OpenAPI는 3.1.0 / document 2.3.0, 25 paths / 37 operations 전체 `implemented`, 누락 schema ref 0건이었다.
- Playwright 목록은 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다. 실제 E2E와 수동 QA는 실행하지 않았다.
### `REV-P10-013` — 최하단 Progress와 상단 수동 QA current-state가 최신 상태와 충돌함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | Goal plan 현재 상태·Progress, `P10-R11` 완료 증거 |
| 소유 Task | 신규 `P10-R12` |
**근거·영향**
- plan 상단은 `P1-R17/P9-R12/P10-R11` 수정 완료를 선언하지만 최하단 최신 Progress는 세 Task를 여전히 남은 항목으로 표시한다.
- 상단 남은 조건은 실제 개발 API Series/FanTalk/Comments/file policy만 표시해 기존 Phase 1 기록이 계속 대기로 둔 실제 crop pixel 비교와 stale ADMIN server 확인을 누락한다.
- 후속 실행자가 상단과 append-only Progress 중 무엇을 기준으로 해야 하는지 판단할 수 없고, 완료 Task를 다시 열거나 수동 QA를 빠뜨릴 수 있다.
**권장 조치·판정 기록**
- 과거 Progress는 보존하고 이번 재점검 결과를 더 최신 기록으로 append한다. `P9-R13` 완료 뒤 plan top/tail과 Phase 10 최신 결론을 자동 보완 완료 및 모든 수동 QA 대기로 맞춘다.
- 2026-07-31 — top·최하단 Progress와 Phase 1 수동 QA 기록을 대조해 Low로 확정. 완료된 `P10-R11`을 다시 열지 않고 신규 `P10-R12`로 전환했다.
- 2026-07-31 — `P10-R12`에서 plan top/tail current-state와 Phase 10 review metadata를 자동 보완 완료, 실제 crop pixel·stale ADMIN·개발 API 수동 QA 대기 기준으로 맞춰 수정 완료했다.
### 종료 판정
- `P10-R11`의 exact 명령과 자동 Gate 결과 자체는 재현됐다.
- **최종 결론:** `REV-P10-013`/`P10-R12` 수정 완료. 자동 보완 Task는 남아 있지 않고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 21. P10-R12 수정 결과 검증 — 2026-07-31
- 무엇을: plan 상단 상태, 현재 상태, 실행 순서, 발견된 문제, 최하단 최신 Progress와 Phase 10 리뷰 metadata·종료 판정을 같은 현재 상태로 정렬했다.
- 왜: 후속 실행자가 완료된 `P9-R13`/`P10-R12`를 다시 열지 않고, 남은 범위를 실제 crop pixel·stale ADMIN·개발 API Series/FanTalk/Comments/file policy 수동 QA로만 판단할 수 있게 하기 위해서다.
- 검증: `tail -n 20 docs/20260725_AI캐릭터관리자웹/plan-task.md | rg -n 'P1-R17.*P9-R12.*P10-R11.*P9-R13|crop pixel.*stale ADMIN.*Series/FanTalk/Comments/file policy'`, 필수 12 section 검사, Markdown local link 검사, `npm run test:run`, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check`가 통과했다.
- 남은 항목: 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원·검증 범위에서 제외한다.
## 22. P10-R12 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- plan 상단 다섯 current-state 위치, `P10-R12` checklist·실행 명령, 최하단 최신 Progress와 Phase 10 metadata·종료 판정을 대조했다.
- `P10-R12`의 exact `tail -n 20 | rg` 명령은 과거 남은 항목과 최신 완료·수동 QA 행을 함께 출력하며 현재 stale top 상태에서도 exit 0이었다.
### `REV-P10-014` — P10-R12 완료 상태 충돌을 검증 명령이 false positive로 통과함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | Goal plan 현재 상태·checklist·Progress, review 가이드 실제 명령 증거 |
| 소유 Task | 신규 `P10-R13` |
**근거·영향**
- plan 상단 상태·현재 상태·실행 순서·Progress·발견된 문제는 `P10-R12`를 다음 보완으로 유지하고 Task checklist도 모두 미완료다.
- 반면 최하단 최신 Progress와 Phase 10 metadata·판정 기록·종료 판정은 `P10-R12` 완료 및 자동 Task 없음으로 표시한다.
- `tail -n 20 | rg '완료 패턴|수동 QA 패턴'`은 OR 조건이고 top·checklist·review를 읽지 않아 이 충돌 상태에서도 exit 0이다. 과거 tail의 남은 항목도 함께 match했다.
- 후속 실행자가 완료 Task를 다시 열 수 있고, 기록된 명령으로는 current-state 일치를 증명할 수 없다.
**권장 조치·판정 기록**
- 기존 docs test에 top, `P10-R12` checklist, 마지막 Progress marker 이후, Phase 10 metadata와 최신 종료 판정을 독립 scope로 검사하는 contract를 추가한다. 현재 상태와 충족 checklist를 맞추고 OR 기반 tail 명령을 교체한다.
- 2026-07-31 — exact 명령 exit 0과 실제 top/checklist 충돌을 동시에 재현해 Low로 확정. 완료됐다고 기록된 `P10-R12`를 다시 열지 않고 신규 `P10-R13`으로 전환했다.
- 2026-07-31 — `P10-R13`에서 docs contract가 plan top, `P10-R12` checklist, 최신 Progress와 Phase 10 최신 section을 독립 scope로 검사하게 했다. plan top/checklist/review를 자동 보완 완료와 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기로 맞춰 수정 완료했다.
### 종료 판정
- 최하단의 남은 수동 QA 목록과 Chromium/mobile Chrome 지원 범위 자체는 올바르다.
- **최종 결론:** `REV-P10-014`/`P10-R13` 수정 완료. 자동 보완 Task는 남아 있지 않고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 23. P10-R13 수정 후 검증 — 2026-07-31
### 검토 범위와 실행 증거
- plan top current-state, `P10-R12` checklist, 최신 Progress marker, Phase 10 리뷰 metadata와 종료 판정을 독립 scope로 대조했다.
- 기존 `tail -n 20 | rg` OR match는 현재 완료 증거에서 사용하지 않고, docs contract가 stale top/checklist 상태를 직접 실패시키도록 변경했다.
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 9 tests passed. `npm run test:run` — 81 files / 418 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, 필수 12 section Node 검사와 `git diff --check` — 모두 exit 0. build는 기존 500kB chunk warning만 표시했다.
### 종료 판정
- **최종 결론:** `REV-P10-014`/`P10-R13` 수정 완료. 자동 보완 Task는 남아 있지 않고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 24. P10-R13 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- plan 상단 current-state 다섯 위치, `P10-R13` 최신 Progress record, Phase 10 review metadata·최신 H2·종료 판정과 docs contract를 독립 대조했다.
- finding 기록 전 docs contract 2 files / 9 tests, fresh full unit 81 files / 418 tests, `typecheck`, `lint`, 개발/운영 build가 통과했다. finding과 신규 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다. Playwright 목록은 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다.
- negative control에서 stale `P10-R13` record 뒤 후속 record에 필수 문구를 두면 `plan.slice(latestProgressIndex)` 기반 assertion이 통과했다. contract는 plan 상단 `## Progress`와 Phase 10 metadata를 추출하지 않고, 최신 H2 내부 `### 종료 판정`도 독립 범위로 검사하지 않는다.
### `REV-P10-015` — current-state contract가 Progress·metadata·종료 판정을 독립 범위로 닫지 않음
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P10-R13` top/checklist/latest Progress/review 독립 scope 완료 증거 |
| 소유 Task | 신규 `P10-R14` |
**근거·영향**
- plan 상단 다섯 current-state 중 `## Progress`는 contract 대상에서 빠져 있어 다른 네 위치와 충돌해도 통과한다.
- 최신 Progress는 exact marker를 찾은 뒤 EOF까지 읽는다. append-only 후속 record가 생기면 stale `P10-R13` record에 없는 완료·수동 QA 문자열을 뒤 record에서 가져올 수 있다.
- Phase 10 review는 최신 H2 전체에서 토큰을 찾을 뿐 상단 `## 1. 리뷰 정보` metadata와 H2 내부 `### 종료 판정`을 별도 추출하지 않는다. metadata나 종료 판정이 stale해도 같은 H2의 다른 H3로 통과할 수 있다.
- 현재 문서의 실제 상태는 일치하고 제품·OpenAPI·Playwright 설정에는 신규 결함이 없지만, 기록된 contract가 그 일치를 지속적으로 증명하지 못한다.
**권장 조치·판정 기록**
- plan 상단 Progress, exact `P10-R13` record, Phase 10 metadata, 최신 H2와 종료 판정을 각각 추출한다. Progress는 다음 독립 record 또는 EOF에서 닫고 후속 record/H3 negative assertion을 남긴다.
- 2026-07-31 — synthetic 후속 record false positive와 실제 누락 scope를 재현해 Low로 확정. 완료된 `P10-R13`을 다시 열지 않고 `P9-R16` 뒤 신규 `P10-R14`로 전환했다.
- 2026-08-01 — `P10-R14`에서 plan 상단 `## Progress`, exact `P10-R13` Progress record, Phase 10 metadata, 최신 H2와 내부 `### 종료 판정`을 각각 닫힌 범위로 검사하게 했다. 후속 Progress/H3 synthetic false positive assertion을 남겨 수정 완료로 판정했다.
- 수정 후 검증: docs contract 2 files / 9 tests passed, `typecheck`, `lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check` 모두 exit 0. Playwright `--list`는 mock 104 tests와 server 18 tests를 Chromium/mobile Chrome에서만 수집했다.
### 종료 판정
- `REV-P10-014`의 top/checklist/current-state 수정 내용 자체는 현재 문서에서 확인됐다.
- **최종 결론:** `REV-P10-015`/`P10-R14` 수정 완료. 자동 보완 Task는 남아 있지 않고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA는 별도 대기다.
## 25. P10-R14 수정 후 검증 — 2026-07-31
### 검토 범위와 실행 증거
- plan 상단 `## Progress`, exact `P10-R13` Progress record, Phase 10 metadata, 최신 H2와 내부 종료 판정을 독립 scope로 대조했다.
- docs contract는 후속 Progress record와 H3 문자열을 이전 section 판정으로 가져오지 않는 synthetic assertion을 포함한다.
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 9 tests passed. `npm run typecheck`, `npm run lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check` — 모두 exit 0. Playwright `--list`는 mock 104 tests와 server 18 tests를 Chromium/mobile Chrome에서만 수집했다.
### 종료 판정
- **최종 결론:** `REV-P10-015`/`P10-R14` 수정 완료. 자동 보완 Task는 남아 있지 않고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 26. P10-R14 수정 결과 재점검 — 2026-08-01
### 검토 범위와 실행 증거
- `progressRecordAtMarker()` 호출 marker, plan §7 최하단, top current-state, Phase 10 metadata·`## 25` 종료 판정을 current working tree와 synthetic mutation으로 대조했다.
- finding 기록 전 docs contract 2 files / 9 tests, fresh full unit 81 files / 418 tests, `typecheck`, `lint`, 필수 12 section·Markdown link·diff 검사가 통과했다. Playwright 목록은 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다. 신규 finding과 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다.
- §7의 em-dash형 `P10-R13` record를 stale로 바꿔도 contract가 선택한 Task 본문의 괄호형 record는 변하지 않아 test가 통과한다. plan 최하단은 `P9-R16``P10-R14`를 남은 항목으로 유지하며, 2026-08-01 `P10-R14` 기록이 참조하는 새 `## 25`는 2026-07-31로 기재됐다.
### `REV-P10-016` — contract가 §7 최신 Progress 대신 Task-local 기록을 검사함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P10-R14` §7 Progress 경계·top/tail/review current-state 완료 증거 |
| 소유 Task | 신규 `P10-R15` |
**근거·영향**
- `mock-preview-docs.test.ts`는 완료된 `P9-R16`·`P10-R14` checklist를 검사하지 않고 `P10-R12` checklist와 `**P10-R13 수정 검증 기록 (2026-07-31):**`을 선택한다. 후자는 Phase 10 Task 본문의 로컬 기록이며, 원래 EOF false positive가 발생한 §7 marker는 `**P10-R13 수정 검증 기록 — 2026-07-31:**`이다.
- 현재 plan 최하단의 마지막 record는 `P9-R16``P10-R14`를 남은 자동 Task로 표시하지만 plan top과 Phase 10 review는 둘 다 완료라고 선언한다. 완료 기록이 §7에 append되지 않았다.
- `P10-R14` Task 기록은 2026-08-01인데 새 Phase 10 검증 H2는 2026-07-31로 적혀 실행 증거 날짜도 일치하지 않는다.
- 제품·OpenAPI·Playwright 설정에는 영향이 없지만 current-state contract가 다시 실제 stale Progress를 놓친다.
**권장 조치·판정 기록**
- current Task checklist를 검사하고 contract를 §7 em-dash marker로 옮겨 다음 record에서 닫은 뒤 이번 후속 수정 완료를 새 최하단 record로 append한다. Phase 10에는 날짜 정정과 current 판정을 새 H2로 누적하고 top/tail/metadata/실제 마지막 H2·종료 판정을 함께 검사한다.
- 2026-08-01 — 실제 marker 두 개의 index와 tail mutation 전후 선택 결과가 동일함을 재현해 Low로 확정. 완료된 `P10-R14`를 다시 열지 않고 `P9-R17` 뒤 신규 `P10-R15`로 전환했다.
- 2026-08-01 — `P10-R15`에서 §7 em-dash형 record와 current 완료 record를 append하고 docs contract 2 files / 9 tests를 통과해 수정 완료로 판정했다.
### 종료 판정
- `P10-R14`의 helper 경계와 metadata/H3 분리 자체는 fresh docs contract에서 통과했다.
- **최종 결론:** `REV-P10-016` 후속 goal 필요. 다음 자동 순서는 `P9-R17``P10-R15`이며 실제 crop pixel·stale ADMIN server와 개발 API 수동 QA는 별도 대기다.
## 27. P10-R15 수정 후 검증 — 2026-08-01
### 검토 범위와 실행 증거
- `P9-R16`·`P10-R14` checklist, §7 em-dash형 Progress marker, plan top current-state, Phase 10 metadata와 실제 마지막 H2·종료 판정을 독립 scope로 대조하게 했다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps Phase 10 current state"` — 1 file / 1 failed / 5 skipped. §7 최신 `P9-R17·P10-R15` 완료 record가 없어 실패했다.
- GREEN: `REV-P10-016`/`P10-R15` 수정 완료 상태를 append-only로 기록하고, §7 em-dash형 Progress를 current-state contract 대상으로 고정했다. focused GREEN은 1 file / 1 passed / 5 skipped, docs contract는 2 files / 9 tests passed, 전체 unit은 81 files / 418 tests passed였다. `typecheck`, `lint`, 필수 section·link·diff 검사는 exit 0이고 Playwright `--list`는 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다. 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA는 완료 주장하지 않는다.
### 종료 판정
- **최종 결론:** `REV-P10-016`/`P10-R15` 수정 완료. 자동 보완 Task는 완료됐고 Chromium/mobile Chrome 지원 범위는 유지한다. 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 28. P10-R15 수정 결과 재점검 — 2026-08-01
### 검토 범위와 실행 증거
- `P10-R15`의 checklist, §7 em-dash형 Progress와 current-state contract, Phase 10 metadata·실제 마지막 H2·종료 판정을 current working tree와 synthetic mutation으로 대조했다.
- finding 기록 전 docs contract는 2 files / 9 tests, 전체 unit은 81 files / 418 tests가 통과했고 `typecheck`, `lint`도 exit 0이었다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
- 신규 finding·Task를 current-state에 반영한 뒤 docs contract는 2 files 중 1 failed / 1 passed, 9 tests 중 2 failed / 7 passed의 RED가 됐다. Phase 10 실패는 완료 상태만 기대하던 top current-state assertion에서 발생했다. 필수 section 12/12, Markdown link broken 0, `git diff --check`는 통과했다.
- `P10-R15`이 추가한 완료 record와 review 결론은 확인했지만 contract가 그 Task와 실제 최신 §7 record를 지속해서 보장하지 못한다.
### `REV-P10-017` — 완료 finding·Task와 실제 최신 §7 Progress가 contract에서 누락됨
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P10-R15` finding/checklist/latest Progress current-state 완료 증거 |
| 소유 Task | 신규 `P10-R16` |
**근거·영향**
- metadata와 `## 27``REV-P10-016`/`P10-R15` 수정 완료를 선언하지만 `REV-P10-016` 표의 상태는 `확정`으로 남아 있고 해당 finding의 날짜별 수정 근거도 없다.
- docs contract는 `P9-R16`·`P10-R14` checklist만 검사하며 완료 주체인 `P10-R15` 자체 checklist를 검사하지 않는다.
- `latestProgress``P9-R17·P10-R15` marker를 고정 선택한다. 이 뒤에 `보완 필요`인 새 독립 Progress를 append해도 과거 완료 record만 검사해 통과하며, synthetic fixture는 실제 §7 em-dash형이 아닌 과거 Task-local 괄호형 marker를 계속 사용한다.
- 제품·OpenAPI·Playwright 설정에는 영향이 없지만 append-only 문서의 실제 현재 상태를 다시 놓칠 수 있다.
**권장 조치·판정 기록**
- `REV-P10-016``수정 완료`로 종결하고 날짜별 근거를 append한다. `P10-R15` checklist를 직접 검사하며 §7의 실제 마지막 독립 Progress record를 동적으로 선택하고 synthetic marker를 em-dash형으로 통일한다.
- 2026-08-01 — checklist mutation과 후속 stale Progress append에도 선택 record가 변하지 않음을 재현해 Low로 확정. 완료된 `P10-R15`를 다시 열지 않고 `P9-R18`~`P9-R19` 뒤 신규 `P10-R16`으로 전환했다.
- 2026-08-01 — `P10-R15` checklist와 `REV-P10-016`·`REV-P10-017` 상태를 직접 검사하고 §7의 실제 마지막 독립 record를 동적으로 선택하게 해 수정 완료로 판정했다.
### 종료 판정
- `P10-R15`의 §7 em-dash형 record 추가와 top/review 완료 상태 수정은 확인됐고 Chromium/mobile Chrome 2-project 범위는 유지된다.
- **최종 결론:** `REV-P10-017` 후속 goal 필요. 다음 자동 순서는 `P9-R18``P9-R19``P10-R16`이며 실제 crop pixel·stale ADMIN server 및 개발 API 수동 QA는 별도 대기다.
## 29. P10-R16 수정 후 검증 — 2026-08-01
### 검토 범위와 실행 증거
- `P10-R15` 자체 checklist, `REV-P10-016`·`REV-P10-017` 상태, plan §7의 실제 마지막 독립 Progress record를 current contract에 포함했다.
- RED: 고정된 과거 marker를 선택한 synthetic Progress는 현재 record를 찾지 못해 1 failed / 8 skipped였고, `P10-R16` current-state focused test는 plan 상단의 보완 대기 상태로 1 failed / 8 skipped였다.
- GREEN: §7에서 em-dash 날짜형 독립 marker의 마지막 항목을 선택하도록 최소 helper를 추가하고 synthetic marker를 실제 형식으로 통일했다.
- GREEN/회귀: Phase 10 current-state와 최신 Progress focused는 2 passed / 7 skipped, docs contract는 2 files / 12 tests passed였다.
- 전체 검증: 81 files / 421 tests, `typecheck`, `lint`, 필수 section·Markdown link·diff 검사가 통과했다. Playwright 목록은 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다.
### 종료 판정
- **최종 결론:** `REV-P10-017`/`P10-R16` 수정 완료. 실제 마지막 독립 Progress와 top/review current-state가 일치하고 자동 보완 Task는 완료됐다. Chromium/mobile Chrome 2-project 지원 범위는 유지하며 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 30. P10-R16 수정 결과 재점검 — 2026-08-01
### 검토 범위와 실행 증거
- 독립 reviewer가 `latestProgressRecord()`의 마지막 marker 선택과 `progressRecordAtMarker()`의 record 시작점 선택을 동일 marker 반복 조건으로 대조했다.
- 나머지 Task 경계, finding/checklist 상태, fenced·동일 H2 처리와 plan top/tail·Phase 9/10 review 동기화는 일치했다.
### `REV-P10-018` — 동일 Progress marker 반복 시 첫 record를 다시 선택함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P10-R16` 실제 마지막 독립 Progress record contract |
| 소유 Task | 신규 `P10-R17` |
**근거·영향**
- `latestProgressRecord()`는 마지막 marker의 문자열만 얻은 뒤 `progressRecordAtMarker()`에 전달한다. 후자는 `findIndex()`로 첫 동일 문자열 occurrence를 선택한다.
- append-only §7에 동일 제목·날짜 marker가 반복되면 오래된 완료 record를 반환해 최신 `보완 필요` 상태를 놓칠 수 있다.
**권장 조치·판정 기록**
- 기존 marker helper가 뒤에서 마지막 occurrence를 찾게 하고 동일 marker 반복 negative-control을 남긴다.
- 2026-08-01 — 독립 reviewer가 line-level 흐름과 synthetic 조건을 대조해 Low로 확정. 완료된 `P10-R16`을 다시 열지 않고 신규 `P10-R17`로 전환했다.
- 2026-08-01 — marker를 뒤에서 찾아 마지막 동일 occurrence를 사용하고 중복 marker synthetic을 통과해 수정 완료로 판정했다.
### 종료 판정
- **최종 결론:** `REV-P10-018` 후속 goal 필요. `P10-R17`을 plan에 추가했으며 Chromium/mobile Chrome 지원 범위와 수동 QA 대기는 유지한다.
## 31. P10-R17 수정 후 검증 — 2026-08-01
### 검토 범위와 실행 증거
- `progressRecordAtMarker()`가 첫 occurrence 대신 마지막 동일 marker occurrence를 선택하게 하고 `P10-R17` checklist와 `REV-P10-018` 상태를 current contract에 포함했다.
- RED: 동일 marker 반복 synthetic이 과거 완료 record를 반환해 1 failed / 8 skipped였고, GREEN은 1 passed / 8 skipped였다. current-state RED는 plan 상단의 보완 대기 상태로 1 failed / 8 skipped였다.
- 검증: Phase 10 focused 2 passed / 7 skipped, docs contract 2 files / 12 tests, 전체 unit 81 files / 421 tests가 통과했다. `typecheck`, `lint`, 필수 section·Markdown link·diff 검사도 exit 0이었다. Playwright 목록은 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다.
- 독립 재리뷰: 직전 동일 marker Important는 닫혔고 신규 Critical/Important/Minor 없음, 요청 범위 merge ready로 판정됐다.
### 종료 판정
- **최종 결론:** `REV-P10-018`/`P10-R17` 수정 완료. 마지막 동일 marker occurrence와 실제 최신 Progress를 검사하며 자동 보완 Task는 완료됐다. Chromium/mobile Chrome 2-project 지원 범위는 유지하고 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.

View File

@@ -0,0 +1,146 @@
# Phase 2 Mock Preview 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 2 / browser MSW, mock session, preview shell |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- mock/server mode 경계, mock 인증, fixture 보안과 preview shell의 현재 동작을 검증한다.
- mock 구현이 실제 OpenAPI operation과 분리 모드 원칙을 유지하는지 확인한다.
### 포함 범위
- 코드: `src/shared/mocks`, mock bootstrap과 preview shell
- 테스트: mock mode boundary, preview shell, storage, 전체 mock Gate
- 문서: `MOCK-*`, `P2-*`, OpenAPI implemented operation
- 수동 검증: handler와 API base URL 구성 정적 대조
### 제외 범위
- 도메인별 mock store 의미 검증은 각 해당 Phase에서 판정
- 외부 서버와 mock fixture의 데이터 동일성 보증
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. runtime fallback, 비밀정보 저장과 계약 밖 handler를 중점 확인했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `MOCK-001`~`MOCK-005`, PRD §13
- 계획: `P2-T1`~`P2-GATE`
- 코드·테스트: `src/shared/mocks/**`, `tests/e2e/mock-mode-boundary.spec.ts`, `mock-preview-shell.spec.ts`, `error-mapping.spec.ts`
- 계약 점검: OpenAPI 25 paths, 37 operations, 전부 `x-implementation-status=implemented`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
VITE_API_MODE=mock / Playwright 4 projects
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 실패 | 201 passed, 22 skipped, 5 failed |
| error-mapping 실패 분석 | 완료 | 4개 프로젝트 모두 과거 origin 사용; `REV-P0-004`로 귀속 |
| WebKit Series 768px focused 재실행 | 성공 | 1 passed, timeout 비재현 |
| mock preview·storage 관련 E2E | 성공 | preview login/logout, MSW 등록, secret/file body 비영구 저장 test 통과 |
## 5. 발견 사항 요약
**확정 발견 사항 없음.**
전체 mock Gate의 origin 실패는 Phase 0 설정·문서·test 기준 불일치이므로 `REV-P0-004`에서 관리한다.
## 6. 발견 사항 상세
Phase 2 자체의 신규 확정 항목은 없다. browser MSW는 명시적 mock mode에서만 등록되고 server mode runtime 404 fallback은 사용하지 않으며, mock preview session과 storage 보안 test가 통과했다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음. Phase 2 신규 회귀 Task를 추가하지 않았다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | handler·bootstrap·test·계약 대조 |
| 후보 항목 판정 완료 | 충족 | 신규 후보 없음 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 항목 없음 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 없음
**남은 항목:** Phase 2 자체 신규 항목 없음. 전체 mock Gate 복구는 `P0-R2`에 귀속한다.
## 9. 수정 후 검증 기록
이번 리뷰에서 수정한 항목 없음.
## 10. 2차 점검 결과 — 2026-07-30
- explicit mock mode Chromium matrix 57 tests가 통과했고 browser MSW 등록·server no-fallback 경계가 유지됐다.
- Phase 2 신규 발견은 없다. 도메인 mock 의미 불일치는 각 소유 Phase의 신규 Task로만 기록했다.
## 11. 2026-07-31 재점검
- **기준:** OpenAPI 2.3.0의 25 paths / 37 implemented operations, explicit `server`/`mock` mode와 browser MSW 경계를 현재 코드·test와 대조했다.
- **검증:** mock worker 등록, production graph 제외, no-fallback unit은 전체 72 files / 360 tests 안에서 통과했다. `npm run e2e` server allowlist도 36 passed였다.
- **판정:** mock fixture/handler의 Phase 2 기반에서 신규 계약 위반은 확인되지 않았다. 도메인별 crop·mutation 결함은 각 소유 Phase에 등록했다.
- **교차 Phase:** 당시 Mobile Safari 장시간 실행 실패 후보는 focused 반복에서 재현되지 않았고, 이후 지원 project 축소로 현재 Task에서 제외했다.
- **신규 Task:** 없음.
## 12. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** explicit `server`/`mock` 선택, production mock 거부, browser MSW 등록·fixture store·no-fallback 경계를 `MOCK-001~009`와 대조했다.
- **실행 증거:** `src/shared` 31 files / 117 tests, server E2E 18 tests, mock Chromium matrix 57 tests 통과. OpenAPI JSON도 parse 성공했고 현재 계약은 25 paths / 37 operations다.
- **판정:** Phase 2 기반과 실제 Page/API adapter를 공유하는 mock 경계에서 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API 수동 QA는 mock 성공과 별도 상태로 유지한다.
- **신규 Task:** 없음.
## 13. 종합 재점검 — 2026-07-31
- **검토 범위:** explicit `server`/`mock` mode, browser MSW 등록·unhandled request 차단, OpenAPI 2.3.0 fixture/handler 경계를 현재 Page·adapter와 재대조했다.
- **실행 증거:** OpenAPI JSON parse 및 25 paths / 37 operations 집계 성공. exact `npm run e2e:mock`은 현재 구성의 Chromium·Mobile Chrome에서 109 passed / 5 skipped, exit 0이었다.
- **판정:** Phase 2 소유의 확정 신규 발견 사항 없음.
- **교차 Phase:** 공용 crop 문제는 `REV-P1-016~017`/`P1-R10~R11`, 지원 browser project 축소는 `REV-P9-007`/`P9-R7`에만 등록한다.
- **남은 위험:** mock 성공은 실제 개발 API integration 증거가 아니다. WebKit 계열 mock Gate는 현재 지원 범위에서 제외한다.
- **신규 Phase 2 Task:** 없음.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** explicit server/mock mode, browser MSW, no-auto-fallback, production 차단, README·script contract와 4-project mock matrix.
- **실행 증거:** `npm run e2e:mock`은 Chromium 52, WebKit 44, Mobile Chrome 47, Mobile Safari 43으로 총 186 passed / 22 skipped / 0 failed. `npm run e2e:mock -- --list --project=chromium`은 인자를 전달해 52 tests를 정상 수집했다.
- **판정:** runtime mode 경계와 mock handler에는 신규 결함이 없다. 다만 전체 unit에서 `mode-boundary.test.ts``mock-preview-docs.test.ts`가 현재 wrapper/README 불일치로 2건 실패했다.
- **소유 판정:** root cause가 `P9-R7`의 browser Gate 복원 변경이므로 `REV-P9-008`/`P9-R8`에 전환하고 Phase 2 Task를 중복 추가하지 않는다.
- **남은 위험:** mock 성공은 실제 개발 API integration 완료 증거가 아니다.
## 15. 최종 재검증 — 2026-07-31
- **검토 범위:** explicit server/mock mode, browser MSW 등록, production 차단, no-auto-fallback과 4-project fixture/handler 흐름.
- **실행 증거:** exact `npm run e2e:mock`은 Chromium 52, WebKit 44, Mobile Chrome 47, Mobile Safari 43으로 186 passed / 22 skipped / 0 failed였다. 전체 unit 394 tests와 server E2E 36 tests도 통과했다.
- **판정:** Phase 2 소유의 확정 신규 발견 사항 없음. 기존 `REV-P9-008`은 수정 완료 상태다.
- **남은 위험:** mock 성공은 실제 개발 API integration 증거가 아니다.
- **신규 Phase 2 Task:** 없음.
## 16. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** explicit `server|mock` mode, browser MSW 등록·fixture store, production mock 차단, no-auto-fallback, 4-project Playwright 구성을 `MOCK-001~009`·OpenAPI와 대조했다.
- **실행 증거:** OpenAPI 25 paths / 37 implemented operations·schema ref 검사, `typecheck`·`lint`·개발/운영 build, server E2E 36 tests가 통과했다. 전체 unit 5 failed / 392 passed는 `REV-P9-009`/`P9-R9`로 분리했다. bare mock E2E는 Chromium 52 passed 후 WebKit main 2 failed / 32 passed / 7 skipped로 중단됐고 동일 2-test focused는 통과해 `REV-P9-010`/`P9-R10`의 browser Gate 비결정성으로 분리했다.
- **판정:** Phase 2 mock/server 경계·handler 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 2 Task 없음.
- **남은 위험:** mock 통과는 실제 개발 API integration 증거가 아니므로 server 수동 QA를 계속 분리한다.

View File

@@ -0,0 +1,350 @@
# Phase 3 Character Workspace 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 3 / Character 목록·상세·편집·워크스페이스 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Character CRUD, URL 문맥, 비활성 read-only 정책과 탭 접근성이 요구사항·계약에 맞는지 확인한다.
- 데스크톱·모바일·keyboard-only 워크스페이스 회귀 범위를 점검한다.
### 포함 범위
- 코드: `src/features/characters`, `src/layouts/CharacterWorkspaceLayout.tsx`
- 테스트: Character unit/integration 및 `character-workspace.spec.ts`
- 문서: Character 요구사항, PRD §7.2·§14.2, `P3-*`
- 수동 검증: WAI-ARIA Tabs Pattern과 markup·keyboard handler 정적 대조
### 제외 범위
- Phase 4~8 하위 리소스의 도메인별 mutation
- 실제 외부 서버 Character fixture를 이용한 수동 QA
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 핵심 Character 관리 불능 또는 데이터·보안 위험 |
| High | Character 요구사항·계약의 주요 위반 |
| Medium | 특정 입력 방식·화면에서 기능 또는 접근성이 깨짐 |
| Low | 비핵심 UX·운영 문구·유지보수 정합성 문제 |
상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: Character 요구사항, PRD §7.2, §10.3, §14.2
- 계획: `P3-T1`~`P3-GATE`
- 코드: `src/layouts/CharacterWorkspaceLayout.tsx:50-63`
- 테스트: `src/layouts/CharacterWorkspaceLayout.test.tsx:89-105`, `tests/e2e/character-workspace.spec.ts`
- 외부 기준: [WAI-ARIA Authoring Practices Guide Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright 4 projects, 320/768/1280px와 200% zoom 자동화 포함
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 전체 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | Character workspace 관련 실행 항목은 통과 또는 프로젝트 정책상 skip; 전체 실패는 Phase 0 origin 4건과 비재현 Series timeout |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| tab markup 정적 검증 | 실패 | 5개 `role=tab`이 모두 기본 tab stop이며 ArrowLeft/ArrowRight·roving tabindex 없음 |
| 비활성 안내 문구 정적 검증 | 실패 | 사용자 화면과 test에 내부 Task ID `P3-T2` 노출 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P3-004` | Medium | 수정 완료 | ARIA tablist가 화살표 키·roving tabindex·tabpanel 이름 연결을 제공하지 않는다 | `P3-R3` | `P3-R3` |
| `REV-P3-005` | Low | 수정 완료 | 비활성 캐릭터 안내에 내부 계획 ID와 오래된 범위 문구가 노출된다 | `P3-R3` | `P3-R3` |
## 6. 발견 사항 상세
### REV-P3-004 — ARIA tablist가 화살표 키·roving tabindex·tabpanel 이름 연결을 제공하지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §7.2, §10.3, §14.2
- **관련 계약:** 없음
- **소유 Task:** 신규 `P3-R3`
**관찰 내용**
워크스페이스는 링크 5개를 `role="tab"`으로 선언하지만 모두 기본 tab stop이다. 선택 tab만 `tabIndex=0`으로 두는 roving tabindex, 좌우 화살표 이동, Home/End 처리와 `tabpanel``aria-labelledby` 연결이 없다.
**근거**
- 코드: `src/layouts/CharacterWorkspaceLayout.tsx:56-63`
- 테스트: tabs의 선택 상태는 확인하지만 화살표 이동·roving tabindex·tabpanel accessible name 회귀 test가 없음
- 문서: PRD §14.2는 keyboard-only 탭 이동을 성공 기준으로 둠
- 표준: WAI-ARIA Tabs Pattern은 horizontal tablist에서 좌우 화살표 이동과 활성 tab 하나의 tab sequence 진입을 정의함
**재현 또는 검증 절차**
1. `/ai-characters/:characterId`에서 키보드로 워크스페이스 tablist에 진입한다.
2. `ArrowRight` 또는 `ArrowLeft`를 누른다.
3. 선택·focus가 다음/이전 tab으로 이동하지 않는 것을 확인한다.
4. `Tab`을 반복하면 모든 tab을 개별 순회하며 tabpanel이 tab label로 명명되지 않는 것을 확인한다.
**영향**
키보드와 보조기술 사용자는 ARIA tab widget의 표준 동작을 사용할 수 없고, five-tab navigation에 불필요한 tab stop을 반복한다.
**권장 조치**
기존 link navigation을 유지하면서 roving tabindex, 방향키/Home/End, tab ID와 `aria-labelledby` 연결을 추가하고 component/E2E keyboard 회귀 test를 작성한다.
**판정 기록**
- 2026-07-30 — PRD keyboard 기준, APG pattern과 현재 markup·handler를 대조해 확정.
- 2026-07-30 — `P3-R3`에서 link navigation 의미를 `nav`로 정정하고 현재 위치·keyboard focus 회귀 test를 추가해 수정 완료 판정.
### REV-P3-005 — 비활성 캐릭터 안내에 내부 계획 ID와 오래된 범위 문구가 노출된다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 비활성 Character workspace read-only 정책
- **관련 계약:** 없음
- **소유 Task:** 신규 `P3-R3`
**관찰 내용**
비활성 banner가 “`P3-T2 범위에는 ... 진입점이 없습니다`”라고 표시한다. 현재는 후속 Phase에서 mutation이 구현됐고 공통 정책으로 차단되므로 내부 Task ID와 과거 범위 설명 모두 사용자 관점에서 부정확하다.
**근거**
- 코드: `src/layouts/CharacterWorkspaceLayout.tsx:50-54`
- 테스트: `src/layouts/CharacterWorkspaceLayout.test.tsx:103`이 내부 문구를 고정
- 문서: PRD §12는 비활성 workspace의 mutation 진입점 차단이라는 제품 정책만 규정
**재현 또는 검증 절차**
1. 비활성 Character 상세에 진입한다.
2. read-only banner를 읽는다.
3. 사용자에게 의미 없는 계획 ID `P3-T2`와 현재 구현 범위에 맞지 않는 문구가 노출되는 것을 확인한다.
**영향**
운영자에게 내부 개발 정보를 노출하고 실제 read-only 정책의 이유와 범위를 혼동시킨다.
**권장 조치**
Task ID를 제거하고 “비활성 캐릭터에서는 생성·수정·삭제할 수 없다”처럼 현재 정책을 설명하는 운영 문구로 교체한다.
**판정 기록**
- 2026-07-30 — 현재 후속 Phase 구현과 고정 test 문구를 대조해 확정.
- 2026-07-30 — `P3-R3`에서 내부 Task ID를 제거하고 현재 read-only 정책 문구로 교체해 수정 완료 판정.
## 7. 확정 항목의 plan·goal 전환
- `REV-P3-004`, `REV-P3-005``plan-task.md` 신규 `P3-R3`
- goal objective: `[P3-R3] 워크스페이스 tab keyboard semantics와 비활성 운영 문구를 복구한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Character 코드·test·접근성 기준 대조 |
| 후보 항목 판정 완료 | 충족 | 2건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P3-R3` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** `REV-P3-004`, `REV-P3-005` 수정 완료
**남은 항목:** Phase 3 자체 회귀 없음. 공통 비활성화 mutation pending·오류 복구는 `REV-P9-004`/`P9-R4`에서 추적한다.
## 9. 수정 후 검증 기록
### P3-R3 수정 후 검증 — 2026-07-30
- RED: `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx`는 기존 tab semantics와 내부 Task 문구 assertion에서 2 failed / 3 passed였다.
- GREEN: link 기반 화면 이동은 native `nav` semantics와 `aria-current=page`로 정렬하고, 비활성 안내에서 `P3-T2`를 제거했다. focused layout test는 5 passed였다.
- 회귀: 관련 9 files / 45 tests, Chromium Character E2E 11 tests가 통과했고 `npm run typecheck`, `npm run lint`는 exit 0이었다.
- 2차 전체 검증: `npm run test:run` 72 files / 358 tests, `npm run build` 255 modules transformed, server allowlist E2E 36 passed로 현재 수정 상태를 재확인했다.
## 10. 2026-07-31 재점검
### 실행·판정 요약
- **기준:** commit `dd30e36323543e8f60e9983326503653e8001f12`, PRD §7.1~7.2·`CHAR-001~018`·`FILE-010`과 OpenAPI Character operation 재대조.
- **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다.
- **신규 발견:** High 1건, Medium 2건. 기존 `REV-P3-004~005`는 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P3-006` | Medium | 수정 완료 | 워크스페이스 header가 필수 `characterId`를 숨긴다 | `P3-R4` | `P3-R4` |
| `REV-P3-007` | Medium | 수정 완료 | Character URL이 계약 query `searchTerm` 대신 `search`를 사용한다 | `P3-R5` | `P3-R5` |
| `REV-P3-008` | High | 수정 완료 | Character 원본 image를 crop 적용 전에 제출할 수 있다 | `P3-R6` | `P3-R6` |
### REV-P3-006 — 워크스페이스 header가 필수 `characterId`를 숨긴다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §7.2
- **소유 Task:** 신규 `P3-R4`
**관찰 내용**
수정 전 공통 workspace header는 이미지·이름·상태만 표시하며 `character.id`를 렌더링하지 않았다. 테스트는 요구사항과 반대로 `characterId:` 미노출을 성공 조건으로 고정했다. 프로필 본문의 `characterUUID`는 numeric `characterId`를 대체하지 않는다.
**근거**
- 문서: `prd.md:155-159` — 상단에 `characterId`를 항상 표시
- 코드: `src/layouts/CharacterWorkspaceLayout.tsx:40-55`
- 테스트: `src/layouts/CharacterWorkspaceLayout.test.tsx:81-85`
- 이력: `plan-task.md`의 2026-07-30 visual QA 기록이 header ID 제거를 명시한다.
**재현 또는 검증 절차**
1. `/ai-characters/101` 또는 하위 workspace route에 진입한다.
2. 공통 header에서 이미지·루나·공개 상태는 보이지만 numeric `101` 식별자는 없음을 확인한다.
**영향 및 권장 수정 방향**
이름이 같거나 유사한 캐릭터를 운영할 때 현재 작업 대상을 오인할 수 있다. 공통 header에 label과 함께 numeric ID를 복구하고 active/inactive 및 모든 하위 route 회귀 test를 추가한다.
**판정 기록:**
- 2026-07-31 — PRD 명시 요구사항과 구현·반대 assertion을 대조해 확정.
- 2026-07-31 — `P3-R4`에서 active/inactive header에 numeric `characterId`를 복구하고 layout·Character focused unit으로 수정 완료를 확인했다.
### REV-P3-007 — Character URL이 계약 query `searchTerm` 대신 `search`를 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §7.1
- **관련 계약:** `GET /api/v2/admin/ai-characters` query `searchTerm`, `page`, `size`
- **소유 Task:** 신규 `P3-R5`
**관찰 내용**
수정 전 API adapter에는 `searchTerm`을 전달하지만 브라우저 URL parser와 serializer는 별도 `search` key를 사용했다. 따라서 계약 이름의 직접 링크 `?searchTerm=루나`는 검색 상태로 복원되지 않고 기존 테스트도 `?search=루나`를 정상 규칙으로 승인했다.
**근거**
- 문서: `prd.md:147-150`
- 코드: `src/features/characters/pages/CharacterListPage.tsx:26-43,55`
- 테스트: `src/features/characters/tests/character-list.test.tsx:50-66,103`
**재현 또는 검증 절차**
1. `/ai-characters?searchTerm=루나&page=1&size=20`으로 직접 진입한다.
2. 검색 input이 빈 값이고 API request의 `searchTerm`이 복원되지 않는지 확인한다.
3. UI에서 검색하면 URL이 `search=...`로 생성되는지 확인한다.
**영향 및 권장 수정 방향**
공유·새로고침 URL과 계약 추적성이 어긋나며 외부에서 생성한 정상 계약 deep link가 무시된다. URL read/write key만 `searchTerm`으로 맞추고 직접 진입·pagination 왕복 test를 추가한다.
**판정 기록:**
- 2026-07-31 — PRD·OpenAPI query 계약과 구현·테스트의 `search` URL key를 대조해 확정.
- 2026-07-31 — `P3-R5`에서 Character URL read/write key를 `searchTerm`으로 정렬하고 Character focused unit·정적 Gate로 수정 완료를 확인했다.
**판정 기록:** 2026-07-31 — PRD·OpenAPI·URL code/test를 대조해 확정.
### REV-P3-008 — Character 원본 image를 crop 적용 전에 제출할 수 있다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-007`, `FILE-009~010`, PRD §10.5
- **관련 계약:** Character create 필수 `image`, update optional `image` multipart part
- **소유 Task:** 신규 `P3-R6`
**관찰 내용**
생성·수정의 `selectImage`는 파일 선택 즉시 raw `File`을 committed `image` state에 넣은 뒤 crop source를 비동기로 준비한다. 생성 form은 준비·dialog 상태를 submit guard에 포함하지 않고 crop 취소도 raw `image`를 지우지 않는다. 따라서 원본 제출 또는 취소한 파일 제출이 가능하다. 수정도 source 준비가 끝나기 전 raw replacement를 보낼 수 있고 준비 reject·연속 선택의 stale resolution을 처리하지 않는다.
**근거**
- 문서: `prd.md:346-361`
- 코드: `src/features/characters/pages/CharacterCreatePage.tsx:63-92,103-127,165-180`
- 코드: `src/features/characters/pages/CharacterEditPage.tsx:78-109,120-145,184-205`
- 테스트가 승인하는 회귀: `src/features/characters/tests/CharacterCreatePage.test.tsx:147-170`은 crop 적용 없이 선택한 raw file이 multipart에 포함되는 것을 기대한다.
**재현 또는 검증 절차**
1. source 준비 Promise를 pending으로 둔 채 Character 생성 필수 text를 채우고 valid PNG를 선택한다.
2. `생성`을 누르면 crop 적용 전 raw PNG가 request에 포함될 수 있다.
3. source 준비 뒤 crop dialog에서 취소하고 생성해도 같은 raw file이 남는다.
4. 수정 화면에서도 느린 첫 선택과 빠른 둘째 선택을 교차 resolve하면 마지막 선택이 아닌 dialog가 열릴 수 있다.
**영향**
1:1·최대 800px 보장을 거치지 않은 원본이나 사용자가 취소한 파일이 서버에 저장될 수 있어 media 무결성과 운영자 의도를 직접 위반한다.
**권장 수정 방향**
raw selection과 crop 적용 결과를 분리하고 마지막 selection token, 준비 상태, reject 오류, 준비/dialog 중 submit guard를 둔다. create 취소는 새 선택을 제거하고 edit 취소는 기존 서버 media를 유지해야 한다.
**판정 기록:**
- 2026-07-31 — PRD file 정책, 비동기 state 전이, 현재 통과 test를 함께 대조해 확정.
- 2026-07-31 — `P3-R6`에서 create/edit committed image를 crop apply 결과로 한정하고 pending/cancel/stale/reject lifecycle test를 추가했다. `npm run test:run -- src/features/characters`는 6 files / 43 tests passed이고 `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 성공했다. E2E는 사용자 지시에 따라 최종 회귀 단계로 이연해 수정 완료 판정.
### plan·goal 전환 및 종료 판정
- `REV-P3-006``P3-R4`
- `REV-P3-007``P3-R5`
- `REV-P3-008``P3-R6`
- **최종 결론:** High 1건·Medium 2건 모두 수정 완료. Blocker 없음.
- **교차 QA:** 당시 Mobile Safari dirty-leave 실패는 focused 1회와 `--repeat-each=5`에서 모두 통과했고, 이후 지원 project 축소로 현재 Task에서 제외했다.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** Character 목록 query, 상세 workspace, create/update/deactivate payload, inactive read-only, image 정책·crop lifecycle을 `CHAR-*`, OpenAPI, `P3`와 대조했다.
- **실행 증거:** `npm run test:run -- src/features/characters` 6 files / 43 tests 통과. mock Chromium의 Character workspace·교차 resource·접근성 흐름도 전체 57 tests 안에서 통과했다.
- **판정:** `REV-P3-006~008` 수정 완료 상태가 유지되며 Phase 3 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Character mutation 수동 QA는 별도 Gate 상태다.
- **신규 Task:** 없음.
## 12. 종합 재점검 — 2026-07-31
- **검토 범위:** Character 목록·상세·생성·수정·비활성화, original work lookup, dirty-leave와 image 적용 lifecycle을 PRD `CHAR-*`·OpenAPI·현재 test에 재대조했다.
- **실행 증거:** Character를 포함한 도메인 묶음 36 files / 206 tests passed. 현재 mock Chromium·Mobile Chrome 전체 Gate 109 passed / 5 skipped에도 Character 흐름이 포함됐다.
- **판정:** Character endpoint·DTO·화면 소유의 확정 신규 발견 사항 없음.
- **교차 Phase:** Character도 영향을 받는 crop frame/좌표와 Blob URL 문제는 공용 컴포넌트 소유 `REV-P1-016~017`/`P1-R10~R11`에서 수정한다.
- **남은 위험:** 실제 개발 API Character mutation 수동 QA와 `P9-R7`의 Safari dirty-leave 재검증이 필요하다.
- **신규 Phase 3 Task:** 없음.
## 13. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** Character list/search/detail/create/update/deactivate, original work lookup, optional field serialization, crop lifecycle와 workspace capability.
- **실행 증거:** 관련 unit은 전체 실행에서 통과했고 mock 4-project matrix의 Character journey·axe·zoom·read-only 흐름도 0 failure였다. OpenAPI 2.3.0 parse·schema ref 점검도 통과했다.
- **판정:** Phase 3 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Character mutation·active-only 결과와 실기기 Safari QA는 자동 mock 검증과 분리된 수동 QA다.
- **신규 Phase 3 Task:** 없음.
## 14. 최종 재검증 — 2026-07-31
- **검토 범위:** Character list/search/detail/create/update/deactivate, original work lookup, optional field serialization, image lifecycle와 workspace capability.
- **실행 증거:** 전체 unit 394 tests와 4-project mock matrix의 Character·axe·320px/200% zoom 시나리오가 0 failure였고 OpenAPI schema ref 검사도 통과했다.
- **판정:** Phase 3 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Character mutation·active-only 결과와 실기기 Safari QA.
- **신규 Phase 3 Task:** 없음.
## 15. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** Character list/search/detail/create/update/deactivate, original work lookup, optional field serialization, inactive read-only과 image lifecycle를 `CHAR-001~018`·OpenAPI·`P3`에 대조했다.
- **실행 증거:** Character workspace·mutation을 포함한 실패 후보 5-spec focused는 5 files / 25 tests passed, server E2E는 36 passed, type·lint·build는 exit 0이었다. full unit 5 failure는 `REV-P9-009`/`P9-R9`로, WebKit direct-route navigation 경합은 동일 focused 통과 후 `REV-P9-010`/`P9-R10`으로, Character/Audio 이미지에도 영향을 주는 공용 crop frame은 `REV-P1-019`/`P1-R13`으로 분리했다.
- **판정:** Character endpoint·DTO·workspace 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 3 Task 없음.
- **남은 위험:** 실제 Character mutation·active-only 수동 QA와 공용 `P1-R13~R15`, `P9-R9~R10` 완료 후 회귀가 필요하다.

View File

@@ -0,0 +1,361 @@
# Phase 4 Audio Content 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 4 / Audio 목록·상세·생성·수정·soft delete·player |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 및 외부 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- Audio multipart DTO, 예약 UTC, 파일 정책, CRUD와 soft delete 불변식을 검증한다.
- schema·API adapter·contract test가 동일한 request 경계를 강제하는지 확인한다.
### 포함 범위
- 코드: `src/features/audio-contents`
- 테스트: Audio unit/contract/UI/E2E
- 문서: `AUDIO-001`~`AUDIO-033`, 관련 OpenAPI operation, `P4-*`
- 수동 검증: update/deactivate payload schema 정적 대조
### 제외 범위
- 실제 대용량 upload와 외부 CDN 재생 품질
- Comments 동작은 Phase 8에서 판정
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 데이터 mutation 계약 위반과 경계 test의 잘못된 허용을 우선한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUDIO-006`, `AUDIO-007`, `AUDIO-012`, `AUDIO-019`, `AUDIO-023`~`AUDIO-033`
- 계약: Audio POST/PUT multipart request schema
- 계획: `P4-T1`~`P4-GATE`
- 코드: `src/features/audio-contents/schemas/audio-content-schema.ts:35-44`
- 테스트: `src/features/audio-contents/tests/audio-contract.test.ts:159-202`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Vitest + Playwright 4 projects
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | Audio E2E 흐름은 통과; 전체 origin 실패는 `REV-P0-004` |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| Audio update schema·test 정적 대조 | 실패 | 일반 update에서 nullable boolean `isActive`, test는 `isActive:true`를 허용·기대 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-007` | Medium | 수정 완료 | 일반 Audio update schema와 contract test가 `isActive=true`를 허용한다 | `P4-R4` | `P4-R4` |
## 6. 발견 사항 상세
### REV-P4-007 — 일반 Audio update schema와 contract test가 `isActive=true`를 허용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-007`
- **관련 계약:** Audio PUT request의 optional `isActive`를 제품 정책상 soft delete로 제한
- **소유 Task:** 신규 `P4-R4`
**관찰 내용**
`audioContentUpdateRequestSchema``isActive`가 nullable boolean이라 `true`, `false`, `null`을 모두 허용한다. contract test는 일반 수정 request에 `isActive:true`를 넣고 그대로 전송되는 것을 성공 조건으로 고정한다. PRD는 일반 수정에서는 필드를 생략하고 soft delete 전용 request에서만 `false`를 보내며 `true`는 전송하지 않도록 명시한다.
**근거**
- 코드: `src/features/audio-contents/schemas/audio-content-schema.ts:35-44`
- 테스트: `src/features/audio-contents/tests/audio-contract.test.ts:159-202`
- 문서: `prd.md:225``AUDIO-007`
- 계약 해석: OpenAPI의 optional boolean 범위보다 제품 mutation 정책이 더 좁으며 별도 deactivate adapter가 이미 존재
**재현 또는 검증 절차**
1. `audioContentUpdateRequestSchema.parse({ isActive: true })`를 호출한다.
2. parsing이 성공하는 것을 확인한다.
3. 기존 contract test가 일반 update body의 `isActive:true`를 기대하는 것을 확인한다.
4. 요구 결과는 일반 update에서 `isActive` 자체를 거부하고 deactivate request만 `{ isActive:false }`를 허용하는 것이다.
**영향**
후속 UI·adapter 변경이 활성 복원 request를 잘못 보내도 schema와 contract test가 차단하지 못해 확정된 soft-delete-only 정책이 회귀할 수 있다.
**권장 조치**
일반 update schema에서 `isActive`를 제거하고 `{isActive:false}` strict schema를 deactivate 전용으로 분리한다. `true`, `null`, unknown field 거부와 두 payload의 정확한 직렬화를 test한다.
**판정 기록**
- 2026-07-30 — PRD, schema, adapter contract test의 상반된 허용 범위를 대조해 확정.
- 2026-07-30 — `P4-R4`에서 normal update schema/API/mock parser와 deactivate schema/API/mock parser를 분리해 수정 완료. 일반 update는 `isActive=true/null/false`를 거부하고 deactivate만 `{ isActive:false }`를 허용한다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P4-007``plan-task.md` 신규 `P4-R4`
- goal objective: `[P4-R4] Audio 일반 update와 soft delete schema를 분리해 isActive 전송 정책을 강제한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Audio code·test·contract 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P4-R4` |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료
**남은 항목:** Audio 실제 server integration은 기존 Gate 정책에 따라 mock UI 증거와 분리해 추적한다.
## 9. 수정 후 검증 기록
- 2026-07-30 — RED: `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts`에서 normal update의 `isActive:false`가 거부되지 않아 실패하는 것을 확인했다.
- 2026-07-30 — GREEN/focused: `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts` 결과 1 file / 10 tests passed.
- 2026-07-30 — 회귀: `npm run test:run -- src/features/audio-contents src/shared/mocks` 결과 14 files / 67 tests passed. 첫 병렬 실행에서 `audio-list.test.tsx`가 loading 상태로 timeout됐으나 단독 재현은 2 passed였고, 같은 전체 focused 명령 재실행은 14 files / 67 tests passed로 통과했다.
- 2026-07-30 — E2E/static: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 결과 21 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다.
- 2026-07-30 — LSP/size: `audio-content-schema.ts`, `audio-content-api.ts`, `audio-content-mock-store.ts`, `audio-content-handlers.ts`, `handlers.ts`, `audio-contract.test.ts` diagnostics는 오류 0건이었다. Audio handler 추출 후 `src/shared/mocks/handlers.ts`는 238 lines, `src/shared/mocks/audio-content-handlers.ts`는 133 lines다.
## 10. 2차 점검 결과 — 2026-07-30
- Phase 4 request/schema 자체의 신규 계약 위반은 없고 `REV-P4-007` 수정 완료 상태가 유지된다.
- Audio 비활성화 pending·실패 복구의 공통 결함은 중복 등록하지 않고 `REV-P9-004`/`P9-R4`에서 추적한다.
## 11. 2026-07-31 재점검
### 실행·판정 요약
- **범위:** Audio 목록 URL·API query, create/edit cover crop lifecycle, upload·deactivate를 PRD `AUDIO-001~033`, `FILE-006~009`와 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다.
- **신규 발견:** Medium 2건. 기존 `REV-P4-007`은 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-008` | Medium | 수정 완료 | Audio URL이 계약 query `search_word` 대신 `search`를 사용한다 | `P4-R5` | `P4-R5` |
| `REV-P4-009` | Medium | 수정 완료 | Audio cover crop 준비·연속 선택 상태가 저장 경계에 없다 | `P4-R6` | `P4-R6` |
### REV-P4-008 — Audio URL이 계약 query `search_word` 대신 `search`를 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §7.1
- **관련 계약:** Audio 목록 query `search_word`, `page`, `size`
- **소유 Task:** 신규 `P4-R5`
**관찰 내용과 근거**
브라우저 URL은 `search`를 읽고 쓰고 API 호출 직전에만 `search_word`로 이름을 바꾼다. `?search_word=루나` 직접 링크는 UI 검색 상태로 복원되지 않는다.
- 코드: `src/features/audio-contents/pages/AudioContentListPage.tsx:25-43,55-58`
- 테스트: `src/features/audio-contents/tests/audio-list.test.tsx:87-128``?search=루`를 정상 URL로 사용한다.
- 문서: `prd.md:147-150`
**재현 또는 검증 절차**
1. `/ai-characters/101/audio-contents?search_word=루나&page=1&size=20`에 직접 진입한다.
2. 검색 input과 request가 URL 검색어를 복원하지 않는지 확인한다.
3. UI 검색 뒤 URL이 `search=...`가 되는지 확인한다.
**영향 및 권장 수정 방향**
계약 이름으로 공유한 deep link가 무시되고 URL·API 추적성이 달라진다. URL parser/serializer key만 `search_word`로 맞추고 직접 진입·검색·pagination 왕복 test를 추가한다.
**판정 기록:**
- 2026-07-31 — PRD·OpenAPI·page/test 대조로 확정.
- 2026-07-31 — `P4-R5`에서 Audio list URL read/write key를 `search_word`로 교체했다. `?search_word=루나&page=1&size=20` 직접 진입은 input과 API request를 복원하고, 검색 변경 URL에는 `search`가 남지 않는다.
### REV-P4-009 — Audio cover crop 준비·연속 선택 상태가 저장 경계에 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-006~009`, PRD §10.5
- **소유 Task:** 신규 `P4-R6`
**관찰 내용과 근거**
`selectCoverImage``createCropSource`를 바로 await하지만 준비 상태, selection token, reject 오류를 관리하지 않는다. submit button은 audio upload 중에만 비활성화되며 source 준비 중에는 저장할 수 있다. create는 필수 cover validation으로 일부 차단되지만 edit는 새 선택 의도와 무관하게 기존 cover 유지 payload를 먼저 저장할 수 있고, 연속 선택은 느린 이전 Promise가 최신 dialog를 덮을 수 있다.
- 코드: `src/features/audio-contents/components/AudioContentForm.tsx:65-96,116-126,233-244`
- 문서: `prd.md:353-359,524-538`
- 비교 근거: Community form/sheet는 selection ID와 `isImagePreparing`을 사용해 동일 경계를 처리한다.
**재현 또는 검증 절차**
1. edit form에서 pending `createCropSource`를 주입하고 새 cover를 선택한다.
2. source가 resolve되기 전에 저장하면 새 cover 선택이 확정되지 않은 채 update가 진행된다.
3. 느린 첫 파일과 빠른 둘째 파일을 선택해 첫 Promise를 나중에 resolve하면 오래된 dialog가 열릴 수 있다.
4. Promise reject 시 사용자 오류 없이 unhandled rejection이 될 수 있다.
**영향 및 권장 수정 방향**
운영자가 선택한 새 cover가 누락되거나 이전 선택 dialog가 열려 잘못된 파일을 적용할 수 있다. committed crop result와 준비 상태를 분리하고 마지막 선택만 허용하며 준비/crop 중 submit을 막고 오류를 표시한다.
**판정 기록:**
- 2026-07-31 — 비동기 state 전이와 파일 정책을 대조해 확정.
- 2026-07-31 — `P4-R6`에서 Audio cover source 준비 상태와 selection token을 추가하고 준비/crop 중 submit을 차단했다. Promise reject는 inline 오류로 표시하고, crop 적용 결과만 `coverImage`에 commit하며 수정 취소는 기존 서버 cover 유지 계약을 보존한다.
### plan·goal 전환 및 종료 판정
- `REV-P4-008``P4-R5`
- `REV-P4-009``P4-R6`
- **최종 결론:** 신규 Medium 2건 수정 완료. Blocker/High 없음.
- **교차 QA:** 당시 Mobile Safari Audio 접근성 시나리오의 로그인 input 유실은 focused `--repeat-each=5`에서 5 passed였고, 이후 지원 project 축소로 현재 Task에서 제외했다.
## 12. 최종 Phase별 점검 — 2026-07-31
### 실행 결과
| 명령 또는 검증 | 결과 | 판정 |
|---|---|---|
| `npm run test:run -- src/features/audio-contents` | 8 files / 53 tests passed | 성공 |
| `npm run e2e:mock -- --project=chromium` | 전체 57 tests passed, Audio 6 scenarios 포함 | 성공 |
| `npm run typecheck` / `npm run lint` / `npm run build` | 모두 exit 0 | 성공 |
| PRD file flow와 Audio cover selection 정적 대조 | crop 전 원본 validator 호출 없음 | 실패, 신규 finding 확정 |
병렬로 네 개 Vitest process를 실행한 첫 Audio focused run에서는 `audio-list.test.tsx` 1건이 로딩 대기에서 timeout됐으나, 해당 파일 단독 2/2와 Audio 전체 단독 53/53이 통과했다. 같은 양상이 기존 §9·`P4-R5` 기록에도 있어 이번 계약 finding과 분리했고 신규 제품 결함으로 등록하지 않았다.
### 신규 발견 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-010` | Medium | 수정 완료 | Audio cover가 원본 파일 정책 검증 전에 crop 준비로 진입한다 | `P4-R7` | `P4-R7` |
### REV-P4-010 — Audio cover가 원본 파일 정책 검증 전에 crop 준비로 진입한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-001~003`, `FILE-006~007`, PRD `Image crop UI 흐름` 1~3단계
- **관련 계약:** cover JPEG/PNG, exact max `10,485,760 bytes`, extension/MIME pair
- **소유 Task:** 신규 `P4-R7`
**관찰 내용**
`selectCoverImage`는 새 파일을 선택하면 `validateAudioCoverFile`을 호출하지 않고 바로 `createCropSource(file)`을 실행한다. 파일 정책 검증은 crop 적용 결과가 `coverImage`에 저장된 뒤 form submit validation에서만 일어난다. 따라서 `10,485,761 bytes` 원본, GIF, 확장자/MIME 불일치 파일도 먼저 crop source 생성과 Dialog 경로로 들어간다.
**근거**
- 문서: `prd.md:350-356,382-389`
- 코드: `src/features/audio-contents/components/AudioContentForm.tsx:83-109,112-117`
- validator: `src/features/audio-contents/validation/audio-cover-policy.ts:7-27`
- 테스트 공백: `audio-contract.test.ts:216-227`은 validator 단위 계약만 확인하고, Audio form tests에는 invalid 원본이 `createCropSource`를 호출하지 않는 경계가 없다.
- 비교 근거: `SeriesForm`은 동일 단계에서 `validateSeriesImageFile` 실패 시 crop source 호출 전에 반환한다.
**재현 또는 검증 절차**
1. Audio 생성·수정 form에 호출 횟수를 기록하는 `createCropSource`를 주입한다.
2. `10,485,761 bytes` PNG 또는 `.png`/`image/jpeg` 파일을 cover로 선택한다.
3. 현재 구현에서 `createCropSource`가 1회 호출되는 것을 확인한다.
4. 요구 결과는 호출 0회, crop Dialog 0개, 즉시 inline 정책 오류다.
**영향과 권장 조치**
부적합 원본이 불필요한 decode/canvas 작업에 들어가며, 큰 원본이 crop 결과 크기만 작아져 제출 검증을 통과하면 “선택 직후 원본 상한 검증” 계약을 우회한다. 기존 validator와 오류 메시지를 재사용해 crop source 생성 전에 반환하고 create/edit 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — PRD의 명시적 처리 순서, Audio component와 validator·form tests를 대조해 확정.
- 2026-07-31 — `plan-task.md`에 미완료 신규 Task `P4-R7`로 전환. 애플리케이션 코드는 이번 리뷰 범위에서 수정하지 않았다.
- 2026-07-31 — `P4-R7`에서 `selectCoverImage`가 기존 cover validator를 crop source 준비 전에 재사용하도록 수정했다. RED focused는 2 failed / 18 passed, GREEN focused는 2 files / 20 tests passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, Audio directory LSP diagnostics는 통과했다. 개발 중 E2E는 사용자 지시에 따라 보류했다.
### plan·goal 전환 및 종료 판정
- `REV-P4-010``plan-task.md` 신규 `P4-R7`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **남은 항목:** 기존 실제 개발 API Audio 수동 QA.
## 13. 종합 재점검 — 2026-07-31
- **검토 범위:** Audio 목록·상세·multipart 생성/수정·UTC 예약·soft delete·player·Comments 연결을 OpenAPI와 PRD `AUDIO-*`에 재대조했다.
- **실행 증거:** Audio를 포함한 도메인 묶음 36 files / 206 tests passed. exact server E2E 18/18과 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** `REV-P4-010`/`P4-R7` 수정 완료 상태가 유지되며 Audio 도메인 소유의 확정 신규 finding은 없다.
- **교차 Phase:** Audio cover에 영향을 주는 공용 crop 결함은 `REV-P1-016~017`/`P1-R10~R11`로만 추적한다.
- **남은 위험:** 실제 개발 API upload/예약/soft delete 수동 QA와 Safari Gate 복원이 필요하다.
- **신규 Phase 4 Task:** 없음.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** Audio list/detail/create/update/upload/player, UTC release, price·file policy, cover crop와 Comments 연결.
- **실행 증거:** 관련 unit, `typecheck`·`lint`·build와 4-project mock Audio journey가 0 failure였고 server allowlist 36 tests도 통과했다.
- **판정:** `P4-R7`까지의 수정 완료 상태가 유지되며 Phase 4 소유의 신규 기능 결함은 없다.
- **문서 교차 항목:** 상단의 “교차 회귀 대기”와 이미 완료된 공통 crop 수정 이력 불일치는 `REV-P10-008`/`P10-R7`에서 정리한다.
- **남은 위험:** 실제 개발 API upload/예약/soft delete와 서버 파일 거부 수동 QA.
## 15. 최종 재검증 및 신규 판정 — 2026-07-31
### 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-011` | Medium | 수정 완료 | 완료된 network 실패를 뒤늦게 취소하면 upload 정산이 이중 실행되어 다음 session의 401을 무시한다 | `P4-R8` | `P4-R8` |
### REV-P4-011 — 완료된 network 실패를 뒤늦게 취소하면 upload 정산이 이중 실행되어 다음 session의 401을 무시한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-017`, `AUTH-005`, PRD §13의 upload 취소·재시도와 session 401 복구
- **관련 계약:** Audio multipart upload와 HTTP 401 공통 처리. OpenAPI endpoint·DTO 변경 없음.
- **소유 Task:** 신규 `P4-R8`
**관찰 내용**
`uploadAudioContent`의 resolve/reject helper는 호출될 때마다 보호 upload count를 감소시킨다. `onload`만 AbortSignal listener를 제거하고 `onerror`는 제거하지 않으므로, network error로 Promise가 이미 reject된 뒤 같은 signal이 abort되면 reject/정산이 다시 실행된다. count가 음수가 되고 authentication-expiry latch가 다음 login session까지 true로 남을 수 있다.
**근거**
- 코드: `src/features/audio-contents/api/upload-audio-content.ts:68-80`은 settle guard 없이 모든 reject에서 count를 감소시키고, `:96-100`은 listener cleanup을 `onload`에만 둔다. listener는 `:132`에서 등록된다.
- 테스트: `src/features/audio-contents/tests/audio-upload.test.ts:123-166`의 cancel/retry와 `:191-228`의 401 burst는 각각 독립 경로만 검증한다.
- 문서: `AUDIO-017`, `AUTH-005`, 기존 `P4-R2`는 취소·재시도 이후에도 session expiry 흐름을 보존해야 한다.
**재현 또는 검증 절차**
1. signal-bearing protected upload를 network error로 먼저 종료한 뒤 같은 `AbortController`를 abort한다.
2. 첫 session의 upload를 401로 종료해 만료 callback 1회를 확인하고, 새 token을 설정한다.
3. 새 session의 upload도 401로 종료해 callback 총 2회를 기대하는 임시 진단 test를 focused 실행한다.
4. 실제 결과는 exit 1, 1 failed였고 `clearSession` 기대 2회 대비 실제 1회였다. 진단 코드는 판정 후 제거해 제품 test 변경을 남기지 않았다.
**영향**
network error와 cancel이 인접한 제한 조건 뒤에는 새로 로그인해도 다음 upload 401에서 session 제거·login 이동이 실행되지 않을 수 있다. 업로드 오류 자체는 표시되지만 인증 만료 복구가 누락된다.
**권장 조치**
요청별 settled guard와 공통 terminal cleanup으로 Promise settle, count 감소와 AbortSignal listener 해제를 정확히 한 번만 수행한다. network error→late abort→첫/새 session 401 순서를 회귀 test로 고정하고 새 upload abstraction은 만들지 않는다.
**판정 기록**
- 2026-07-31 — terminal handler data flow를 추적하고 임시 진단 test에서 새 session callback 누락을 재현해 Medium 확정.
- 2026-07-31 — `plan-task.md` 신규 `P4-R8`로 전환하고 `P1-R12` 완료 뒤 실행하도록 의존성을 기록. 애플리케이션 코드는 수정하지 않음.
- 2026-07-31 — `P4-R8`에서 요청별 settled guard와 공통 terminal cleanup을 추가해 수정 완료. reviewer blocker로 fetch-first 순서와 주입 auth authoritative token 처리를 보강했다. upload focused 2 files / 13 tests, Audio feature 9 files / 58 tests, auth/app/shared 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, 개발/운영 build, LSP diagnostics가 통과했다.
### 종료 판정
- **자동 검증:** `P4-R8` focused 2 files / 13 tests, Audio feature 9 files / 58 tests, 교차 auth focused 3 files / 22 tests, `src/app src/features/auth src/shared/api` 포함 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check`, 변경 파일 LSP diagnostics가 통과했다. E2E는 사용자 지시에 따라 반복 실행하지 않았다.
- **최종 결론:** `REV-P4-011` 수정 완료. Blocker/High 없음.
- **남은 항목:** 실제 개발 API upload/예약/soft delete·서버 파일 거부 수동 QA.
## 16. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** Audio list/detail/player/create/update/deactivate, XHR upload·cancel·auth lifecycle, UTC reservation, cover/audio file policy를 `AUDIO-001~033`·OpenAPI·`P4`·`P10-T2`에 대조했다.
- **실행 증거:** Audio contract/upload/auth lifecycle test는 full output에서 통과했고, type·lint·개발/운영 build·server E2E 36 tests도 통과했다. full unit 5 failure는 `REV-P9-009`/`P9-R9`, cover crop frame은 공용 `REV-P1-019`/`P1-R13`으로 분리했다.
- **판정:** Audio endpoint·DTO·upload transport 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 4 Task 없음.
- **남은 위험:** 실제 upload·container/codec·예약·soft delete 수동 QA와 `P1-R13`, `P9-R9` 회귀가 남는다.

View File

@@ -0,0 +1,283 @@
# Phase 5 Series Management 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 5 / Series CRUD, 장르, 콘텐츠 연결·해제·순서 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 신규 회귀 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Series CRUD·lookup·연결·순서 payload와 responsive UI가 요구사항·계약에 맞는지 검증한다.
- API 원본 enum/ID와 사용자 표시 label의 경계가 일관적인지 확인한다.
### 포함 범위
- 코드: `src/features/series`
- 테스트: Series contract/form/route/order/E2E
- 문서: `SERIES-001`~`SERIES-018`, 관련 OpenAPI operation, `P5-*`
- 수동 검증: list/card/detail에서 같은 DTO의 표시 결과 정적 대조
### 제외 범위
- 실제 서버의 전체 page 순서 저장 fixture
- Phase 0 origin 문제로 실행되지 못한 server E2E의 중복 판정
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 계약 payload 무결성과 운영자에게 노출되는 label 일관성을 중점 확인했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `SERIES-002`, `SERIES-005`, `SERIES-007`, `SERIES-015`, PRD §13
- 계약: Series list/detail item, genre lookup, CRUD·link·unlink·order operations
- 계획: `P5-T1`~`P5-GATE`
- 코드: `SeriesList.tsx:8-38`, `SeriesListItem.tsx:5-28`, `SeriesSummary.tsx:3-35`
- 테스트: `series-routes.test.tsx:147` 등 raw label 기대
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright 4 projects, 320/768/1280px
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | Series 전체 흐름은 WebKit 768px 1회 timeout 외 통과 |
| `npm run e2e:mock -- tests/e2e/series.spec.ts --project=webkit --grep "desktop and tablet Series management flow remains available at 768px"` | 성공 | 1 passed, 12.6s; 결함으로 확정하지 않음 |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| 표시 label 정적 대조 | 실패 | 목록·card는 raw day/state/genreId/boolean, 상세도 raw genreId/boolean |
| 생성·수정 schema 2차 계약 대조 | 실패 | 누락 `genreId`와 중복 `publishedDaysOfWeek`가 parsing·mock create에서 허용됨 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P5-004` | Medium | 수정 완료 | Series 목록·card·상세가 enum·genreId·boolean 원본을 운영자에게 노출한다 | `P5-R3` | `P5-R3` |
| `REV-P5-005` | Medium | 수정 완료 | Series schema와 mock이 필수 장르·요일 유일성 불변식을 강제하지 않는다 | `P5-R4` | `P5-R4` |
## 6. 발견 사항 상세
### REV-P5-004 — Series 목록·card·상세가 enum·genreId·boolean 원본을 운영자에게 노출한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `SERIES-002`, `SERIES-005`, `SERIES-007`, `SERIES-015`, PRD §13
- **관련 계약:** Series 원본 enum/ID는 수정 payload에 유지
- **소유 Task:** 신규 `P5-R3`
**관찰 내용**
desktop list와 mobile card는 요일·state enum을 그대로 표시하고 `genreId 77`, `isAdult false`, `isActive true` 같은 내부 필드명을 출력한다. 상세는 요일·state만 변환하며 `genreId`와 boolean은 그대로 표시한다. 같은 DTO가 surface마다 서로 다른 label 정책을 사용한다.
**근거**
- 코드: `src/features/series/components/SeriesList.tsx:8-38`
- 코드: `src/features/series/components/SeriesListItem.tsx:5-28`
- 코드: `src/features/series/components/SeriesSummary.tsx:3-35`
- 테스트: `src/features/series/tests/series-routes.test.tsx:147``genreId 77` 노출을 기대
- 문서: `prd.md:283` `SERIES-015`, `prd.md:806`
**재현 또는 검증 절차**
1. Series 목록을 desktop과 mobile width에서 연다.
2. 요일 `SUN`/`RANDOM`, 상태 `PROCEEDING`, `genreId`, `isAdult false`가 표시되는 것을 확인한다.
3. 상세에서는 일부만 한글 label로 바뀌어 surface 간 표현이 다른 것을 확인한다.
4. 요구 결과는 API 원본을 payload에만 유지하고 표시에는 장르명·요일·상태·boolean label을 일관되게 사용하는 것이다.
**영향**
운영자가 API 내부 표현을 해석해야 하고, desktop/mobile/detail 사이에서 같은 시리즈 상태가 다르게 보인다.
**권장 조치**
장르 lookup map과 중앙 Series formatter를 사용해 모든 surface의 표시를 통일하되 form 초기화·update payload에는 기존 원본 enum과 ID를 보존한다.
**판정 기록**
- 2026-07-30 — PRD 표시 경계와 세 UI surface 및 고정 test를 대조해 확정.
- 2026-07-30 — `P5-R3` 구현과 fallback/CJK/header 보완 후 focused unit, full Series mock E2E, static gate, 수동 확인, 독립 visual QA 최종 A/B PASS로 수정 완료 판정.
### REV-P5-005 — Series schema와 mock이 필수 장르·요일 유일성 불변식을 강제하지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `SERIES-006`, `SERIES-007`
- **관련 계약:** `SeriesCreateRequest.publishedDaysOfWeek.uniqueItems=true`, `genreId=0`은 domain에서 무효
- **소유 Task:** 신규 `P5-R4`
**관찰 내용**
`seriesCreateRequestSchema``genreId`가 optional이고 공용 `publishedDaysSchema`에는 중복 검사가 없다. 따라서 생성 schema는 장르가 없는 payload와 `["MON","MON"]`을 모두 허용한다. mock create는 장르가 없으면 첫 fixture 장르를 임의로 넣어 잘못된 request를 성공 처리한다. 화면 form은 장르 선택과 요일 toggle을 제한하지만 API adapter·mock 경계는 같은 불변식을 보장하지 않는다.
**근거**
- 코드: `src/features/series/schemas/series-schema.ts:15-26`
- mock: `src/shared/mocks/series-mock-store.ts:48-55`
- 실행 재현: `seriesCreateRequestSchema.safeParse()` 결과 누락 장르 `true`, 중복 요일 `true`
- 문서: `prd.md:274-275`
- 계약: OpenAPI `SeriesCreateRequest.publishedDaysOfWeek.uniqueItems=true`, `genreId` 설명의 domain validation
**재현 또는 검증 절차**
1. `{title,introduction,publishedDaysOfWeek:["MON"],keyword}`를 create schema에 넣으면 성공한다.
2. `genreId:1`, `publishedDaysOfWeek:["MON","MON"]`을 넣어도 성공한다.
3. 같은 누락 장르 multipart를 mock POST에 보내면 mock store가 첫 장르로 보정해 생성한다.
4. 요구 결과는 누락/0 장르와 중복 요일을 request 경계에서 거부하는 것이다.
**영향**
현재 form 경로 밖의 adapter 호출이나 회귀 test가 계약 밖 Series를 전송할 수 있고, mock preview가 이를 정상 생성으로 숨긴다. update schema도 같은 요일 schema를 재사용하므로 중복 요일을 허용한다.
**권장 조치**
create `genreId`를 positive integer 필수로 만들고 공용 요일 schema에 uniqueness를 추가한다. mock의 첫 장르 fallback을 제거하고 schema·contract test에서 invalid request를 고정한다.
**판정 기록**
- 2026-07-30 — PRD·OpenAPI와 schema 실행 결과·mock fallback을 대조해 확정.
- 2026-07-30 — `P5-R4`에서 생성 `genreId` 필수와 생성·수정 요일 유일성을 schema에 적용하고 mock의 첫 장르 fallback을 제거했다. focused Series 단위 회귀와 정적 Gate로 수정 완료를 확인했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P5-004``plan-task.md` 신규 `P5-R3`
- goal objective: `[P5-R3] Series 표시 label을 중앙화하고 API 원본값과 UI 표현의 경계를 복구한다.`
- `REV-P5-005``plan-task.md` 신규 `P5-R4`
- goal objective: `[P5-R4] Series 필수 장르와 요일 유일성을 schema·mock 경계에서 복구한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Series 코드·test·계약 대조 |
| 후보 항목 판정 완료 | 충족 | 기존 1건 수정 완료, 신규 1건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P5-R3`, `P5-R4` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료.
**남은 항목:** Series 일반 mutation pending 경계는 `REV-P9-004`/`P9-R4`, 실제 server integration은 기존 수동 QA 정책으로 분리한다.
## 9. 수정 후 검증 기록
- 2026-07-30 — RED: `src/features/series/tests/series-routes.test.tsx``tests/e2e/series.spec.ts`에 desktop/mobile 목록·상세의 raw enum·boolean·field-name 노출 금지 assertion을 추가했다. 기존 raw 표시 기대를 운영자용 label 기대와 no-raw assertion으로 바꾸는 과정에서 `REV-P5-004` 재현 조건을 고정했다.
- 2026-07-30 — GREEN/REFACTOR: `src/features/series/lib/series-display-labels.ts`를 추가해 요일, state, genre, 성인 여부, 노출 상태 label을 중앙화하고 `SeriesList`, `SeriesListItem`, `SeriesSummary`가 이를 사용하도록 정리했다. `SeriesListPage``SeriesDetailPage`는 장르 lookup을 함께 불러와 표시 계층에 전달하며, form·mutation payload는 기존 원본 enum/ID를 유지한다.
- 2026-07-30 — 독립 리뷰 1차: code review는 PASS, visual QA B는 PASS였다. visual QA A는 `formatSeriesGenre()``장르 #id` fallback이 lookup 실패 시 내부 ID를 표시할 수 있다고 지적해 REVISE를 반환했다.
- 2026-07-30 — fallback 보완: `formatSeriesGenre()` fallback을 `알 수 없는 장르`로 바꾸고, 장르 lookup miss에서 `장르 #`, `genreId`, `77`이 표시되지 않는 route 회귀 test를 추가했다.
- 2026-07-30 — focused unit: `npm run test:run -- src/features/series/tests/series-routes.test.tsx` 결과 1 file / 5 tests passed. `npm run test:run -- src/features/series` 결과 7 files / 29 tests passed.
- 2026-07-30 — E2E/static: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium` 결과 7 passed. fallback 수정 후 `npm run e2e:mock -- tests/e2e/series.spec.ts` 결과 25 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다.
- 2026-07-30 — 수동 확인: mock mode에서 1280px desktop list/detail과 320px mobile list/detail fresh capture 및 DOM raw leak check를 수행했고 `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #/`는 네 화면 모두 false였다. 사용자가 이 문서의 재현 및 검증 절차를 보고 수동 확인했다.
- 2026-07-30 — visual recheck B 보완: 모바일 안내문·카드 소개문의 CJK 중간 줄바꿈을 막기 위해 `break-keep`을 적용하고, 공통 workspace header의 visible `characterId:` 표시를 제거했다. `CharacterWorkspaceLayout.test.tsx`는 header의 `characterId:` 미노출을 확인한다.
- 2026-07-30 — 최종 검증: `npm run test:run -- --fileParallelism=false src/layouts/CharacterWorkspaceLayout.test.tsx src/features/series/tests/series-routes.test.tsx` 결과 2 files / 10 tests passed. `npm run test:run -- src/features/series` 결과 7 files / 29 tests passed. `npm run e2e:mock -- tests/e2e/series.spec.ts` 결과 25 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 통과했다.
- 2026-07-30 — `P5-R4` RED: `npm run test:run -- src/features/series/tests/series-contract.test.ts` 결과 1 failed / 6 passed로 `genreId` 누락 create payload 허용을 재현했다.
- 2026-07-30 — `P5-R4` GREEN/회귀: `seriesCreateRequestSchema``genreId`를 필수 positive integer로 바꾸고 공용 요일 schema에 uniqueness를 추가했으며, mock create의 첫 장르 fallback을 제거했다. `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx` 결과 2 files / 14 tests passed, `npm run test:run -- src/features/series src/shared/mocks` 결과 13 files / 51 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. 개발 중 E2E는 사용자 지시에 따라 `P10-R5` 이후 최종 E2E로 미뤘다.
- 2026-07-30 — 최종 visual QA: 1280px desktop list/detail, 320px mobile list/detail fresh capture에서 `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #|characterId:/` 누출 0건을 확인했다. 독립 visual QA 최종 A와 B는 모두 PASS, BLOCKING 없음이었다.
- 2026-07-30 — 2차 계약 점검에서 누락 `genreId`와 중복 요일의 schema parse 성공 및 mock 첫 장르 fallback을 재현했다. 애플리케이션 코드는 수정하지 않고 `P5-R4`로 전환했다.
## 10. 2026-07-31 재점검
### 실행·판정 요약
- **범위:** Series CRUD·장르·요일·연결·순서·image lifecycle을 PRD `SERIES-001~018`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`와 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다.
- **신규 발견:** Medium 1건. 기존 `REV-P5-004~005`는 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P5-006` | Medium | 수정 완료 | Series image crop 준비·연속 선택 상태가 저장 경계에 없다 | `P5-R5` | `P5-R5` |
### REV-P5-006 — Series image crop 준비·연속 선택 상태가 저장 경계에 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD §10.5
- **소유 Task:** 신규 `P5-R5`
**관찰 내용**
Series form은 유효 파일을 committed `image`로 바로 넣지는 않지만 `createCropSource` 준비 상태와 마지막 선택 token, reject 오류를 관리하지 않는다. submit은 dialog가 열린 뒤에는 막지만 Promise가 pending인 동안에는 가능하다. edit는 새 image 선택 의도가 반영되지 않은 update를 먼저 보낼 수 있고 이전 느린 선택이 최신 dialog를 덮을 수 있다.
**근거**
- 코드: `src/features/series/components/SeriesForm.tsx:80-116,129-151,198-206`
- 문서: `prd.md:354,356-361,524-538`
- 테스트 공백: crop 적용·취소는 검증하지만 pending source, reject, 선택 순서 역전은 고정하지 않는다.
**재현 또는 검증 절차**
1. edit form에 pending `createCropSource`를 주입하고 새 image를 선택한다.
2. Promise resolve 전에 저장해 새 image 없는 update가 진행되는지 확인한다.
3. 느린 첫 파일과 빠른 둘째 파일을 선택한 뒤 첫 Promise를 마지막에 resolve해 stale dialog가 열리는지 확인한다.
**영향**
사용자가 교체를 선택했지만 기존 image가 유지되거나 잘못된 선택의 crop이 적용될 수 있다. 준비 실패도 화면에서 복구할 수 없다.
**권장 수정 방향**
Series form 내부에 selection token·준비 상태·inline 오류를 추가하고 준비/crop 중 submit을 비활성화·guard한다. crop 결과와 기존 서버 image 유지 계약은 그대로 둔다.
**판정 기록:**
- 2026-07-31 — 비동기 state와 파일 정책을 대조해 확정.
- 2026-07-31 — `P5-R5`에서 Series image source 준비 상태와 selection token을 추가하고 준비/crop 중 submit을 차단했다. Promise reject는 inline 오류로 표시하고, crop 적용 결과만 `image`에 commit하며 수정 취소는 기존 서버 image 유지 계약을 보존한다.
### plan·goal 전환 및 종료 판정
- `REV-P5-006``plan-task.md` 신규 `P5-R5`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **교차 Phase:** 2026-07-30 visual QA에서 제거한 workspace `characterId`는 Series 표시 문제가 아니라 PRD §7.2 위반이므로 `REV-P3-006`/`P3-R4`가 복구를 소유한다.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** Series lookup·CRUD·image crop·연결/해제·전체 순서와 OpenAPI multipart/JSON payload를 `P5`에 재대조했다.
- **실행 증거:** `npm run test:run -- src/features/series` 8 files / 37 tests, mock Chromium Series 시나리오 7 tests가 통과했다.
- **판정:** Series 도메인 계약의 별도 신규 finding은 없다. 다만 create/update submit의 동기 재진입 guard와 저장 진행 표시 공백은 교차 품질 finding `REV-P9-006`/`P9-R6`에 귀속했다.
- **남은 위험:** 실제 개발 API Series 수동 QA와 `P9-R6` 완료가 필요하다.
- **신규 Phase 5 Task:** 없음. 중복 Task 대신 `P9-R6`에서 추적한다.
## 12. 종합 재점검 — 2026-07-31
- **검토 범위:** Series genre·CRUD·210:297 image·contents 연결/해제·전체 순서 저장과 일반 mutation pending 상태를 재대조했다.
- **실행 증거:** Series를 포함한 도메인 묶음 36 files / 206 tests, exact server E2E 18/18, 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** `P9-R6`까지 포함한 Series 도메인 소유의 확정 신규 finding은 없다.
- **교차 Phase:** Series image frame·좌표와 Blob URL 수명은 `REV-P1-016~017`/`P1-R10~R11`에서 수정한다.
- **남은 위험:** 실제 개발 API Series mutation 수동 QA가 남아 있다. Safari/WebKit은 현재 지원 범위에서 제외한다.
- **신규 Phase 5 Task:** 없음.
## 13. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** genre lookup, CRUD, 210:297 crop, contents 연결·해제·전체 순서, 표시 label과 mutation pending.
- **실행 증거:** Series unit과 mock 4-project CRUD·responsive·axe·order journey가 0 failure였고 OpenAPI `keyword`·`genreId`·`contentIdList`·`ids` 계약 대조도 통과했다.
- **판정:** Phase 5 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Series active-only·CRUD·연결·순서와 실기기 browser 수동 QA.
- **신규 Phase 5 Task:** 없음.
## 14. 최종 재검증 — 2026-07-31
- **검토 범위:** Series genre·CRUD, crop, contents 연결/해제, 전체 순서와 mutation pending/error 상태.
- **실행 증거:** 전체 unit 394 tests와 4-project mock Series·responsive·axe·order 시나리오가 0 failure였고 OpenAPI request/response schema 대조도 통과했다.
- **판정:** Phase 5 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Series active-only·CRUD·연결·순서와 실기기 browser 수동 QA.
- **신규 Phase 5 Task:** 없음.
## 15. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** Series genre lookup·CRUD·`210:297` crop·contents 연결/해제·전체 순서·pending/error를 `SERIES-001~018`·OpenAPI·`P5`·`P10-T3`에 대조했다.
- **실행 증거:** Series route를 포함한 실패 후보 5-spec focused는 25 tests passed, Series contract·form·order·contents test는 full output에서 통과했다. type·lint·build·server E2E 36 tests도 통과했다. full unit 비결정성은 `REV-P9-009`/`P9-R9`, crop frame은 `REV-P1-019`/`P1-R13`으로 분리했다.
- **판정:** Series 도메인 계약·UI 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 5 Task 없음.
- **남은 위험:** 실제 Series active-only·CRUD·연결·순서 수동 QA와 `P1-R13`, `P9-R9` 회귀가 남는다.

View File

@@ -0,0 +1,291 @@
# Phase 6 Community Posts 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 6 / Community 목록·생성·Sheet 수정·삭제 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, `REV-P6-004` 수정 완료 및 외부 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- Community pagination, multipart mutation, mobile read-only capability와 Sheet 흐름을 검증한다.
- 목록 상태 label이 운영자 표시 규칙과 일치하는지 확인한다.
### 포함 범위
- 코드: `src/features/community-posts`
- 테스트: Community contract/form/list/sheet/E2E
- 문서: Community 요구사항, PRD §10·§13, `P6-*`
- 수동 검증: desktop list의 상태 표시 정적 대조
### 제외 범위
- Community Comments thread는 Phase 8에서 판정
- 외부 서버의 media upload·audio playback
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 기능·계약 위반을 우선하고 비핵심 표시 문자열은 Low로 판정한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: Community 요구사항과 PRD §13의 중앙 formatter 규칙
- 계약: Community list/create/update/delete
- 계획: `P6-T1`~`P6-GATE`
- 코드: `src/features/community-posts/components/CommunityPostList.tsx:22-30`
- 테스트: `src/features/community-posts/tests/community-list.test.tsx:84-95`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright 4 projects, mobile read-only와 768/1280px 관리 흐름 포함
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | Community 관련 실행 항목 통과 또는 프로젝트 정책상 skip; 전체 origin 실패는 `REV-P0-004` |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| Community list label 정적 대조 | 실패 | `성인 false/true` 원시 boolean 노출, unit test가 문자열 고정 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P6-002` | Low | 수정 완료 | Community desktop 목록이 성인 여부를 `false/true`로 표시한다 | `P6-R2` | `P6-R2` |
## 6. 발견 사항 상세
### REV-P6-002 — Community desktop 목록이 성인 여부를 `false/true`로 표시한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** PRD §13 표시 formatter 일관성
- **관련 계약:** Community list item의 `isAdult` boolean
- **소유 Task:** 신규 `P6-R2`
**관찰 내용**
desktop table의 플래그 열은 고정·댓글 여부는 한글 label로 변환하지만 성인 여부만 `String(post.isAdult)`로 출력한다. unit test도 “성인 false” 문구를 성공 조건으로 고정한다.
**근거**
- 코드: `src/features/community-posts/components/CommunityPostList.tsx:29`
- 테스트: `src/features/community-posts/tests/community-list.test.tsx:93`
- 문서: `prd.md:806`
**재현 또는 검증 절차**
1. Community 목록을 desktop 폭에서 연다.
2. 일반 게시글의 상태 열에서 “성인 false”를 확인한다.
3. 성인 콘텐츠에서는 “성인 true”가 표시됨을 코드로 확인한다.
4. 요구 결과는 “일반 콘텐츠/성인 콘텐츠”처럼 운영자용 label을 표시하는 것이다.
**영향**
비핵심 플래그이지만 화면의 다른 한글 상태와 어긋나며 운영자가 내부 boolean을 해석해야 한다.
**권장 조치**
공통 boolean label 또는 Community formatter로 변환하고 unit/E2E assertion을 사용자 문구 기준으로 갱신한다.
**판정 기록**
- 2026-07-30 — 구현과 현재 성공 test가 원시 boolean을 의도적으로 노출함을 확인해 확정.
- 2026-07-30 — `P6-R2`에서 Community 전용 status label helper를 row/card에 적용하고 unit·mock E2E로 raw `성인 true/false` 비노출을 확인해 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P6-002``plan-task.md` 신규 `P6-R2`
- goal objective: `[P6-R2] Community 상태 boolean을 운영자용 label로 표시한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Community code·test·계약 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P6-R2` |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4, §9 |
**최종 결론:** 수정 검증 완료
**남은 항목:** `REV-P6-002`/`P6-R2` 범위 없음. P6-GATE server mode integration은 기존 외부 의존·server-mode 대상 spec 정합성 항목으로 별도 유지한다.
## 9. 수정 후 검증 기록
### 2026-07-30 — `P6-R2` 수정 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/community-posts` | 성공 | 5 files, 42 tests passed. Unit test가 desktop/mobile status label 2건과 raw `성인 false` 비노출을 검증한다. |
| `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium` | 성공 | 7 passed. Mobile card와 768/1280px desktop table에서 `일반 콘텐츠`/`성인 콘텐츠` label 및 raw `성인 true/false` 비노출을 검증한다. |
| `npm run e2e:mock -- tests/e2e/community.spec.ts` | 성공 | 24 passed / 4 skipped. Skipped 항목은 기존 Chromium audio metadata 한정과 WebKit range input focus 제외 정책이다. |
| LSP diagnostics | 성공 | `src/features/community-posts` directory scan 10 files, 0 diagnostics. `tests/e2e/community.spec.ts`도 0 diagnostics. |
**판정:** `REV-P6-002`는 수정 완료. API schema, request payload, mock mutation shape 변경은 없다.
## 10. 2차 점검 결과 — 2026-07-30
- 전체 unit 72 files / 358 tests와 mock Chromium matrix 57 tests에서 Community 흐름이 통과했다.
- Phase 6 신규 발견은 없으며 `REV-P6-002` 수정 완료 상태가 유지된다.
## 11. 2026-07-31 재점검
### 실행·판정 요약
- **범위:** Community 목록·Sheet·create/edit/fix/deactivate·media·Comments 결합을 PRD `COMMUNITY-001~015`, §10.4와 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다.
- **신규 발견:** Medium 1건. 기존 `REV-P6-002`는 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P6-003` | Medium | 수정 완료 | Community 게시글 비활성화가 확인 없이 즉시 실행된다 | `P6-R3` | `P6-R3` |
### REV-P6-003 — Community 게시글 비활성화가 확인 없이 즉시 실행된다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.4, `COMMUNITY-005`
- **관련 계약:** Community post DELETE soft delete
- **소유 Task:** 신규 `P6-R3`
**관찰 내용**
Sheet의 `비활성화` 버튼은 `deletePost`를 직접 호출해 첫 click에 DELETE를 보낸다. Character·Audio·Series는 같은 영향의 동작에 `ConfirmDeactivateDialog`를 사용하지만 Community만 확인·영향 설명·취소 단계가 없다. 현재 unit/E2E도 첫 click 즉시 성공을 기대해 이 차이를 승인한다.
**근거**
- 문서: `prd.md:510-522` — 영향이 큰 비활성화는 AlertDialog 사용
- 코드: `src/features/community-posts/components/CommunityPostSheet.tsx:110-122,170-174`
- 테스트: `src/features/community-posts/tests/community-sheet.test.tsx:68-73`, `tests/e2e/community.spec.ts:155`
**재현 또는 검증 절차**
1. active Community 게시글 Sheet를 연다.
2. `비활성화`를 한 번 누른다.
3. 확인 dialog 없이 즉시 DELETE와 Sheet close가 실행되는지 확인한다.
**영향**
오조작 한 번으로 게시글이 active-only 목록에서 사라지며 현재 UI에는 복원 기능이 없다. 동일한 soft delete 정책의 다른 도메인과 안전 경계도 불일치한다.
**권장 수정 방향**
기존 공통 `ConfirmDeactivateDialog`를 재사용해 영향 설명·취소·confirm·pending/error를 연결하고, 확인 전·취소 후 request 0건과 confirm 연타 1건을 test로 고정한다.
**판정 기록:**
- 2026-07-31 — PRD UI 원칙과 도메인별 deactivate 구현을 대조해 확정.
- 2026-07-31 — `P6-R3`에서 공통 `ConfirmDeactivateDialog`를 연결하고, 첫 click·취소 후 request 0건과 confirm 연타 시 단일 soft delete를 unit test로 고정해 수정 완료.
### plan·goal 전환 및 종료 판정
- `REV-P6-003``plan-task.md` 신규 `P6-R3`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **공통 UI:** Community/Comments 화면의 pagination ID 충돌은 `REV-P1-015`/`P1-R9`이 소유한다.
### 2026-07-31 — `P6-R3` 수정 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx` | 성공 | 1 file, 7 tests passed. 첫 click 확인 dialog, 취소 focus 복귀, confirm 연타 단일 soft delete를 검증한다. |
| `npm run test:run -- src/features/community-posts` | 성공 | 5 files, 42 tests passed. Community feature 회귀가 통과했다. |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0. Production build는 기존 chunk size warning만 출력했다. |
| `git diff --check` | 성공 | whitespace error 없음. |
| LSP diagnostics | 성공 | `src/features/community-posts` directory scan 10 files, 0 diagnostics. |
**판정:** `REV-P6-003`는 수정 완료. E2E는 개발 중 매 Task 실행하지 않는 사용자 지시에 따라 전체 Task 완료 후 필요 시 수행한다.
## 12. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** Community 목록·create/update/fixed/deactivate, attachment file 정책, list-backed Sheet, 댓글 연결을 `COMMUNITY-*`, OpenAPI, `P6`와 대조했다.
- **실행 증거:** `npm run test:run -- src/features/community-posts` 5 files / 42 tests, mock Chromium Community 시나리오 7 tests가 통과했다.
- **판정:** Community payload·file 정책의 별도 신규 finding은 없다. create/update/fixed mutation의 동기 재진입 guard와 진행 표시 공백은 교차 품질 finding `REV-P9-006`/`P9-R6`에 귀속했다.
- **남은 위험:** 실제 개발 API Community 수동 QA와 `P9-R6` 완료가 필요하다.
- **신규 Phase 6 Task:** 없음. 중복 Task 대신 `P9-R6`에서 추적한다.
## 13. 종합 재점검 — 2026-07-31
- **검토 범위:** Community pagination·create/update/fixed/deactivate·attachment/crop 정책·Sheet·Comments 연결을 재대조했다.
- **실행 증거:** Community를 포함한 도메인 묶음 36 files / 206 tests, exact server E2E 18/18, 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** Community endpoint·multipart payload·Sheet 소유의 확정 신규 finding은 없다.
- **교차 Phase:** JPEG/PNG crop 문제는 `REV-P1-016~017`, 댓글 POST 실패의 초안 소실은 `REV-P8-005`에서 공통 수정한다.
- **남은 위험:** 실제 개발 API Community mutation 수동 QA가 남아 있다. WebKit 계열 Gate는 현재 지원 범위에서 제외한다.
- **신규 Phase 6 Task:** 없음.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** Community pagination, create/update/fixed/deactivate, JPEG/PNG/GIF·audio attachment, Sheet와 Comments 연결.
- **실행 증거:** 관련 unit과 4-project mock Community journey·responsive·axe가 0 failure였고 OpenAPI pagination/multipart schema ref 점검도 통과했다.
- **판정:** Phase 6 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Community mutation·active-only·media reject 수동 QA.
- **신규 Phase 6 Task:** 없음.
## 15. 최종 재검증 — 2026-07-31
- **검토 범위:** Community pagination, create/update/fixed/deactivate, media·crop policy, Sheet와 Comments 연결.
- **실행 증거:** 전체 unit 394 tests와 4-project mock Community·responsive·axe 시나리오가 0 failure였고 OpenAPI pagination/multipart schema ref 검사도 통과했다.
- **판정:** Phase 6 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Community mutation·active-only·media reject 수동 QA.
- **신규 Phase 6 Task:** 없음.
## 16. 2026-07-31 문서 기준 재리뷰
### 검토 범위와 제외
- **검토:** PRD `COMMUNITY-*`, `FILE-001~015`, OpenAPI Community list/multipart operation, create/Sheet media lifecycle, GIF/JPEG/PNG 정책과 test를 current working tree에서 대조했다.
- **제외:** 실제 server의 GIF width·MIME 거부와 memory profiler를 이용한 장시간 사용 계측은 credential·실기기 수동 QA로 남겼다.
### `REV-P6-004` GIF dimension 검사용 Blob URL 미해제
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | `FILE-003`, `FILE-011`, `FILE-014` |
| 소유 Task | `P6-R4` |
**근거**
- `src/features/community-posts/validation/community-post-media-policy.ts:60`에서 `createCropSource(file)`로 preview Blob URL과 `release`를 소유한 source를 만든다.
- 같은 파일 `:61-64`의 GIF 분기는 width가 800px 이하이면 `ready`, 초과면 `error`를 반환하면서 `source.release?.()`를 호출하지 않고 source도 반환하지 않는다.
- `CommunityPostForm.tsx:51,66-69``CommunityPostSheet.tsx:34,63-66`의 cleanup은 `prepared.kind === "crop"`인 JPEG/PNG source만 인수받는다. GIF에서 손실된 release를 호출할 수 없다.
- 현재 Community test는 valid/oversized GIF의 선택·제출 결과는 검증하지만 release 호출 횟수를 직접 검증하지 않는다.
**재현·영향**
1. valid GIF 또는 800px 초과 GIF를 선택할 때마다 `createImageCropSource()`가 만든 object URL이 해제되지 않는다.
2. 게시물 작성·수정에서 대용량 GIF를 반복 교체하면 tab의 memory 사용량이 불필요하게 유지될 수 있다. 저장 file 변조·animation 손실은 확인되지 않았다.
**권장 조치·판정 기록**
- GIF width 판정 직후 `ready`/거부 반환 전에 source를 해제하고, valid·oversized GIF에 release 각 1회, JPEG/PNG crop handoff에 조기 release 0회를 검증한다.
- 2026-07-31 — source 소유권 data flow를 정적 추적해 Medium 확정. 완료된 `P1-R11`을 열지 않고 Community GIF 특수 분기 소유의 신규 `P6-R4`로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P6-R4`에서 `prepareCommunityPostImage()` GIF width 판정 후 source release를 호출하도록 수정했다. RED 1 failure 재현 후 media policy·form·Sheet focused 3 files / 18 tests, Community 회귀 7 files / 45 tests, `typecheck`, `lint`, 개발/운영 build가 통과했고 reviewer blocker였던 release 순서도 `width → release` test로 보강해 delta review `APPROVED`를 받았다. E2E와 반복 GIF 수동 확인은 전체 Task 구현 후 필요 시 수행한다.
### 재리뷰 검증·종료 판정
- **자동 증거:** `npm run typecheck`, `npm run lint`, 개발/운영 build, server E2E 36 tests는 통과했다. 전체 unit은 79 files / 397 tests 중 5 failed / 392 passed였고, Community Sheet를 포함한 후보 5개 spec focused는 5 files / 25 tests passed였다. 비결정성은 `REV-P9-009`/`P9-R9`로 분리했다.
- **판정:** Medium 1건을 확정해 `P6-R4`로 전환했고 수정 완료했다. 오탐·보류로 남은 후보는 없다.
- **남은 위험:** 실제 개발 API media reject·active-only mutation QA와 반복 GIF 선택 memory 계측은 `P6-R4`·수동 QA 완료 전까지 남는다.
## 17. 수정 결과 재리뷰 — 2026-07-31
- **검토:** `prepareCommunityPostImage()`의 valid/oversized GIF release와 JPEG/PNG crop source handoff를 구현·test에서 재대조했다.
- **검증:** `npm run test:run -- src/features/community-posts/tests/community-post-media-policy.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx` — exit 0, 3 files / 18 tests passed. 전체 unit도 두 차례 연속 81 files / 409 tests passed했고 typecheck·lint·개발/운영 build가 통과했다. Community를 포함한 Chromium 4-spec focused mock E2E도 31 tests passed였다.
- **판정:** `REV-P6-004` 수정은 유지됐다. Phase 6 소유의 추가 확정 발견 사항은 없다.
- **남은 위험:** 실제 개발 API media reject·active-only mutation QA와 장시간 반복 선택 memory 계측은 수동 범위다.

View File

@@ -0,0 +1,189 @@
# Phase 7 FanTalk 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 7 / FanTalk 목록·답변 작성·수정·원글 soft delete |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- FanTalk 목록, 답변 판정·작성·수정, 원글 soft delete와 responsive Sheet를 계약에 맞춰 검증한다.
- UTC 응답과 mutation 성공 feedback이 운영자 화면용 표현으로 변환되는지 확인한다.
### 포함 범위
- 코드: `src/features/fan-talks`
- 테스트: FanTalk contract/UI/E2E
- 문서: `FANTALK-001`~`FANTALK-012`, PRD 날짜·표시 규칙, `P7-*`
- 수동 검증: list/card/sheet/reply의 날짜·성공 문구 정적 대조
### 제외 범위
- 제품 범위에서 제외된 별도 FanTalk 상세 route와 서버 sort/filter
- 외부 서버 fixture를 이용한 수동 mutation
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 계약값을 payload/model에 보존하는 것과 사용자 표시를 구분해 판정한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `FANTALK-009`, `FANTALK-010`, PRD §11·§13
- 계약: FanTalk list item과 reply response의 `createdAtUtc`, ID fields
- 계획: `P7-T1`~`P7-GATE`
- 코드: `FanTalkList.tsx:28`, `FanTalkListItem.tsx:15`, `FanTalkReplySheet.tsx:16-20,49-55,109-114`
- 테스트: FanTalk UI test의 raw UTC·내부 ID 성공 문구 assertion
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright 4 projects, 320/768/1280px와 keyboard viewport 포함
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | FanTalk 관련 실행 항목 통과 또는 프로젝트 정책상 skip; 전체 origin 실패는 `REV-P0-004` |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| 날짜·success feedback 정적 대조 | 실패 | 모든 surface가 UTC 원문을 출력하고 생성 성공 문구가 내부 ID 3개를 노출 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P7-001` | Medium | 수정 완료 | FanTalk가 UTC 원문과 내부 ID 중심 성공 문구를 운영자에게 노출한다 | `P7-R1` | `P7-R1` |
## 6. 발견 사항 상세
### REV-P7-001 — FanTalk가 UTC 원문과 내부 ID 중심 성공 문구를 운영자에게 노출한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FANTALK-009`, `FANTALK-010`, PRD §11, §13
- **관련 계약:** list/reply response `createdAtUtc`, reply response ID fields
- **소유 Task:** 신규 `P7-R1`
**관찰 내용**
FanTalk desktop list, mobile card, Sheet 원문과 저장된 답변이 `2026-...Z` 원문을 그대로 표시한다. 새 답변 성공 메시지는 `fanTalk <id> · reply <id> · creator <id> · <UTC>`로 구성돼 성공 여부보다 내부 식별자를 앞세운다.
**근거**
- 코드: `src/features/fan-talks/components/FanTalkList.tsx:28`
- 코드: `src/features/fan-talks/components/FanTalkListItem.tsx:15`
- 코드: `src/features/fan-talks/components/FanTalkReplySheet.tsx:20,52,112`
- 문서: `prd.md:328-329`, `prd.md:637`, `prd.md:806`
- 테스트: 현재 FanTalk test가 raw UTC와 내부 ID 조합을 기대
**재현 또는 검증 절차**
1. FanTalk 목록과 답변 Sheet를 연다.
2. `createdAtUtc``Z` 문자열 그대로 표시되는 것을 확인한다.
3. 미답변 글에 답변을 등록한다.
4. 성공 상태가 내부 fanTalk/reply/creator ID와 UTC 원문을 표시하는 것을 확인한다.
**영향**
운영자가 시간대를 직접 계산해야 하고, 핵심 동작 완료 여부보다 내부 구현 정보가 강조된다. desktop/mobile/Sheet 전체에 같은 문제가 반복된다.
**권장 조치**
중앙 날짜 formatter로 Asia/Seoul 표시를 적용하고 “답변이 등록되었습니다” 같은 행동 중심 success feedback을 사용한다. ID와 UTC 원본은 model·API test에만 보존한다.
**판정 기록**
- 2026-07-30 — 네 surface, response contract와 PRD 중앙 formatter 규칙을 대조해 확정.
- 2026-07-30 — `P7-R1`에서 공통 서울 시각 formatter와 한국어 action-centered success copy를 적용해 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P7-001``plan-task.md` 신규 `P7-R1`
- goal objective: `[P7-R1] FanTalk 날짜와 저장 성공 문구를 운영자용 표현으로 현지화한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | FanTalk code·test·계약 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P7-R1` |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 완료
**남은 항목:** 실제 server integration은 기존 Gate 정책에 따라 mock UI 증거와 분리해 추적한다.
## 9. 수정 후 검증 기록
**P7-R1 수정 완료 — 2026-07-30**
- RED: `npm run test:run -- src/features/fan-talks`는 raw UTC와 내부 ID success copy 노출을 잡아 3 failed tests로 실패했다. `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 목록 raw UTC 표시 assertion에서 실패했다.
- GREEN: `FanTalkList.tsx`, `FanTalkListItem.tsx`, `FanTalkReplySheet.tsx``formatSeoulDateTime`을 사용하고, 저장 성공 status는 `답변이 등록되었습니다.`, `답변이 수정되었습니다.`만 표시한다.
- 검증: `npm run test:run -- src/features/fan-talks`는 3 files / 13 tests passed, `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 7 passed, 전체 `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`는 26 passed / 2 skipped였다.
- 정적 확인: `src/features/fan-talks``tests/e2e` LSP diagnostics 오류 0건, `npm run typecheck` exit 0, targeted `git diff --check` no output.
## 10. 2차 점검 결과 — 2026-07-30
- Phase 7의 원래 목록·POST·표시 범위에서 `REV-P7-001` 수정 완료 상태가 유지된다.
- Phase 10에서 추가된 PUT 응답과 POST→후속 PUT reply ID 결함은 이 보고서에 중복 등록하지 않고 `REV-P10-004~005`/`P10-R4`에서 추적한다.
## 11. 2026-07-31 재점검
- **범위:** FanTalk 목록·답변 POST/PUT·팬 원글 DELETE, `creatorReplies[].fanTalkId`, UTC 표시, mutation 중복 guard를 PRD `FANTALK-001~012`와 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests와 server allowlist 36 tests에서 관련 회귀가 통과했다. 전체 Mock matrix에서도 FanTalk 시나리오는 각 project에서 통과하거나 문서화된 platform-policy skip만 발생했다.
- **판정:** Phase 7 신규 발견은 없다. `REV-P7-001`, `REV-P10-004~005` 수정 완료 상태가 유지된다.
- **남은 항목:** 실제 개발 API FanTalk mutation은 기존 수동 QA 대기 상태다.
- **신규 Task:** 없음.
## 12. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** FanTalk 목록, 답변 POST/PUT, 원글 DELETE, reply ID·UTC·목록 재조회 불변식을 `FANTALK-*`, OpenAPI, `P7`·`P10`과 대조했다.
- **실행 증거:** `npm run test:run -- src/features/fan-talks` 3 files / 13 tests, mock Chromium FanTalk 시나리오 7 tests가 통과했다.
- **판정:** endpoint·DTO의 별도 신규 finding은 없다. 미답변 원글 DELETE 실패가 표시되지 않고 confirm dialog가 pending/error를 받지 않는 문제는 다른 일반 mutation 상태 공백과 함께 `REV-P9-006`/`P9-R6`에 귀속했다.
- **남은 위험:** 실제 개발 API FanTalk mutation 수동 QA와 `P9-R6` 완료가 필요하다.
- **신규 Phase 7 Task:** 없음. 중복 Task 대신 `P9-R6`에서 추적한다.
## 13. 종합 재점검 — 2026-07-31
- **검토 범위:** FanTalk 목록·POST/PUT reply·팬 원글 DELETE, reply ID·UTC·재조회·mutation pending/error를 OpenAPI와 PRD `FANTALK-*`에 재대조했다.
- **실행 증거:** FanTalk를 포함한 도메인 묶음 36 files / 206 tests, exact server E2E 18/18, 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** Phase 7 소유의 확정 신규 finding 없음. `P9-R6`의 DELETE pending/error 보강도 현재 test에서 통과했다.
- **남은 위험:** 실제 개발 API FanTalk mutation 수동 QA가 필요하다. Safari/WebKit은 현재 지원 범위에서 제외한다.
- **신규 Phase 7 Task:** 없음.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** FanTalk list, reply POST/PUT, 팬 원글 DELETE, reply row ID, UTC 표시와 pending/error 회복.
- **실행 증거:** 관련 unit과 4-project mock FanTalk create/edit/delete journey가 0 failure였고 OpenAPI PUT/POST response schema 분리도 contract test에서 통과했다.
- **판정:** Phase 7 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API 답변 수정·원글 삭제·동시 POST 후 reply 1개 불변식 수동 QA.
- **신규 Phase 7 Task:** 없음.
## 15. 최종 재검증 — 2026-07-31
- **검토 범위:** FanTalk list, reply POST/PUT, 팬 원글 DELETE, reply ID·UTC 표시와 pending/error recovery.
- **실행 증거:** 전체 unit 394 tests와 4-project mock FanTalk 시나리오가 0 failure였고 OpenAPI POST/PUT response schema 분리도 통과했다.
- **판정:** Phase 7 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API 답변 수정·원글 삭제·동시 POST 후 reply 1개 불변식 수동 QA.
- **신규 Phase 7 Task:** 없음.
## 16. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** FanTalk list, reply POST/PUT, 팬 원글 DELETE, reply ID·UTC·pending/error·단일 답변 불변식을 `FANTALK-001~012`·OpenAPI·`P7`·`P10-T5`에 대조했다.
- **실행 증거:** FanTalk reply를 포함한 실패 후보 5-spec focused는 25 tests passed, FanTalk contract/pending test는 full output에서 통과했다. type·lint·build·server E2E 36 tests도 통과했다. full unit 비결정성은 `REV-P9-009`/`P9-R9`로 분리했다. WebKit full 중 FanTalk journey는 1회 실패했지만 동일 focused test가 통과해 `REV-P9-010`/`P9-R10`으로 분리했다.
- **판정:** FanTalk endpoint·DTO·UI 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 7 Task 없음.
- **남은 위험:** 실제 개발 API reply 수정·원글 삭제·동시 POST 수동 QA와 `P9-R9~R10` 회귀가 남는다.

View File

@@ -0,0 +1,286 @@
# Phase 8 Comments 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 8 / Audio·Community Comments 2단계 thread와 mock/server integration |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 및 외부 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- Comments의 정확한 2단계 구조, 작성자 기반 수정 권한, row 단위 soft delete와 target 격리를 검증한다.
- mock store가 실제 OpenAPI 및 PRD 불변식을 의미 있게 재현하는지 확인한다.
### 포함 범위
- 코드: `src/features/comments`, `src/shared/mocks/comment-*`
- 테스트: Comments contract/UI/E2E와 mock store 간접 검증
- 문서: `COMMENT-001`~`COMMENT-009`, `MOCK-005`, Comments OpenAPI operation, `P8-*`
- 수동 검증: create/update/delete store 조건과 fixture writer/creator 대조
### 제외 범위
- 외부 server DB의 cascade·ownership 실제 동작
- API origin 불일치 수정과 실제 server Comments E2E 재실행
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 실제 데이터 손실·보안 위험 또는 핵심 댓글 흐름 불능 |
| High | 확정 댓글 계약·권한·삭제 의미를 mock/검증이 위반하는 주요 회귀 |
| Medium | 일부 target·작성자·depth 조건의 기능 오류 |
| Low | 비핵심 표시·문서 정합성 문제 |
상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `COMMENT-002`, `COMMENT-003`, `COMMENT-004`, `COMMENT-007`, `MOCK-005`
- 계약: Audio·Community comments POST/PUT/DELETE, DELETE는 해당 row만 비활성화하고 자식 상태를 바꾸지 않음
- 계획: `P8-T1`~`P8-GATE`
- 코드: `src/shared/mocks/comment-mock-store.ts:34-37,65-83`
- 테스트: `src/features/comments/tests/comment-contract.test.ts`, `tests/e2e/comments.spec.ts`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Vitest + Playwright 4 projects
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 성공 | P8-R2 후 Comments mock E2E 10 passed / 2 skipped |
| `npm run e2e` | 실패 | Phase 0 origin 불일치로 Comments 실제 server 검증 전 단계 Gate 실패 |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| mock store mutation contract | 성공 | P8-R2 후 reply-parent 거부, fan PUT 거부, row-only delete 보존 contract 통과 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P8-003` | High | 수정 완료 | Comments mock store가 depth·수정 권한·row-only 삭제 불변식을 위반한다 | `P8-R2` | `P8-R2` |
| `REV-P8-004` | Medium | 수정 완료 | Comments adapter가 FanTalk 전용 `size=20..50` clamp를 공통 pagination에 적용한다 | `P8-R3` | `P8-R3` |
## 6. 발견 사항 상세
### REV-P8-003 — Comments mock store가 depth·수정 권한·row-only 삭제 불변식을 위반한다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `COMMENT-002`~`COMMENT-004`, `COMMENT-007`, `MOCK-005`
- **관련 계약:** Comments create/update/delete operation
- **소유 Task:** 신규 `P8-R2`
**관찰 내용**
mock store는 새 댓글의 `parentId`가 같은 target에 속하는지만 검사해 직접 답글 ID를 부모로 넣은 3단계 댓글을 허용한다. update는 target과 ID만 일치하면 작성자와 무관하게 수정한다. delete는 선택 row뿐 아니라 `parentId === commentId`인 모든 직접 답글을 함께 제거한다.
**근거**
- 코드: `src/shared/mocks/comment-mock-store.ts:34-37` — parent가 활성 root인지 확인하지 않음
- 코드: `src/shared/mocks/comment-mock-store.ts:65-75``writerId === creatorId` 확인 없음
- 코드: `src/shared/mocks/comment-mock-store.ts:78-83` — root와 직접 답글을 함께 filter
- 문서: `prd.md:338-344`, `prd.md:849`
- 계약: `api-contract.openapi.json` Comments DELETE 설명은 해당 row만 비활성화하고 자식 댓글 상태를 바꾸지 않음
- 테스트 누락: UI에서 팬 수정 버튼이 없는지만 검증하며 직접 API fan PUT 거부, reply-parent POST 거부, root DELETE 후 reply 보존을 검증하지 않음
**재현 또는 검증 절차**
1. 같은 target의 기존 직접 답글 ID를 `parentId`로 POST한다.
2. mock store가 성공 처리해 3단계 row를 만드는 것을 확인한다.
3. `writerId !== creatorId`인 팬 댓글 ID로 PUT한다.
4. mock store가 성공 처리하는 것을 확인한다.
5. 답글이 있는 root를 DELETE하고 답글 목록도 함께 사라지는 것을 확인한다.
6. 요구 결과는 각각 요청 거부, 팬 PUT 거부, root row만 삭제하고 자식 상태 보존이다.
**영향**
mock preview와 contract test가 실제 댓글 계약과 다른 데이터 구조·권한·삭제 의미를 승인한다. 서버 연동 전 핵심 회귀를 숨기고 root 삭제 시 자식 데이터 손실을 정상 동작처럼 보이게 한다.
**권장 조치**
parent가 같은 target의 활성 root인지, update 대상의 `writerId`가 target creator ID와 같은지 검사한다. delete는 해당 ID row만 제거한다. Audio·Community 양쪽에 3개 음성 contract test와 E2E 보존 assertion을 추가한다.
**판정 기록**
- 2026-07-30 — PRD·OpenAPI와 mock store의 세 mutation branch를 직접 대조해 확정.
### REV-P8-004 — Comments adapter가 FanTalk 전용 `size=20..50` clamp를 공통 pagination에 적용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §11.1 공통 pagination, `COMMENT-006~007`
- **관련 계약:** OpenAPI 공통 `Size` parameter
- **소유 Task:** 신규 `P8-R3`
**관찰 내용**
Comments `normalizeSize``size=1`을 20으로, `size=51`을 50으로 바꾼다. 공통 계약은 default 20·minimum 1만 정의하고 maximum은 없으며 `20..50` 보정은 FanTalk에만 적용한다. 현재 UI는 size 20을 사용해 화면 회귀가 드러나지 않지만 adapter 입력 계약이 다르다.
**근거**
- 코드: `src/features/comments/api/comment-api.ts:27-33,48-50`
- 문서: `prd.md:636`
- 계약: OpenAPI `components.parameters.Size`의 default 20, minimum 1, maximum 없음
- 테스트: Comments contract test는 mock handler `size=1`을 사용하지만 API adapter가 생성하는 `size=1`·`51` query를 검증하지 않음
**재현 또는 검증 절차**
1. `getRootComments(..., {size:1})` 또는 `getReplies(..., {size:1})`를 호출한다.
2. 실제 request query가 `size=20`이 되는 것을 확인한다.
3. `size=51``size=50`이 된다.
4. 요구 결과는 각각 `size=1`, `size=51`을 그대로 보내고 0 이하만 1로 보정하는 것이다.
**영향**
작은 page를 요청하는 소비자는 과다 데이터를 받고 50개를 넘는 page를 요청하는 소비자는 요청값과 다른 pagination 결과를 받는다. adapter와 OpenAPI contract test의 신뢰성이 낮아진다.
**권장 조치**
Comments normalization에서 maximum과 minimum 20을 제거하고 최소 1만 적용한다. root와 reply 양쪽에 1·51 경계 test를 추가한다.
**판정 기록**
- 2026-07-30 — PRD·OpenAPI 공통 parameter와 adapter 계산을 대조해 확정.
- 2026-07-30 — `P8-R3`에서 Comments size normalization을 default 20·minimum 1로 정렬하고 root/replies adapter contract test로 `size=1`·`size=51` 보존을 확인해 수정 완료로 판정했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P8-003``plan-task.md` 신규 `P8-R2`
- goal objective: `[P8-R2] Comments mock mutation의 2단계·작성자 수정·row-only delete 불변식을 복구한다.`
- `REV-P8-004``plan-task.md` 신규 `P8-R3`
- goal objective: `[P8-R3] Comments pagination을 공통 size 최소값 계약과 정렬한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Comments code·test·PRD·OpenAPI 대조 |
| 후보 항목 판정 완료 | 충족 | 기존 1건 수정 완료, 신규 1건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P8-R2`, `P8-R3` 완료 |
| 확정 항목 수정 | 충족 | `P8-R2`, `P8-R3` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료.
**남은 항목:** 실제 개발 API Comments 수동 QA.
## 9. 수정 후 검증 기록
- 수정 전 기록 — 2026-07-30: 아직 수정하지 않았다. `P8-R2` 완료 시 검증 결과를 누적한다.
### P8-R2 수정 후 검증 — 2026-07-30
- 무엇을: root/direct reply 2단계 생성, AI 작성 row 수정, 대상 row만 삭제하는 Comments mock mutation 불변식을 복구했다.
- 왜: reply를 부모로 둔 3단계 row 생성, fan row 직접 PUT, root DELETE의 자식 row 제거가 mock preview와 contract test에서 실제 계약 위반을 승인하고 있었다.
- 어떻게:
- RED: `npm run test:run -- src/features/comments/tests/comment-contract.test.ts`는 1 file / 3 failed / 3 passed였다. Audio·Community reply-parent POST와 fan PUT은 200으로 성공했고, Audio root DELETE 뒤 직접 reply 목록은 0건이었다.
- GREEN: `CommentMockStore`가 target의 root parent만 생성 대상으로 허용하고, creator ID와 writer ID가 일치하는 row만 수정하며, DELETE는 대상 row만 제외하도록 수정했다. focused contract test는 1 file / 6 tests passed였다.
- 회귀: `npm run test:run -- src/features/comments src/shared/mocks`는 8 files / 33 tests passed였다. `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`은 3 passed, 전체 `npm run e2e:mock -- tests/e2e/comments.spec.ts`는 10 passed / 2 skipped였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/phase8-comments.md src/shared/mocks src/features/comments tests/e2e/comments.spec.ts`는 모두 exit 0이었다.
- 진단: `src/features/comments/tests/comment-contract.test.ts`, `src/shared/mocks/comment-mock-store.ts` LSP diagnostics는 모두 0건이었다.
- 수동 확인: Chromium mock E2E에서 Audio create/reply/edit/delete, Community 320px controls, keyboard-only Audio create flow를 실행해 모두 통과했다. UI의 fan PUT 0-request assertion도 기존 E2E로 유지했다.
- 2026-07-30 — 2차 계약 점검에서 Comments `size=1`→20, `size=51`→50 보정을 확인했다. 애플리케이션 코드는 수정하지 않고 `P8-R3`로 전환했다.
### P8-R3 수정 후 검증 — 2026-07-30
- 무엇을: Comments adapter의 `size` query normalization에서 FanTalk 전용 `20..50` clamp를 제거하고 공통 pagination 계약인 default 20·minimum 1만 적용했다.
- 왜: OpenAPI 공통 `Size`에는 maximum이 없고, `20..50` 보정은 FanTalk 전용이므로 Comments root/replies 요청값을 바꾸면 안 되기 때문이다.
- 어떻게: RED `npm run test:run -- src/features/comments/tests/comment-contract.test.ts`는 1 failed / 5 passed로 root `size=1``20`, replies `size=51``50`으로 바뀌는 실패를 재현했다. 수정 후 같은 command는 1 file / 6 tests passed, `npm run test:run -- src/features/comments`는 2 files / 11 tests passed였다. `npm run typecheck`, `npm run lint`는 exit 0이었고 LSP diagnostics는 변경 파일 0건이었다. 개발 중 E2E는 사용자 지시에 따라 `P10-R5` 이후 최종 E2E로 미뤘다.
## 10. 2026-07-31 재점검
- **범위:** Audio·Community root/reply 2단계, AI 작성 row PUT, 작성자 무관 row-only DELETE, pagination과 target 격리를 PRD `COMMENT-001~008` 및 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests와 server allowlist 36 tests가 통과했다. 전체 Mock matrix의 Comments 시나리오도 각 project에서 통과하거나 문서화된 keyboard platform-policy skip만 발생했다.
- **판정:** Phase 8 신규 구현 결함은 없다. 공통 `ResourcePagination` ID 충돌은 shared UI 소유인 `REV-P1-015`/`P1-R9`로만 등록해 중복 Task를 만들지 않았다.
- **문서 상태:** 상단 리뷰 상태와 §5 요약의 `REV-P8-004` 상태가 §6·§8·§9의 수정 완료 기록과 일치하지 않는 문제는 `REV-P10-007`/`P10-R6`에 포함했다.
- **남은 항목:** 실제 개발 API Comments 수동 QA.
- **신규 Task:** 없음.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** Audio·Community root/direct reply 2단계, 작성자별 수정·row-only DELETE, pagination과 cache 재조회를 `COMMENT-*`, OpenAPI, `P8`과 대조했다.
- **실행 증거:** `npm run test:run -- src/features/comments` 2 files / 11 tests, mock Chromium Comments 시나리오 3 tests가 통과했다.
- **판정:** Comments endpoint·payload·2단계 구조의 별도 신규 finding은 없다. `runMutation`의 동기 재진입 guard와 accessible 진행 표시 공백은 교차 품질 finding `REV-P9-006`/`P9-R6`에 귀속했다.
- **남은 위험:** 실제 개발 API Comments 수동 QA와 `P9-R6` 완료가 필요하다.
- **신규 Phase 8 Task:** 없음. 중복 Task 대신 `P9-R6`에서 추적한다.
## 12. 종합 재점검 — 2026-07-31
### `REV-P8-005` — 댓글 POST 실패 후 입력 초안이 성공처럼 초기화됨
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD §10.5 오류·재시도 상태, `COMMENT-001~006`; OpenAPI mutation 성공 `data=null`/실패 error envelope |
| 소유 Task | `P8-R4` |
| 코드 근거 | `src/features/comments/components/CommentForm.tsx:22~24`, `CommentThread.tsx:83~118` |
| test 근거 | Comments tests는 성공·pending은 검증하지만 루트/답글 POST 실패 후 textarea 값 보존과 같은 form 재시도를 검증하지 않는다. |
**재현 또는 검증 절차**
1. Audio 또는 Community 댓글에서 루트 댓글이나 답글을 입력한다.
2. 첫 POST가 500 error를 반환하게 한다.
3. `runMutation`이 오류를 화면에 저장한 뒤 reject를 소비하고 정상 resolve하는지 확인한다.
4. `CommentForm``await onSubmit` 다음 줄에서 textarea를 비워 사용자가 입력한 내용을 잃는지 확인한다.
**영향과 권장 조치**
서버 오류는 표시되지만 운영자는 같은 내용을 바로 재시도할 수 없고 댓글을 다시 입력해야 한다. mutation callback이 명시적인 성공 여부를 반환하게 하고 성공한 POST에서만 form 값을 초기화한다. localStorage나 optimistic update는 추가하지 않는다.
**판정 기록**
- 2026-07-31 — component 간 Promise contract와 실패 catch 경로를 정적으로 추적해 확정.
- 2026-07-31 — Audio·Community에 중복 Task를 만들지 않고 Comments 소유 신규 `P8-R4`로 전환.
- 2026-07-31 — `P8-R4`에서 댓글 생성 실패 후 초안 유지와 성공 후 초기화 contract를 단위 테스트로 고정해 수정 완료 판정.
### Phase 8 결론
- **자동 검증:** Comments를 포함한 도메인 묶음 36 files / 206 tests, exact server E2E 18/18, 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** endpoint·DTO·2단계 thread 구조는 통과했고 Medium 1건은 `P8-R4`에서 수정 완료됐다.
- **남은 위험:** 실제 개발 API Comments 수동 QA가 남아 있다. Safari/WebKit은 현재 지원 범위에서 제외한다.
### P8-R4 수정 후 검증 — 2026-07-31
- 무엇을: 루트 댓글과 열린 답글 form의 POST 실패 시 입력 초안을 유지하고, 같은 form 재시도 성공 후에만 textarea를 비우도록 수정했다.
- 왜: `runMutation`이 실패를 내부 alert로 처리하면서도 `CommentForm`에는 성공처럼 resolve해 사용자의 초안을 잃게 했기 때문이다.
- 검증: RED `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx`는 1 failed / 5 passed로 실패 후 root textarea가 빈 값이 되는 문제를 재현했다. GREEN focused는 1 file / 6 tests passed, Comments 회귀는 `npm run test:run -- src/features/comments` 3 files / 13 tests passed였다. `src/features/comments` LSP diagnostics는 5 TSX files / 0 diagnostics였다.
- E2E: 사용자 지시에 따라 개발 중 반복 E2E는 실행하지 않고 최종 회귀 단계에서 필요 시 실행한다.
## 13. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** Audio·Community 2단계 thread, writer 권한, row-only delete, root/reply pagination, 실패 초안 보존과 pending 상태.
- **실행 증거:** Comments unit은 전체 실행에서 통과했고 4-project mock Comments journey는 platform 근거가 있는 keyboard skip 외 0 failure였다.
- **판정:** `P8-R4`까지의 수정 완료 상태가 유지되며 Phase 8 기능 소유의 신규 결함은 없다.
- **문서 교차 항목:** 상단 리뷰 상태가 최신 `P8-R4` 수정 완료 기록과 어긋나는 문제는 `REV-P10-008`/`P10-R7`에서 정리한다.
- **남은 위험:** 실제 개발 API Comments mutation·권한·cache 재조회 수동 QA.
## 14. 최종 재검증 — 2026-07-31
- **검토 범위:** Audio·Community 2단계 thread, writer 권한, row-only delete, pagination, 실패 초안 보존과 pending/error 상태.
- **실행 증거:** 전체 unit 394 tests와 4-project mock Comments 시나리오가 정책상 keyboard skip 외 0 failure였고 OpenAPI 10개 Comments operation·schema ref 검사도 통과했다.
- **판정:** Phase 8 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 실제 개발 API Comments mutation·권한·cache 재조회 수동 QA.
- **신규 Phase 8 Task:** 없음.
## 15. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** Audio·Community 2단계 thread, target별 path/schema, writer 수정 권한, row-only delete, root/reply pagination, 초안 보존·pending/error를 `COMMENT-001~008`·OpenAPI·`P8`·`P10-T6`에 대조했다.
- **실행 증거:** Comments contract/thread/pending test는 full output에서 통과했고, OpenAPI 10개 Comments operation·schema ref·type·lint·build·server E2E 36 tests도 통과했다. full unit 비결정성은 `REV-P9-009`/`P9-R9`로 분리했다.
- **판정:** Comments endpoint·DTO·2단계 UI 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 8 Task 없음.
- **남은 위험:** 실제 Comments mutation·권한·cache 재조회 수동 QA와 `P9-R9` 회귀가 남는다.

View File

@@ -0,0 +1,828 @@
# Phase 9 Cross-cutting Quality 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 9 / responsive, accessibility, error recovery, cross-domain quality Gate |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-08-01 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md`, 이전 Phase 9 리뷰 |
| 리뷰 상태 | `REV-P9-018`~`REV-P9-019`/`P9-R18`~`P9-R19` 수정 완료, 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- 비활성 Character mutation 차단, field 오류 연결, responsive·keyboard·axe 품질과 Gate 기록을 재검증한다.
- 이전 Phase 9 리뷰의 발견 상태와 완료된 회귀 Task 기록이 일치하는지 확인한다.
### 포함 범위
- 코드: 전 도메인의 capability, form error, responsive/accessibility 공통 구현
- 테스트: 전체 unit, typecheck, lint, build, mock/server E2E
- 문서: PRD §10·§13·§14, `P9-*`, `review-phase-9-20260729.md`
- 수동 검증: 이전 발견 ID와 `P9-R1`·`P9-R2` 완료 증거의 문서 대조
### 제외 범위
- 외부 server credential·fixture 기반 수동 QA
- 각 도메인에서 별도 소유한 신규 발견의 중복 등록
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 접근성·반응형 기능 문제와 검증 이력의 추적 가능성을 모두 확인한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: PRD §10.2~§10.4, §13, §14.2
- 계획: `P9-T1`~`P9-GATE`, `P9-R1`, `P9-R2`
- 이전 리뷰: `reviews/review-phase-9-20260729.md:70-147`
- 현재 구현: inactive mutation capability와 form error/focus 회귀 test
### 실행 환경
```text
macOS 26.0 (Build 25A354)
Node v24.12.0 / npm 11.7.0
Playwright Chromium, Mobile Chrome
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` / `npm run lint` | 성공 | 모두 exit 0 |
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run build` | 성공 | exit 0, 253 modules transformed |
| `npm run e2e:mock` | 실패 | 201 passed, 22 skipped, 5 failed; 4건은 `REV-P0-004`, 1건 timeout은 focused 재실행 통과 |
| `npm run e2e` | 실패 | 12 passed, 24 failed; `REV-P0-004`의 동일 origin 원인 |
| 이전 Phase 9 리뷰와 plan 완료 기록 대조 | 성공 | `REV-P9-001~002` 수정 완료와 `P9-R1~R2`, Gate 완료 기록 일치 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P9-003` | Low | 수정 완료 | 이전 Phase 9 리뷰가 완료된 P9-R1·P9-R2를 미해결로 표시한다 | `P9-R3` | `P9-R3` |
| `REV-P9-004` | Medium | 수정 완료 | 일반 mutation 일부가 pending 중 중복 요청을 허용하고 비활성화 실패를 화면에 표시하지 않는다 | `P9-R4` | `P9-R4` |
## 6. 발견 사항 상세
### REV-P9-003 — 이전 Phase 9 리뷰가 완료된 P9-R1·P9-R2를 미해결로 표시한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 리뷰 가이드 §4 수정 후 상태·검증 누적 규칙
- **관련 계약:** 없음
- **소유 Task:** 신규 `P9-R3`
**관찰 내용**
2026-07-29 Phase 9 리뷰는 `REV-P9-001`, `REV-P9-002``확정`으로 유지하고 최종 남은 항목을 `P9-R1`, `P9-R2`라고 기록한다. 이후 plan 하단에는 두 회귀 Task와 자동 Gate 완료 기록이 있으나 원 리뷰의 상태를 `수정 완료`로 갱신하고 수정 후 검증을 누적하지 않았다.
**근거**
- 리뷰: `reviews/review-phase-9-20260729.md:70-71,78,109,147`
- 계획: `plan-task.md``P9-R1`, `P9-R2` 완료 체크와 후속 검증 기록
- 가이드: `docs/agent-guide/review.md` §4는 수정 후 상태 변경과 실제 검증 결과 누적을 요구
**재현 또는 검증 절차**
1. 이전 Phase 9 리뷰의 발견 요약과 최종 남은 항목을 읽는다.
2. `plan-task.md` 하단의 `P9-R1`, `P9-R2` 완료 기록과 관련 test 결과를 확인한다.
3. 같은 항목이 한 문서에서는 미해결, 다른 문서에서는 완료로 표시되는 것을 확인한다.
**영향**
다음 reviewer와 구현자가 이미 완료된 회귀 Task를 다시 수행하거나 현재 남은 작업을 잘못 판단할 수 있다. 코드 기능보다 이력 추적의 정합성 문제다.
**권장 조치**
기존 내용을 삭제하지 않고 이전 리뷰의 상태를 `수정 완료`로 변경하고 날짜별 수정 후 검증 기록을 추가한다. 신규 2026-07-30 발견과는 구분한다.
**판정 기록**
- 2026-07-30 — 이전 리뷰의 상태·최종 결론과 plan의 완료·Gate 기록을 대조해 확정.
- 2026-07-30 — 과거 Phase 9 리뷰에 `REV-P9-001~002` 수정 완료 상태와 `P9-R1~R2`, `P9-GATE` 검증 기록을 누적해 완료.
### REV-P9-004 — 일반 mutation 일부가 pending 중 중복 요청을 허용하고 비활성화 실패를 화면에 표시하지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §13 일반 mutation 중복 제출 차단·오류 복구
- **관련 계약:** Character/Audio/Series soft delete, Series link/unlink/order operation
- **소유 Task:** 신규 `P9-R4`
**관찰 내용**
공통 `ConfirmDeactivateDialog`에는 pending/disabled/error interface가 없고 Character·Audio·Series의 confirm handler도 guard와 `catch`가 없다. 요청이 끝나기 전에 확인을 다시 누르면 같은 soft-delete를 반복 호출할 수 있고 실패하면 visible 오류 없이 rejected Promise가 남는다. Series 연결·해제·순서 저장도 pending guard/state가 없어 버튼이 요청 중 계속 활성화된다.
**근거**
- 공통 UI: `src/shared/ui/confirm-deactivate-dialog.tsx:3-34`
- Character: `src/features/characters/pages/CharacterDetailPage.tsx:53-76`
- Audio: `src/features/audio-contents/components/AudioContentForm.tsx:172-178,228`
- Series: `src/features/series/components/SeriesForm.tsx:152-157,191`
- Series link/order: `src/features/series/components/SeriesContents.tsx:72-98,138-139`, `src/features/series/pages/SeriesOrderPage.tsx:79-92,118`
- 테스트 검색: 대상 흐름의 pending Promise 이중 입력·deactivate reject visible alert test 0건
**재현 또는 검증 절차**
1. deactivate API Promise를 pending 상태로 유지하고 confirm 버튼을 빠르게 두 번 누르면 request가 두 번 호출된다.
2. Promise를 reject하면 dialog/page에 오류 alert가 없고 호출부가 오류를 처리하지 않는다.
3. 같은 방식으로 Series 연결·해제·순서 저장 버튼을 반복하면 pending 중 추가 request가 가능하다.
**영향**
느린 네트워크나 반복 입력에서 같은 mutation이 중복 전송되고 실패 원인과 재시도 방법을 운영자가 확인할 수 없다. soft-delete가 idempotent하더라도 요청 폭증과 잘못된 성공/오류 피드백이 발생할 수 있다.
**권장 조치**
각 mutation 경계에 synchronous guard와 pending state를 두고 controls를 비활성화한다. 공통 deactivate dialog에는 pending·오류 표시를 추가하되 서로 다른 도메인 mutation을 새 범용 hook으로 합치지 않는다.
**판정 기록**
- 2026-07-30 — 관련 handler·dialog·test를 정적 대조해 pending guard와 오류 처리 부재를 확정.
- 2026-07-30 — `P9-R4`에서 공통 deactivate dialog pending/error UI와 Character/Audio/Series deactivate, Series link/unlink/order pending guard를 추가하고 focused unit 검증으로 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P9-003``plan-task.md` 신규 `P9-R3`
- goal objective: `[P9-R3] 이전 Phase 9 리뷰에 완료된 회귀 Task의 상태와 검증 이력을 누적해 문서 정합성을 복구한다.`
- `REV-P9-004``plan-task.md` 신규 `P9-R4`
- goal objective: `[P9-R4] 일반 mutation의 pending 중복 차단과 비활성화 오류 복구를 보강한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | cross-cutting 코드·Gate·리뷰 이력 대조 |
| 후보 항목 판정 완료 | 충족 | 기존 1건 수정 완료, 신규 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P9-R3`, `P9-R4` 완료 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** `REV-P9-003`, `REV-P9-004` 수정 완료
**남은 항목:** 실제 개발 API fixture가 필요한 integration은 `P10-GATE` 수동 QA로 분리한다.
## 9. 수정 후 검증 기록
### P9-R3 수정 후 검증 — 2026-07-30
- 무엇을: `review-phase-9-20260729.md``REV-P9-001~002` 상태, 판정 기록, 최종 결론, 남은 항목, 수정 후 검증 기록을 `P9-R1~R2``P9-GATE` 완료 증거에 맞춰 누적했다.
- 왜: 현재 리뷰의 `REV-P9-003`이 지적한 과거 리뷰와 최신 plan/Gate 상태의 불일치를 닫기 위해서다.
- 검증: stale 현재 상태 검색과 `git diff --check -- docs/20260725_AI캐릭터관리자웹`를 통과했다.
- 2026-07-30 — 2차 정적 점검에서 공통 deactivate 3개 흐름과 Series link/unlink/order의 pending guard, deactivate failure UI 부재를 확인했다. 애플리케이션 코드는 수정하지 않고 `P9-R4`로 전환했다.
### P9-R4 수정 후 검증 — 2026-07-30
- 무엇을: `ConfirmDeactivateDialog`에 pending/error UI를 추가하고, Character/Audio/Series 비활성화와 Series 연결·해제·순서 저장에 synchronous guard와 pending state를 추가했다.
- 왜: 일반 mutation이 pending 중 중복 request를 보내지 않고 실패 후 운영자가 같은 화면에서 재시도할 수 있게 하기 위해서다.
- 검증: `npm run test:run -- src/features/characters/tests/CharacterDetailPage.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx src/features/series/tests/series-form.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/features/series/tests/series-order.test.tsx src/features/series/tests/series-contents.test.tsx` 결과 6 files / 28 tests passed. 변경 test 파일 LSP diagnostics는 모두 clean이었다. E2E는 `P10-R5`까지 구현 후 최종 실행한다.
## 10. 2026-07-31 재점검
### 실행 결과
| 명령 | 결과 | 판정 |
|---|---|---|
| `npm run test:run` | 72 files / 360 tests passed | 성공 |
| `npm run typecheck` | exit 0 | 성공 |
| `npm run lint` | exit 0 | 성공 |
| `npm run build` | exit 0, 255 modules transformed | 성공 |
| `npm run e2e` | 36 passed | 성공. sandbox의 `listen EPERM` 뒤 승인된 로컬 server 실행으로 재검증 |
| `npm run e2e:mock` | 205 passed / 22 skipped / 1 failed | 당시 Mobile Safari Character dirty-leave dialog 미출현. 이후 지원 project 축소로 현재 Gate 대상 아님 |
### 신규 발견 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P9-005` | Medium | 수정 완료 | Series mutation은 중복 guard만 있고 visible pending 상태가 없다 | `P9-R5` | `P9-R5` |
### REV-P9-005 — Series mutation은 중복 guard만 있고 visible pending 상태가 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.5 저장 중 상태·300ms feedback, §13 일반 mutation 중복 제출 차단
- **소유 Task:** 신규 `P9-R5`
**관찰 내용**
Series 연결·해제·순서 저장은 `useRef` 동기 guard로 반복 request만 막는다. React pending state가 없어 link button, unlink dialog의 취소/확인, order 이동/저장 control은 request 중에도 활성 상태이며 `aria-busy`나 진행 문구가 없다. 이는 `P9-R4`의 완료 기록이 선언한 “pending state·disabled controls·aria-busy”와 현재 코드가 일치하지 않는 회귀다.
**근거**
- 코드: `src/features/series/components/SeriesContents.tsx:20-31,74-109,149-150,169-188`
- 코드: `src/features/series/pages/SeriesOrderPage.tsx:48-55,80-99,118-126`
- 코드: `src/features/series/components/SeriesOrderList.tsx:3-12`
- 계획 이력: `plan-task.md` 기존 `P9-R4` Interfaces와 2026-07-30 GREEN 기록
**재현 또는 검증 절차**
1. link/unlink/order API Promise를 pending으로 유지한다.
2. mutation control을 누른 뒤 관련 button이 계속 enabled이고 진행 상태가 없는지 확인한다.
3. 연타 request는 ref guard 때문에 1건이지만 운영자가 pending 여부를 알 수 없고 순서 이동 같은 다른 관련 control을 계속 조작할 수 있다.
**영향 및 권장 수정 방향**
느린 네트워크에서 저장 여부가 보이지 않고 request payload와 화면 순서를 다르게 바꿀 수 있다. 기존 ref guard는 보존하고 reactive pending state로 관련 control을 비활성화하며 accessible progress를 표시한 뒤 reject 시 재시도를 허용한다.
**판정 기록:**
- 2026-07-31 — 현재 코드와 `P9-R4` 완료 인터페이스 및 PRD를 대조해 확정.
- 2026-07-31 — `P9-R5`에서 Series link/unlink/order에 reactive pending state와 disabled/status UI를 추가하고 focused·Series unit·정적 Gate로 수정 완료.
### plan·goal 전환 및 종료 판정
- `REV-P9-005``P9-R5`
- **최종 결론:** 신규 Medium 1건 수정 완료. Mobile Safari/WebKit 기반 후보는 현재 Playwright 지원 project에서 제외되어 Task로 전환하지 않는다.
- **주의:** 과거 Mobile Safari 실패 기록은 이력으로만 남기고 현재 자동 Gate 대상에는 포함하지 않는다.
## 11. 최종 Phase별 점검 — 2026-07-31
### 실행 결과
| 명령 또는 검증 | 결과 | 판정 |
|---|---|---|
| Phase별 focused unit | P0·1 64, shared 117, P3 43, P4 53, P5 37, P6 42, P7 13, P8 11 tests passed | 성공 |
| `npm run typecheck` / `npm run lint` / `npm run build` | 모두 exit 0, build 255 modules | 성공 |
| `npm run e2e` | 18/18 passed | 성공. 현재 Chromium·Mobile Chrome 구성 |
| `npm run e2e:mock -- --project=chromium` | 57/57 passed | 성공 |
| 일반 mutation handler·pending UI 정적 대조 | 일부 handler에 동기 guard·진행/실패 표시 없음 | 실패, 신규 finding 확정 |
### 신규 발견 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P9-006` | Medium | 수정 완료 | 일부 일반 mutation이 동기 재진입 차단과 접근 가능한 진행·실패 피드백을 보장하지 않는다 | `P9-R6` | `P9-R6` |
### REV-P9-006 — 일부 일반 mutation이 동기 재진입 차단과 접근 가능한 진행·실패 피드백을 보장하지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.5 저장 중 상태·300ms 피드백, §13 일반 mutation 중복 제출 차단
- **관련 계약:** Series create/update, Community create/update/fixed, FanTalk reply/delete, Comments create/update/delete
- **소유 Task:** 신규 `P9-R6`
**관찰 내용**
Series form, Community create·Sheet save/fixed, Comments `runMutation`은 React `isSaving`만 설정하고 handler 진입 시 동기 guard를 확인하지 않는다. state가 반영되기 전 같은 submit/click이 재진입하면 동일 mutation을 다시 시작할 수 있다. 관련 submit button은 pending 뒤 disabled되지만 label·`role=status`·`aria-busy`가 없어 저장 진행을 전달하지 않는다.
FanTalk reply/delete는 `isSavingRef`로 중복 요청은 막지만 진행 표시가 없고, DELETE confirm dialog에 `isPending`·`errorMessage`를 전달하지 않는다. 특히 답변이 없는 원글에서 DELETE가 실패하면 `visibleReply === null` 조건 때문에 sheet 오류 alert도 렌더링되지 않는다.
**근거**
- Series: `src/features/series/components/SeriesForm.tsx:152-175,226`
- Community: `src/features/community-posts/components/CommunityPostForm.tsx:117-139,159`, `CommunityPostSheet.tsx:37-51,100-111,179-185`
- FanTalk: `src/features/fan-talks/components/FanTalkReplyForm.tsx:42-44`, `FanTalkReplySheet.tsx:40-88,115-120`
- Comments: `src/features/comments/components/CommentThread.tsx:82-96,136-168`, `CommentForm.tsx:11-24,40-42`, `CommentItem.tsx:28-41`
- 테스트 공백: FanTalk reply 이중 submit과 일부 deactivate만 고정되어 있고 위 대상의 pending Promise 재진입·visible progress·미답변 DELETE reject 회귀는 없다.
**재현 또는 검증 절차**
1. Series create 또는 Community create API를 pending Promise로 둔다.
2. 같은 form에 submit event를 state rerender 전 연속 두 번 전달한다.
3. 현재 handler에 동기 early return이 없어 request가 두 번 시작되는지 확인한다.
4. FanTalk 미답변 item의 DELETE를 reject하고 dialog가 닫힌 뒤 오류 alert가 없는지 확인한다.
5. 각 pending 상태에서 접근 가능한 progress text/`aria-busy`가 없는지 확인한다.
**영향과 권장 조치**
느린 네트워크·키보드 submit·자동화 입력에서 생성/수정 요청이 중복될 수 있고, 운영자는 저장 진행 또는 FanTalk 삭제 실패를 인지하지 못한다. backend idempotency나 새 상태 library를 추가하지 않고 각 mutation 경계에 ref 기반 최소 guard, 기존 `isSaving` 기반 disabled·status, 기존 confirm dialog pending/error prop을 연결한다.
**판정 기록**
- 2026-07-31 — PRD 공통 상태·중복 제출 계약과 handler·UI·test를 도메인별로 대조해 확정.
- 2026-07-31 — 중복 Phase Task를 만들지 않고 `plan-task.md`의 미완료 신규 Task `P9-R6`로 전환. 애플리케이션 코드는 이번 리뷰 범위에서 수정하지 않았다.
- 2026-07-31 — `P9-R6`에서 Series form, Community create/Sheet save/fixed, Comments mutation, FanTalk DELETE에 동기 guard와 accessible pending/error feedback을 추가하고 focused/domain 검증으로 수정 완료.
### plan·goal 전환 및 종료 판정
- `REV-P9-006``plan-task.md` 신규 `P9-R6`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **남은 항목:** 기존 실제 개발 API 수동 QA.
## 12. P9-R6 수정 후 검증 — 2026-07-31
- 무엇을: Series form, Community create/Sheet save/fixed, Comments mutation, FanTalk 미답변 원글 DELETE의 pending 중 재진입 차단과 접근 가능한 진행·실패 피드백을 검증했다.
- 왜: `REV-P9-006`의 일반 mutation 재진입·피드백 누락을 닫고, 기존 `P9-R4`·`P9-R5` 완료 범위와 중복되지 않는 도메인 경계만 보강했는지 확인하기 위해서다.
- 검증: `npm run test:run -- src/features/series/tests/series-form-pending.test.tsx src/features/community-posts/tests/community-mutation-pending.test.tsx src/features/comments/tests/comment-thread-pending.test.tsx src/features/fan-talks/tests/fan-talk-delete-pending.test.tsx`는 4 files / 5 tests passed. `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-mutation-pending.test.tsx`는 2 files / 9 tests passed. `npm run test:run -- src/features/series src/features/comments`는 12 files / 50 tests passed. `npm run test:run -- src/features/fan-talks`는 4 files / 14 tests passed.
- 분리 기록: `npm run test:run -- src/features/series src/features/community-posts src/features/comments src/features/fan-talks`는 120초 timeout 중 `community-sheet.test.tsx` 목록 로딩 실패를 1건 표시했으나, 해당 spec 단독과 인접 pending spec 실행이 모두 통과해 P9-R6 코드 변경 실패가 아니라 넓은 병렬 suite의 비결정 로딩 실패로 분리한다.
- E2E 검증: `npm run e2e:mock -- tests/e2e/series.spec.ts tests/e2e/community.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts --project=chromium`은 24 tests passed.
- 정적 검증: 변경 TS/TSX 일부 LSP diagnostics는 3초 제한으로 timeout됐고, `CommentThread.tsx`, `comment-thread-pending.test.tsx`, `FanTalkReplySheet.tsx`, `fan-talk-delete-pending.test.tsx`는 diagnostics 0건이었다. 보완 검증으로 `npm run typecheck`, `npm run lint`, `npm run build`, 관련 파일 `git diff --check`를 실행했고 모두 exit 0이었다. build는 기존 500kB chunk warning만 표시했다.
## 13. 종합 재점검 — 2026-07-31
### `REV-P9-007` — Safari/WebKit 지원 project 제거로 완료 Gate가 축소됨
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD §13 browser support, `P9-T2` browser 자동 Gate, Phase 9 수용 기준 |
| 소유 Task | `P9-R7` |
| 설정·문서 근거 | `playwright.config.ts:34~37``chromium`, `mobile-chrome`만 정의하고 README Browser Support도 Safari를 제외한다. PRD `prd.md:807`과 완료된 `P9-T2` checklist는 Safari desktop/mobile을 유지한다. |
| 실행 근거 | `npx playwright test --list --project=webkit` — exit 1, `Project(s) "webkit" not found. Available projects: "chromium", "mobile-chrome"`. |
**재현 또는 검증 절차**
1. `playwright.config.ts`의 project 목록과 PRD §13, `P9-T2` 완료 checklist를 대조한다.
2. `npx playwright test --list --project=webkit``--project=mobile-safari`를 실행한다.
3. 현재 두 project가 수집되지 않아 과거 4-project Gate와 같은 범위를 재실행할 수 없는지 확인한다.
4. 현재 exact `npm run e2e:mock` 109 passed / 5 skipped와 `npm run e2e` 18/18이 Chromium·Mobile Chrome에만 해당함을 확인한다.
**영향과 권장 조치**
명시적으로 지원하는 browser 계열 절반이 자동 acceptance에서 빠졌고, 과거 Mobile Safari dirty-leave 실패를 project 제거로 종결한 기록은 PRD 변경 결정 없이 Gate를 약화한다. 기존 `webkit`·`mobile-safari` project를 복원하고, 재현되는 실패는 제품 코드/테스트/환경 원인을 분리해 수정한 뒤 exact full matrix를 다시 실행한다. Safari 지원 축소가 제품 결정이라면 코드부터 줄이지 말고 별도 결정으로 PRD를 먼저 변경해야 한다.
**판정 기록**
- 2026-07-31 — 요구사항, 과거 Gate 완료 기록, 현재 config/README와 실제 project 수집 실패를 대조해 확정.
- 2026-07-31 — 기존 완료 checklist를 되돌리지 않고 신규 `P9-R7`로 전환.
- 2026-07-31 — `P9-R7`에서 `webkit`·`mobile-safari` project를 복원하고 full mock/server/static Gate를 재실행해 수정 완료로 전환.
- 2026-07-31 — 후속 제품 결정으로 현재 지원·자동 검증 범위를 Chromium/mobile Chrome으로 축소했다. 이 finding은 과거 이력으로 보존한다.
**수정 후 검증 기록**
- RED: `npx playwright test --list --project=webkit``npx playwright test --list --project=mobile-safari`는 project-not-found로 실패해 자동 Gate 축소를 재현했다.
- Unit/static: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`는 1 file / 4 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다.
- E2E: 최종 `npm run e2e:mock`은 Chromium 52 passed, WebKit 34+10 passed / 8 skipped, Mobile Chrome 47 passed / 5 skipped, Mobile Safari 34+9 passed / 9 skipped로 0 failure였다. `npm run e2e`는 server allowlist 36 passed였다.
- 범위 분리: skipped 항목은 기존 keyboard-only/WebKit tab focus 또는 Chromium 전용 audio metadata case로 제한했고, 실제 Edge·Safari 기기 최신 2개 major 확인은 릴리스 QA 범위로 유지한다.
### Phase 9 결론
- **자동 검증:** unit은 두 묶음 합계 77 files / 387 tests passed였고, `P9-R7` focused docs test는 1 file / 4 tests passed였다. `typecheck`·`lint`·build exit 0, 당시 exact mock E2E와 server E2E는 통과했다. 현재 지원 Gate는 Chromium/mobile Chrome만 대상으로 한다.
- **판정:** `REV-P9-007`은 과거 `P9-R7`에서 수정 완료됐고, 후속 제품 결정으로 Safari/WebKit은 지원 범위에서 제외됐다.
- **남은 위험:** 기존 실제 개발 API QA만 별도다.
## 14. 요청 기준 재리뷰 — 2026-07-31
### 실행 결과
| 명령 또는 검증 | 결과 | 판정 |
|---|---|---|
| `npm run test:run` | 실패 | 78 files 중 2 failed / 76 passed, 394 tests 중 2 failed / 392 passed |
| focused mock 문서·mode test | 실패 | 2 files, 2 failed / 5 passed로 동일 원인 재현 |
| `npm run typecheck`, `npm run lint` | 성공 | 모두 exit 0 |
| `npm run build:dev`, `npm run build:prod` | 성공 | 각 256 modules, 기존 502.94kB chunk warning만 존재 |
| `npm run e2e` | 성공 | 4 projects / 36 passed |
| `npm run e2e:mock` | 성공 | 4 projects / 186 passed / 22 skipped / 0 failed |
| OpenAPI·금지 패턴·diff 정적 검사 | 성공 | 2.3.0, 25 paths / 37 implemented operations, 누락 schema ref·production 금지 패턴·diff 오류 0건 |
### `REV-P9-008` — 분할 mock E2E script가 문서·mode contract와 불일치해 전체 unit Gate가 실패함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `MOCK-001~004`, PRD §13 browser Gate, `P9-GATE` 전체 자동 검증 0 failure
- **관련 계약:** API payload 영향 없음. npm script의 bare/full·filtered/focused 실행 계약
- **소유 Task:** 신규 `P9-R8`
**관찰 내용**
`P9-R7`은 bare `npm run e2e:mock`을 네 browser project의 분할 script로 실행하고 인자가 있으면 Playwright에 전달하는 wrapper로 변경했다. README는 여전히 direct `VITE_API_MODE=mock playwright test`라고 설명하고, `mode-boundary.test.ts`도 같은 direct 문자열만 허용한다. `mock-preview-docs.test.ts`는 변경된 실제 script 전체가 README에 있어야 한다고 요구하므로 두 test가 동시에 실패한다.
**근거**
- 코드: `package.json:17~24`
- 문서: `README.md:40~41`, `docs/agent-guide/scripts.md:12~13`
- 테스트: `src/shared/mocks/__tests__/mode-boundary.test.ts:20~34`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts:23~36`
- 실행: 전체 unit 2 failed / 392 passed, focused 실행 2 failed / 5 passed
- 반대 근거: `npm run e2e:mock -- --list --project=chromium` 52 tests 정상 수집, exact mock matrix 186 passed / 22 skipped / 0 failed
**재현 또는 검증 절차**
1. `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`를 실행한다.
2. mode test가 direct script 기대와 wrapper 차이로 실패하는지 확인한다.
3. docs test가 README에 wrapper 전체 문자열이 없어서 실패하는지 확인한다.
4. `npm run e2e:mock -- --list --project=chromium`으로 filtered 인자 전달 자체는 성공하는지 구분한다.
**영향**
제품 runtime과 브라우저 journey는 통과하지만 `P9-GATE`의 전체 unit 0 failure 조건이 깨지고, README 사용자는 bare 명령이 분할 full matrix를 실행한다는 사실을 알 수 없다.
**권장 조치**
bare/full과 filtered/focused의 공개 동작을 README·agent guide 한 기준으로 문서화하고, contract test는 긴 내부 shell 문자열 복제 대신 해당 public semantics와 `VITE_API_MODE=mock` 경계를 검증한다. Safari project나 E2E spec을 줄이지 않는다.
**판정 기록**
- 2026-07-31 — 전체·focused unit에서 동일 2건을 재현하고 package/README/test 변경 이력을 역추적해 확정.
- 2026-07-31 — runtime 인자 전달과 exact 4-project matrix는 통과해 기능 결함이 아닌 Low 문서·Gate 회귀로 판정.
- 2026-07-31 — `plan-task.md` 신규 `P9-R8`로 전환. 애플리케이션 코드는 수정하지 않음.
- 2026-07-31 — `P9-R8`에서 script/document contract test와 README·agent guide를 public 실행 의미 기준으로 정렬하고 full unit Gate를 복구해 수정 완료.
### 종료 판정
- **최종 결론:** `REV-P9-008` 수정 완료. Phase 9 unit Gate는 복구됐고 mock/server E2E pass 기록은 유지된다.
- **남은 항목:** 실제 개발 API 수동 QA.
## 15. P9-R8 수정 후 검증 — 2026-07-31
- 무엇을: `e2e:mock` wrapper를 direct script 문자열로 고정하던 test와 README/agent guide 설명을 public 실행 의미 기준으로 정렬했다.
- 왜: `P9-R7`에서 복원한 4-project mock matrix wrapper가 실제로는 통과하지만, unit contract와 README가 서로 다른 문자열을 기대해 전체 unit Gate가 실패했기 때문이다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 failed / 5 passed.
- GREEN/REFACTOR: focused script/docs contract는 2 files / 7 tests passed. `npm run e2e:mock -- --list --project=chromium`은 filtered 인자 전달로 52 tests를 수집했다. 내부 wrapper 전체 문자열을 README에 복제하지 않고, raw direct script와 분할 wrapper 책임을 test에서 분리했다.
- Unit Gate: `production-graph.test.ts``audio-list.test.tsx`, `community-sheet.test.tsx`의 부하성 timeout을 단독 재현으로 분리하고 integration test timeout만 명시했다. 최종 `npm run test:run`은 78 files / 394 tests passed였다.
- 정적 검증: `npm run typecheck`, `npm run lint`, 관련 `git diff --check`는 모두 exit 0이었다. E2E full matrix는 이번 요청 기준 재리뷰 직전 `npm run e2e:mock` 186 passed / 22 skipped / 0 failed와 `npm run e2e` 36 passed 기록을 보존하고, 개발 중 반복 E2E를 줄이라는 지시에 따라 재실행하지 않았다.
## 16. 최종 재검증 — 2026-07-31
- **검토 범위:** responsive/accessibility, error recovery, mock/server boundary, browser matrix와 전체 자동 Gate.
- **실행 증거:** `test:run` 78 files / 394 tests, `e2e` 36 tests, `e2e:mock` 186 passed / 22 skipped, typecheck·lint·개발/운영 build가 통과했다. UI/UX 기준 검색 결과 중 PRD와 충돌하는 dark/OLED 제안은 적용하지 않고 밝은 token, focus, keyboard, live error, 320px/200% zoom 기준만 대조했다.
- **판정:** Phase 9 소유의 별도 확정 신규 발견 사항 없음. 인증·upload lifecycle 두 건은 root-cause 파일 소유 Phase의 `REV-P1-018`/`P1-R12`, `REV-P4-011`/`P4-R8`로만 전환한다.
- **남은 위험:** 실제 개발 API와 위 두 신규 회귀 Task. Edge/Safari는 현재 지원 범위에서 제외한다.
- **신규 Phase 9 Task:** 없음.
## 17. 2026-07-31 문서 기준 재리뷰
### 검토 범위와 제외
- **검토:** Phase 0~10 공통 unit·type·lint·build·server/mock E2E Gate, responsive/accessibility test 구성, Vitest 격리·cleanup과 실패 후보 spec을 current working tree에서 재실행·대조했다.
- **제외:** 실제 Edge·Safari 기기 매트릭스와 개발 API credential이 필요한 수동 QA는 기존 대기 범위로 유지했다.
### `REV-P9-009` 전체 unit Gate의 비결정적 loading·timeout 실패
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | Phase 9 Gate 0 failure, 리뷰 가이드 §3·5 실제 검증 증거 |
| 소유 Task | `P9-R9` |
**근거·재현**
- fresh `npm run test:run -- --reporter=verbose`는 exit 1, 79 files 중 5 failed / 74 passed, 397 tests 중 5 failed / 392 passed, 717.76초였다.
- 실패는 `CharacterWorkspaceLayout.test.tsx`의 detail loading 잔류, `community-sheet.test.tsx``fan-talk-reply.test.tsx`의 list loading 잔류, `series-routes.test.tsx`의 5초 timeout 등으로 나타났다. 이전 fresh full 실행에서는 Character mutation 등 실패 집합이 달라져 단일 기능 assertion의 재현성은 없었다.
- 실패 후보 5개를 같이 실행한 `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/series/tests/series-routes.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters/tests/character-mutation-reload.test.tsx src/features/fan-talks/tests/fan-talk-reply.test.tsx`는 exit 0, 5 files / 25 tests passed였다.
- full output에는 touched integration spec의 React `act(...)` 미적용 warning과 loading 완료 전 assertion이 함께 관찰됐다. 단독 통과이므로 이를 제품 기능 실패로 판정하지는 않았다.
**영향**
- Phase 9 Gate의 “전체 unit 0 failure” 조건이 현재 재현 가능하게 충족되지 않아 신규 수정의 안전한 release 회귀 기준으로 사용할 수 없다. 제품 data 손실·인증 우회는 이 항목에서 확인되지 않았다.
**권장 조치·판정 기록**
- 전역 timeout 상향·skip·커버리지 축소로 가리지 말고, request·timer·history listener·React update의 완료·cleanup 경계를 축소 재현한 뒤 최소 수정한다.
- 2026-07-31 — 두 차례 full 실행과 5-spec focused 실행을 대조해 Medium Gate 회귀로 확정. 완료된 `P9-R8`을 열지 않고 신규 `P9-R9`로 전환했다. 제품·test 코드는 수정하지 않았다.
- 2026-07-31 — `P9-R9`에서 Character create mutation reload test가 보호 route probe 완료 전 form field를 조회하던 비동기 대기를 `findByLabelText`로 맞췄다. focused 1 file / 3 tests passed, 전체 unit은 2회 연속 81 files / 409 tests passed로 수정 완료했다.
### `REV-P9-010` bare mock WebKit Gate의 navigation·mutation 대기 비결정성
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD §14.2 4-browser matrix, `P9-T2`, `P9-GATE` |
| 소유 Task | `P9-R10` |
**근거·재현**
- fresh bare `npm run e2e:mock`은 Chromium 52 passed 후 WebKit main에서 2 failed / 32 passed / 7 skipped로 exit 1이었고, 후속 WebKit tail·Mobile Chrome·Mobile Safari segment를 실행하지 못했다.
- Character 실패는 `page.goto('/ai-characters/new')`가 이전 login의 `/ai-characters` navigation에 의해 interruption된 경합이었다. FanTalk 실패는 기존 reply PUT 후 `답변이 수정되었습니다.` success status를 5초 안에 찾지 못했으며 동시에 목록 URL navigation 완료를 대기하고 있었다.
- 동일 WebKit 두 test만 실행한 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/fan-talk.spec.ts --project=webkit --workers=1 --grep "direct create and edit routes|FanTalk mock journey"`는 exit 0, 2 passed / 28.2초였다.
**영향·권장 조치·판정 기록**
- 제품 기능 실패는 focused에서 재현되지 않았지만 bare release Gate가 중간에 중단되어 4-browser 증거를 새로 생성할 수 없으므로 Medium으로 판정했다.
- login URL만이 아니라 보호 route ready까지 대기하고, FanTalk request→success→refetch/navigation의 시각 상태를 명시적으로 순서화한 뒤 WebKit 반복·bare full matrix를 재검증한다.
- 2026-07-31 — bare 실패와 동일 focused 통과를 대조해 Gate 비결정성으로 확정. 신규 `P9-R10`으로 전환했고 제품·E2E 코드는 수정하지 않았다.
- 2026-07-31 — 사용자 지시에 따라 추가 E2E는 Chromium·Mobile Chrome만 실행했다. `P9-R10`에서 login helper가 보호 route heading까지 기다리게 하고 FanTalk 수정 뒤 list-backed 버튼 상태를 기다리게 해 focused Chromium 2 passed, Mobile Chrome 2 passed로 수정 완료했다.
### 재리뷰 검증·종료 판정
- **통과:** `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` exit 0. server E2E는 Chromium·WebKit·Mobile Chrome·Mobile Safari 36 passed였다. build는 503.04kB chunk warning만 표시했다.
- **실패:** 전체 unit 5 failed / 392 passed, bare mock E2E는 Chromium 52 passed 후 WebKit main 2 failed / 32 passed / 7 skipped로 중단. unit 실패 후보 5-spec focused 25 passed, WebKit 실패 2-test focused 2 passed였다.
- **판정:** Medium 2건을 확정해 `P9-R9~R10`으로 전환했고, 둘 다 수정 완료했다. 오탐·보류로 남은 후보는 없다.
- **남은 위험:** WebKit·Mobile Safari 자동 E2E는 2026-07-31 사용자 지시에 따라 실행하지 않았다. 실제 browser 기기·개발 API 수동 QA는 계속 대기 상태다.
## 18. 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- `P9-R9~R10`, PRD §13, Playwright project, npm wrapper, README·agent guide와 contract test를 current staged working tree에서 대조했다.
- 전체 `npm run test:run` — 두 차례 연속 각각 81 files / 409 tests passed. docs contract는 2 files / 7 tests passed했고 typecheck·lint·개발/운영 build·staged diff check도 통과했다.
- `npm run e2e:mock -- --list` — Chromium/mobile Chrome만 12 files / 104 tests 수집. `npm run e2e -- --list` — 같은 두 project만 4 files / 18 tests 수집.
- 수정된 Character/FanTalk focused mock E2E는 Chromium·mobile Chrome 합계 4/4 passed였다. WebKit·Mobile Safari는 지원 범위에서 제외해 실행하지 않았다.
### `REV-P9-011` — Chromium-only 결정과 현재 문서·smoke 경로가 불일치함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD §13·Decision Log, Chromium/mobile Chrome 지원 결정, `P9-T2`, 현재 `P9-GATE` |
| 소유 Task | 신규 `P9-R11` |
**근거·재현**
- executable 설정은 `playwright.config.ts:34~37`의 두 project, `package.json:17~20`의 두 분할 script로 정정됐고 contract test도 WebKit/mobile Safari 부재를 검증한다.
- 반면 `plan-task.md``P9-R8` 기대 결과는 여전히 “mock matrix 4 projects”, 완료 기록은 “4-project 분할 script”를 현재 의미처럼 사용한다.
- `P9-R10` 제목·Goal·실행 명령·기대 결과는 WebKit 반복과 bare 4-project matrix를 요구하면서 같은 Task의 완료 기록은 Chromium/mobile Chrome만 실행했다고 적는다.
- 이 리뷰의 직전 최신 결론도 관련 요구사항을 “4-browser matrix”로 두고 WebKit/Mobile Safari 미실행을 남은 위험으로 표시한다.
- PRD §13은 지원 범위를 Chrome/mobile Chrome으로 바꿨지만 Decision Log는 2026-07-29에서 끝나 변경 날짜·사유·Playwright/QA 영향 범위를 기록하지 않았다.
- `P9-R9` server smoke 명령은 존재하지 않는 `tests/e2e/server-boundary.spec.ts`를 사용한다. 실제 spec은 `tests/e2e/server-mode-boundary.spec.ts`다.
**영향·권장 조치**
현재 실행 코드는 두 project로 일치하지만 다음 에이전트가 완료 Task의 실행 계약을 기준으로 WebKit/mobile Safari를 복원하거나 존재하지 않는 smoke spec을 실행할 수 있다. PRD Decision Log에 Chrome-only 결정의 사유·영향을 추가하고, 과거 pass/failure 수치는 삭제하지 않은 채 당시 이력으로 표시하며 현재 실행 명령·기대 결과·최신 결론·smoke 경로와 docs contract를 정정한다.
**판정 기록**
- 2026-07-31 — 현재 config/script/test의 104·18 test 수집, PRD Decision Log, `P9-R8~R10`·최신 리뷰 문구와 실제 spec 경로를 대조해 Low 확정.
- 2026-07-31 — 완료된 `P9-R8/R10`을 다시 열지 않고 신규 `P9-R11`로 전환. 코드·test·설정은 수정하지 않았다.
- 2026-07-31 — `P9-R11`에서 PRD Decision Log, 현재 Task 정의, docs contract와 최신 결론을 Chromium/mobile Chrome 기준으로 정렬해 수정 완료.
### 종료 판정
- `P9-R9`은 전체 unit 두 차례 연속 409/409로, `P9-R10`의 현재 지원 project 흐름은 focused E2E 4/4로 유지됐다.
- **최종 결론:** `REV-P9-011`/`P9-R11` 문서 계약 수정 완료. 현재 자동 Gate 기준은 Chromium/mobile Chrome이며 실제 개발 API 수동 QA는 별도다.
## 19. P9-R11 수정 후 검증 기록 — 2026-07-31
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 failed / 4 passed. 신규 docs contract가 현재 `P9-R8~R11` Task 정의의 `mock matrix 4 projects`, `--project=webkit`, 잘못된 `server-boundary.spec.ts` 경로를 검출했다.
- GREEN/REFACTOR: PRD Decision Log에 Chrome-only 자동 Gate 결정을 추가하고, plan의 현재 Task 정의와 Phase 9 최신 결론을 Chromium/mobile Chrome·`server-mode-boundary.spec.ts` 기준으로 정정했다. 과거 네 browser project pass/failure 기록은 검증 이력으로 보존했다.
- 검증: docs contract 2 files / 8 tests passed, mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome에서만 수집됐다. 실제 E2E 실행은 사용자 지시에 따라 생략했다.
## 20. P9-R11 수정 결과 재점검 — 2026-07-31
### `REV-P9-012` — Chromium-only 현재 문서 계약의 종료 증거가 불완전함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD Decision Log, `P9-R11` 완료 증거, docs contract 재현성 |
| 소유 Task | 신규 `P9-R12` |
**근거·영향**
- PRD의 2026-07-31 결정 행은 날짜와 Playwright·QA 영향 범위는 기록하지만 Chrome-only로 축소한 사용자 결정 사유를 명시하지 않는다.
- `P9-R10`의 현재 수동 확인은 여전히 “자동 WebKit harness 결정성 Task”라고 쓰고, 본 리뷰 상단 metadata도 `P9-R11` 수정 필요로 남았다. `P9-R11` 기대 결과는 2 files / 7 tests지만 실제 결과는 8 tests다.
- docs contract는 올바른 server spec 경로를 `P9-R9` block이 아니라 plan 전체에서 찾고 최신 결론부터 EOF까지 검사해 과거/후속 이력의 우연한 match로 통과할 수 있으며 `P9-R11` 종료 상태를 직접 고정하지 않는다.
**권장 조치·판정 기록**
- Decision Log 사유, 현재 Task·리뷰 metadata·기대 test 수를 정정하고 contract가 `P9-R9`/최신 결론의 정확한 section만 검사하게 한다.
- 2026-07-31 — executable config와 104·18 list는 정상임을 분리하고 Low 문서/test 계약으로 확정. 완료된 `P9-R11`을 다시 열지 않고 신규 `P9-R12`로 전환했다.
### 종료 판정
- Playwright는 Chromium/mobile Chrome만 수집하고 잘못된 `server-boundary.spec.ts` 실행 경로는 현재 Task에서 제거됐다.
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list`는 Chromium/mobile Chrome 104 tests, `npm run e2e -- --list`는 Chromium/mobile Chrome 18 tests만 수집했다.
- **최종 결론:** `REV-P9-012`/`P9-R12` 수정 완료. WebKit·Mobile Safari는 현재 지원 범위가 아니다.
## 21. P9-R12 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- PRD §13·§16, `P9-R9~R12`, Phase 9 최신 review section, `mock-preview-docs.test.ts`와 실제 Playwright project를 대조했다.
- docs contract 2 files / 8 tests가 통과했고 `npm run e2e:mock -- --list`는 104 tests, `npm run e2e -- --list`는 18 tests를 Chromium/mobile Chrome에서만 수집했다. Safari 계열 project는 실행하지 않았다.
### `REV-P9-013` — Decision Log 사유와 docs contract section 경계가 불완전함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD §13·§16, `P9-R12` 완료 증거, docs contract 재현성 |
| 소유 Task | 신규 `P9-R13` |
**근거·영향**
- PRD Decision Log는 축소가 사용자 직접 지시라는 사실만 적고, 실제 지원 대상이 Chrome 2종뿐이며 불필요한 Safari 계열 실행이 테스트 시간을 크게 늘린다는 결정 근거를 남기지 않았다.
- `mock-preview-docs.test.ts`는 위 사유를 PRD 전체 문자열로 검사하고 Phase 9 review를 특정 heading부터 EOF까지 잘라 검사한다. 후속 review에 같은 문자열이 추가되면 현재 Decision Log나 종료 판정이 stale해도 우연히 통과할 수 있다.
- 현재 Playwright project와 수집 결과는 정상이며 제품·E2E 설정 결함은 아니다. 문제 범위는 결정 추적성과 회귀 test의 section 경계다.
**권장 조치·판정 기록**
- PRD Decision Log에 지원 범위와 테스트 시간 근거를 명시하고, heading level 또는 명시적 종료 heading으로 PRD Decision Log와 최신 Phase 9 review/종료 판정만 추출해 검사한다.
- 2026-07-31 — source scope와 사용자 결정 근거를 원문 대조해 Low로 확정. 완료된 `P9-R12`를 다시 열지 않고 신규 `P9-R13`으로 전환했다.
### 종료 판정
- Chromium/mobile Chrome 2-project 실행 계약과 `P9-R12`의 현재 기능 결과는 유지된다.
- **최종 결론:** `REV-P9-013` 후속 goal 필요. `P9-R13`을 plan에 추가했다.
## 22. P9-R13 수정 결과 검증 — 2026-07-31
### 검토 범위와 실행 증거
- PRD §16 Decision Log의 Chrome-only 결정 행, `P9-R13` plan block, Phase 9 최신 review section, `mock-preview-docs.test.ts`를 대조했다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 2 failed / 3 passed. PRD 결정 행의 `Chrome 2종`·`테스트 시간` 근거 누락과 `## 22. P9-R13 수정 결과 검증` section 부재를 검출했다.
- GREEN: PRD Decision Log에 Chrome 2종 지원 범위와 Safari 계열 실행의 테스트 시간 영향을 명시하고, docs contract가 PRD Decision Log와 Phase 9 최신 section만 검사하도록 닫았다.
### 종료 판정
- **최종 결론:** `REV-P9-013`/`P9-R13` 수정 완료. Chrome 2종과 테스트 시간 근거가 PRD Decision Log에 남았고, 최신 Phase 9 종료 판정은 후속 review section의 우연한 문자열에 의존하지 않는다.
- **남은 항목:** `P10-R12`, 실제 crop pixel·stale ADMIN server QA, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA.
## 23. P9-R13·P10-R12 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- PRD Decision Log, `mock-preview-docs.test.ts`, Community Sheet focus test와 full unit Gate를 current working tree에서 대조했다.
- docs contract 2 files / 8 tests, `typecheck`, `lint`, 개발/운영 build와 diff 검사는 통과했다. Playwright 목록은 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
- 첫 fresh full unit은 Community Sheet focus assertion 1 failed / 416 passed, 두 번째 fresh full은 81 files / 417 tests passed였다. 동일 focus test 단독 5회는 모두 통과했다.
### `REV-P9-014` — Community Sheet focus 반환 test가 full load에서 경합함
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | Phase 9 전체 unit Gate, keyboard·focus 회귀, `P9-R9` 결정성 완료 증거 |
| 소유 Task | 신규 `P9-R14` |
**근거·영향**
- `community-sheet.test.tsx:82`는 비활성화 dialog 취소 직후 trigger focus를 동기 assertion한다. 공통 `ConfirmDeactivateDialog` test는 같은 effect cleanup focus 반환을 `waitFor`로 관찰한다.
- full load에서는 부모 Community Sheet의 refetch/remount focus effect와 중첩 dialog cleanup이 경합해 `닫기` button이 focus를 받았고, 단독 5회와 다음 full에서는 통과했다.
- 제품 focus 구현 변경의 증거는 없지만 동일 working tree의 full Gate가 통과와 실패를 오가므로 완료 증거를 신뢰할 수 없다.
**권장 조치·판정 기록**
- 공통 dialog test와 동일하게 effect 완료를 기다리는 최소 `waitFor`로 assertion을 맞추고 임의 timeout·retry 없이 focused 반복 5회와 full unit 2회로 검증한다.
- 2026-07-31 — fresh full 실패, focused 5회 통과, 다음 fresh full 통과를 대조해 Medium 비결정성으로 확정. 완료된 `P9-R9`을 다시 열지 않고 신규 `P9-R14`로 전환했다.
- 2026-07-31 — `P9-R14`에서 제품 focus 구현은 변경하지 않고 Community Sheet test의 취소 후 trigger focus assertion만 `waitFor`로 맞췄다. focused 반복 5회는 모두 1 passed / 6 skipped, full unit 2회는 모두 81 files / 417 tests passed였다. `typecheck`, `lint`, `build:dev`, `build:prod`, targeted `git diff --check`도 통과했다.
### `REV-P9-015` — PRD Decision Log·최신 종료 판정이 정확한 heading에서 닫히지 않음
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P9-R13` 완료 증거, docs contract section 재현성 |
| 소유 Task | 신규 `P9-R15` |
**근거·영향**
- `mock-preview-docs.test.ts:91`은 PRD Decision Log를 `sectionFrom()`으로 EOF까지 읽고, 115행의 최신 종료 판정도 이미 H2까지만 닫힌 block 안에서 다음 H3를 구분하지 않고 끝까지 읽는다.
- 메모리 negative-control에서 Chrome-only 결정 행을 후속 H2로 옮기거나 완료 문구를 후속 H3로 옮겨도 현재 assertion이 통과했다.
- 실제 PRD 결정 내용과 현재 P9-R13 결론은 올바르지만, 후속 section이 추가되면 stale 현재 section이 우연히 통과할 수 있다.
**권장 조치·판정 기록**
- heading 행을 anchor하고 동일·상위 level의 다음 heading에서 닫는 helper 하나로 PRD H2, 최신 Phase 9 H2와 종료 판정 H3를 각각 추출한다. synthetic 후속 H2/H3 negative test를 남긴다.
- 2026-07-31 — 결정 행·완료 문구 이동 negative-control이 모두 통과함을 재현해 Low로 확정. 완료된 `P9-R13`을 다시 열지 않고 신규 `P9-R15`로 전환했다.
- 2026-07-31 — `P9-R15`에서 heading level 기반 helper로 PRD Decision Log와 Phase 9 종료 판정 section을 닫고 synthetic 후속 H2/H3 negative assertion을 남겼다. docs contract 2 files / 8 tests, mock/server Playwright list, Markdown link 검사와 targeted diff check가 통과했다.
### 종료 판정
- Chrome 2종 지원 결정과 Playwright 2-project 설정 자체는 유지된다.
- **최종 결론:** `REV-P9-014~015`/`P9-R14~R15` 수정 완료. 다음 순서는 Phase 10 `P10-R13`이다.
## 24. P9-R14~R15 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- Community Sheet focus assertion, Markdown section helper, Phase 9 최신 H2와 `P9-R14~R15` 완료 기록을 current working tree에서 대조했다.
- focused Community Sheet test는 5회 모두 1 passed / 6 skipped였고 fresh full unit은 81 files / 418 tests passed였다. finding 기록 전 docs contract는 2 files / 9 tests, `typecheck`, `lint`, 개발/운영 build도 통과했다. finding과 신규 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다.
- Playwright `--list`는 mock 104 tests와 server 18 tests를 Chromium/mobile Chrome에서만 수집했다. WebKit·Mobile Safari는 실행하지 않았다.
- negative control에서 목표 heading 뒤 임의 suffix를 붙인 H2가 exact target으로 선택됐고, 실제 contract가 최신 Phase 9 H2가 아니라 과거 `## 22. P9-R13 수정 결과 검증`을 계속 검사함을 확인했다.
### `REV-P9-016` — exact heading contract가 임의 suffix와 과거 Phase 9 H2를 허용함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | `P9-R15` exact heading anchor·최신 종료 판정 완료 증거 |
| 소유 Task | 신규 `P9-R16` |
**근거·영향**
- `sectionAtHeading()``line === heading` 외에 ``line.startsWith(`${heading} `)``도 허용한다. 날짜 suffix만을 위한 조건이 아니라 임의 suffix를 모두 받으므로 다른 H2를 목표 heading으로 오인할 수 있다.
- Phase 9 contract는 현재 최신 H2가 아닌 과거 `## 22. P9-R13 수정 결과 검증`과 그 종료 판정을 추출한다. 이후 `P9-R14~R15` 완료 또는 신규 finding이 stale해도 현재 계약은 이를 읽지 않는다.
- 제품 focus 동작, Chrome 2종 지원 결정과 Playwright 설정에는 신규 결함이 없지만 문서 완료 증거는 후속 section에서 false positive가 가능하다.
**권장 조치·판정 기록**
- helper를 전체 heading 행 exact match로 축소하고 호출부가 날짜를 포함한 실제 최신 H2를 명시하게 한다. 최신 H2 내부 종료 판정도 별도로 닫고 임의 suffix·과거 H2 negative assertion을 남긴다.
- 2026-07-31 — synthetic heading collision과 실제 H2 대상 불일치를 재현해 Low로 확정. 완료된 `P9-R15`를 다시 열지 않고 신규 `P9-R16`으로 전환했다.
- 2026-07-31 — `P9-R16`에서 `sectionAtHeading()`을 전체 heading 행 exact match로 축소하고 Phase 9 contract 대상을 최신 `## 24. P9-R14~R15 수정 결과 재점검 — 2026-07-31`와 내부 `### 종료 판정`으로 옮겼다. 임의 suffix·후속 H2/H3 false positive assertion을 남겨 수정 완료로 판정했다.
- 수정 후 검증: docs contract 2 files / 9 tests passed, `typecheck`, `lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check` 모두 exit 0. Playwright `--list`는 mock 104 tests와 server 18 tests를 Chromium/mobile Chrome에서만 수집했다.
### 종료 판정
- `REV-P9-014`의 focus test 수정은 focused 5회와 fresh full unit에서 재현됐고 신규 제품 문제는 확인되지 않았다.
- **최종 결론:** `REV-P9-016`/`P9-R16` 수정 완료. `P10-R14` 후속 goal 필요. Chromium/mobile Chrome 2-project 지원 범위는 유지한다.
## 25. P9-R16·P10-R14 수정 결과 재점검 — 2026-08-01
### 검토 범위와 실행 증거
- `P9-R16` exact heading helper와 Phase 9 H2/H3 negative-control, `P10-R14` 완료 뒤 Phase 9 metadata·최신 결론을 current working tree에서 대조했다.
- finding 기록 전 docs contract 2 files / 9 tests, fresh full unit 81 files / 418 tests, `typecheck`, `lint`가 통과했다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다. 신규 finding과 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다.
- Phase 9 review 상단과 마지막 H2 결론은 완료된 `P10-R14`를 계속 후속 필요로 표시하지만 current test는 metadata를 읽지 않고 `## 24`를 고정 대상으로 사용해 통과했다.
### `REV-P9-017` — Phase 9 current metadata가 완료된 P10-R14를 후속으로 유지함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | review current metadata·최신 판정, `P9-R16` 최신 H2 contract |
| 소유 Task | 신규 `P9-R17` |
**근거·영향**
- Phase 9 상단 `리뷰 상태``## 24` 종료 판정은 `P10-R14` 후속 필요를 유지하지만 plan top과 Phase 10 review는 `P10-R14` 완료를 선언한다.
- docs contract는 Phase 9 metadata를 추출하지 않고 `## 24`를 최신 H2로 고정하며 해당 H2가 문서의 마지막 H2인지도 확인하지 않아 이후 append-only section을 현재 판정으로 사용하지 않는다.
- 제품·Chrome 2종 설정에는 영향이 없지만 다음 실행자가 완료된 `P10-R14`를 다시 열 수 있다.
**권장 조치·판정 기록**
- 과거 `## 24` 결론은 보존하고 현재 상태를 기록하는 새 H2를 append한다. Phase 9 metadata·실제 마지막 H2·내부 종료 판정을 독립 추출해 `P10-R14` 완료와 남은 수동 QA를 검사하고 trailing H2 negative-control을 남긴다.
- 2026-08-01 — plan/Phase 10 완료 상태와 Phase 9 metadata·고정 H2를 대조해 Low로 확정. 완료된 `P9-R16`을 다시 열지 않고 신규 `P9-R17`로 전환했다.
- 2026-08-01 — `P9-R17`에서 metadata·실제 마지막 H2·종료 판정을 동기화하고 docs contract 2 files / 9 tests를 통과해 수정 완료로 판정했다.
### 종료 판정
- `REV-P9-016` exact heading 수정과 negative-control은 fresh docs contract에서 재현됐다.
- **최종 결론:** `REV-P9-017` 후속 goal 필요. `P9-R17`을 plan에 추가했으며 Chromium/mobile Chrome 지원 범위는 유지한다.
## 26. P9-R17 수정 후 검증 — 2026-08-01
### 검토 범위와 실행 증거
- Phase 9 metadata, 실제 마지막 H2와 내부 종료 판정을 독립 scope로 대조하게 했다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps current Phase 9"` — 1 file / 1 failed / 5 skipped. Phase 9 metadata가 `P9-R17` 완료를 담지 않아 실패했다.
- GREEN: `REV-P9-017`/`P9-R17` current-state를 append-only 최신 H2로 기록하고, 과거 `## 24`·`## 25` 판정은 보존했다. focused GREEN은 1 file / 1 passed / 5 skipped, docs contract는 2 files / 9 tests passed였다. Chromium/mobile Chrome 2-project 지원 범위와 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기는 유지한다.
### 종료 판정
- **최종 결론:** `REV-P9-017`/`P9-R17` 수정 완료. `P10-R15` 완료 후 자동 보완 Task는 완료됐고 Chromium/mobile Chrome 2-project 지원 범위는 유지한다. 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.
## 27. P9-R17 수정 결과 재점검 — 2026-08-01
### 검토 범위와 실행 증거
- `P9-R17`의 metadata·실제 마지막 H2·종료 판정 동기화와 docs contract helper를 current working tree에서 재검토했다.
- finding 기록 전 docs contract는 2 files / 9 tests, 전체 unit은 81 files / 418 tests가 통과했고 `typecheck`, `lint`도 exit 0이었다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
- 신규 finding·Task를 current-state에 반영한 뒤 docs contract는 2 files 중 1 failed / 1 passed, 9 tests 중 2 failed / 7 passed의 RED가 됐다. Phase 9 실패는 `P9-R17` 범위가 후속 미완료 Task를 포함한 경계에서 발생했다. 필수 section 12/12, Markdown link broken 0, `git diff --check`는 통과했다.
- `P9-R17` 수정 자체는 재현됐지만 소유 finding 상태와 append-only Task 경계, 최신 H2 helper의 Markdown fence·동일 제목 경계를 contract가 보장하지 않는다.
### `REV-P9-018` — 완료 결론과 소유 finding·Task checklist 범위가 불일치함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | review finding lifecycle, 완료 Task checklist의 독립 범위 |
| 소유 Task | 신규 `P9-R18` |
**근거·영향**
- metadata와 `## 26``REV-P9-017`/`P9-R17` 수정 완료를 선언하지만 `REV-P9-017` 표의 상태는 `확정`으로 남아 있다. 완료 날짜별 근거도 해당 finding의 판정 기록에 없다.
- docs contract는 `REV-P9-017` 표를 검사하지 않으며 `P9-R17` Task를 다음 Phase의 `### Task R10.1`까지 추출한다. 따라서 정상적인 후속 Phase 9 Task append가 완료된 `P9-R17` checklist 실패로 섞인다.
- 제품 동작에는 영향이 없지만 현재 finding 상태와 완료 증거를 자동으로 신뢰할 수 없다.
**권장 조치·판정 기록**
- `REV-P9-017` 상태를 `수정 완료`로 종결하고 2026-08-01 검증 근거를 append한다. `P9-R17` Task는 바로 다음 Task에서 닫아 소유 finding·checklist·metadata·최신 결론을 함께 검사한다.
- 2026-08-01 — 실제 표와 section 범위를 대조해 Low로 확정. 완료된 `P9-R17`을 다시 열지 않고 신규 `P9-R18`로 전환했다.
- 2026-08-01 — `P9-R17` 범위를 다음 H3에서 닫고 `REV-P9-017`·`REV-P9-018` 상태와 날짜별 근거를 직접 검사하게 해 수정 완료로 판정했다.
### `REV-P9-019` — 최신 H2 helper가 fenced heading과 동일 제목을 오인함
| 항목 | 내용 |
|---|---|
| 심각도 | Low |
| 상태 | 수정 완료 |
| 관련 요구사항 | Phase 9 실제 마지막 H2·종료 판정 current-state contract |
| 소유 Task | 신규 `P9-R19` |
**근거·영향**
- `latestSectionAtLevel()`은 fence 상태 없이 문서 뒤에서 `## ` 행을 찾으므로 fenced code 안의 heading 예시를 실제 마지막 H2로 선택한다.
- 찾은 heading 문자열을 `sectionAtHeading()`에 다시 넘기며 첫 exact occurrence를 사용하므로 같은 제목이 반복되면 실제 마지막 section이 아니라 앞선 section을 반환한다.
- 문서가 코드 예시나 정정용 동일 제목을 append하면 current-state assertion이 잘못된 section을 검사하거나 원인과 무관하게 실패한다.
**권장 조치·판정 기록**
- 새 parser dependency 없이 기존 helper가 backtick·tilde fence를 제외하고 찾은 마지막 H2 index에서 section을 직접 추출하게 한다. 두 synthetic negative-control을 남긴다.
- 2026-08-01 — fence와 동일 제목 synthetic 문서로 잘못된 section 선택을 재현해 Low로 확정. `P9-R18` 뒤 신규 `P9-R19`로 전환했다.
- 2026-08-01 — fence 밖 heading index를 한 번 수집해 마지막 실제 H2를 직접 추출하고 backtick·tilde fence와 동일 제목 회귀 2건을 통과해 수정 완료로 판정했다.
### 종료 판정
- `P9-R17`의 metadata·최신 결론 수정과 Chromium/mobile Chrome 2-project 범위는 확인됐다.
- **최종 결론:** `REV-P9-018`~`REV-P9-019` 후속 goal 필요. `P9-R18``P9-R19`를 plan에 추가했으며 실제 crop pixel·stale ADMIN server 및 개발 API 수동 QA는 별도 대기다.
## 28. P9-R18~R19 수정 후 검증 — 2026-08-01
### 검토 범위와 실행 증거
- `P9-R18``P9-R17` Task를 다음 H3에서 닫고 `REV-P9-017`·`REV-P9-018`의 상태·날짜별 근거를 current contract에 포함했다. Phase 9 metadata는 Phase 10 current Task를 중복 소유하지 않는다.
- RED: `keeps current Phase 9` focused test가 `REV-P9-017``확정` 상태로 1 failed / 5 skipped였고, checklist assertion 추가 뒤에는 `P9-R18`~`P9-R19` 미완료 상태로 1 failed / 7 skipped였다.
- `P9-R19` RED는 fenced heading과 동일 제목 synthetic 2건이 모두 실패했다. 실제 마지막 H2 index를 직접 사용하도록 최소 수정한 GREEN은 2 passed / 6 skipped였다.
- GREEN/회귀: Phase 9 current-state와 두 synthetic focused는 3 passed / 5 skipped, docs contract는 2 files / 12 tests passed였다.
- 전체 검증: 81 files / 421 tests, `typecheck`, `lint`, 필수 section·Markdown link·diff 검사가 통과했다. Playwright 목록은 Chromium/mobile Chrome만 mock 104 tests와 server 18 tests를 수집했다.
### 종료 판정
- **최종 결론:** `REV-P9-018`~`REV-P9-019`/`P9-R18`~`P9-R19` 수정 완료. Chromium/mobile Chrome 2-project 지원 범위는 유지하며 자동 보완 Task는 완료됐다. 실제 crop pixel·stale ADMIN server 확인과 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남았다.

View File

@@ -0,0 +1,90 @@
# Phase 0 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 0 / `P0-T1`~`P0-GATE`, `P0-R1` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 프로젝트 설정·실행 기반과 Phase 0 완료 기록이 현재 작업 트리에서도 유지되는지 확인한다.
- 후속 Phase 실패를 Phase 0 결함으로 잘못 귀속하지 않는다.
### 포함 범위
- 코드·설정: root package, Vite/TypeScript/Vitest/Playwright 설정, `src/app`, runtime env
- 테스트: Phase 0 unit, typecheck, lint, build 입력
- 문서: Phase 0 Task·Gate·회귀 기록
- 수동 검증: 없음. 브라우저 UI는 Phase별 Playwright 검증으로 대체했다.
### 제외 범위
- Phase 1 이후 도메인 동작과 실제 개발 API server fixture
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다. 요구사항·계약 위반, 실행 불능, 기록과 실제 결과 불일치를 우선 판정했다.
## 4. 검토한 근거
### 문서와 코드
- 계획: `P0-T1`, `P0-T2`, `P0-GATE`, `P0-R1`
- 코드·설정: `package.json`, `vite.config.ts`, `vitest.config.ts`, `playwright.config.ts`, `src/shared/config/env.ts`
- 테스트: `src/app/App.test.tsx`, `src/shared/config/env.test.ts`, `tests/e2e/smoke.spec.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: unit은 stub env, mock E2E는 VITE_API_MODE=mock
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` | 성공 | exit 0 |
| `npm run lint` | 성공 | exit 0 |
| `npm run test:run` | 실패 | 66 files, 290 tests 중 6 failed; 모두 Phase 10 오류 문구 회귀로 판정 |
| `git diff --check` | 성공 | exit 0 |
## 5. 발견 사항 요약
**확정 발견 사항 없음.** 전체 unit 실패 6건은 Phase 0 설정 문제가 아니라 `REV-P10-001`로 전환했다.
## 6. 발견 사항 상세
전환할 Phase 0 발견 사항이 없다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Phase 0 설정·테스트·기록 대조 |
| 후보 항목 판정 완료 | 충족 | Phase 10 소유 실패로 분리 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 없음 |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 기록 |
**최종 결론:** 확정 발견 사항 없음.
**남은 항목:** 전체 unit Gate는 `REV-P10-001` 수정 뒤 재실행한다.
## 9. 수정 후 검증 기록
수정 goal이 없어 기록 없음.

View File

@@ -0,0 +1,189 @@
# Phase 1 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / 공통 API·인증·file/crop 기반 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `AUTH-001~013`, `FILE-001~015`, 공통 API envelope와 crop 계약을 현재 구현과 대조한다.
- 기존 `review-phase-0-1.md`에서 수정 완료된 항목은 보존하고 새 회귀만 판정한다.
### 포함 범위
- 코드: `src/shared/api`, `src/shared/lib/crop-image.ts`, `src/shared/ui/image-crop-dialog.tsx`, 인증 저장소
- 테스트: 공통 API 인증·오류, crop 계산·Dialog
- 문서: Phase 1 Task·Gate와 PRD 인증/file 기준
- 수동 검증: 코드 계산과 keyboard/pointer control 정적 대조
### 제외 범위
- resource별 form과 multipart serializer, 실제 모바일 pinch 기기 QA
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 보안·데이터 손실 또는 핵심 흐름 불능 |
| High | 확정 요구사항·계약 위반 또는 주요 회귀 |
| Medium | 제한 조건의 기능·접근성·복구 문제 |
| Low | 비핵심 UX·문서 정합성 |
상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUTH-005`, `FILE-012`, PRD `10.4`
- 코드: `src/shared/api/client.ts:92`, `src/shared/api/client.ts:102`, `src/shared/api/client.ts:122`, `src/shared/lib/crop-image.ts:67`, `src/shared/lib/crop-image.ts:69`, `src/shared/ui/image-crop-dialog.tsx:38`, `src/shared/ui/image-crop-dialog.tsx:154`
- 테스트: `src/shared/api/__tests__/client-auth.test.ts`, `src/shared/lib/crop-image.test.ts`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: unit stub/MSW
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts` | 성공 | 4 files / 27 tests passed |
| `node` crop 계산 | 실패 재현 | 600px 원본, zoom 1.5에서 source crop 400px, output 600px, 1.5배 확대 |
| 코드 경로 대조 | 실패 재현 | 401 처리가 정상 오류 envelope parse 뒤에만 존재 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-011` | High | 수정 완료 | 비정상 envelope·JSON인 401은 session을 제거하지 않는다 | `P1-T2` | `P1-R6` |
| `REV-P1-012` | High | 수정 완료 | zoom crop이 선택 영역보다 큰 결과를 만들어 no-upscale을 위반한다 | `P1-T5` | `P1-R7` |
| `REV-P1-013` | Medium | 수정 완료 | crop Dialog에 pinch/zoom 입력이 없다 | `P1-T5` | `P1-R7` |
## 6. 발견 사항 상세
### REV-P1-011 — 비정상 401에서 인증 만료 처리가 누락된다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-005`
- **관련 계약:** 보호 요청의 HTTP 401
- **소유 Task:** 신규 `P1-R6`
**관찰 내용**
`response.json()` 실패와 envelope schema 실패는 즉시 공통 `ApiError`를 던진다. session clear와 login 이동은 정상 오류 envelope가 parse된 뒤의 `!apiResponse.success` 분기에서만 실행된다.
**재현 또는 검증 절차**
1. 인증이 필요한 요청에 session을 저장한다.
2. server가 body 없는 401, non-JSON 401 또는 malformed envelope 401을 반환하게 한다.
3. 현재 결과는 status 401 오류만 발생하고 session clear/redirect callback은 0회다.
4. 요구 결과는 body 형태와 무관하게 보호 요청 401에서 1회 session clear와 login 이동이다.
**영향**
만료·무효 token이 브라우저 session에 남고 사용자가 보호 화면에서 반복 실패할 수 있다.
**권장 조치**
응답 body parse 전에 HTTP status 기반 인증 만료를 공통 처리하되 동시 401 burst 1회 규칙을 유지하고 malformed/empty 401 회귀 test를 추가한다.
**판정 기록**
- 2026-07-29 — 코드의 모든 401 경로를 대조해 확정.
- 2026-07-30 — `P1-R6`에서 HTTP status 기반 인증 만료 처리를 body parse 전에 수행하도록 수정하고 malformed JSON, empty body, schema mismatch 401 회귀를 추가해 수정 완료로 판정.
### REV-P1-012 — zoom crop 결과가 선택 원본 영역을 확대한다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-012`
- **관련 계약:** resource별 `noUpscale=true`
- **소유 Task:** 신규 `P1-R7`
**관찰 내용**
출력 크기는 항상 `zoom=1`인 crop 영역으로 계산하지만 실제 source 영역은 현재 zoom으로 축소된다. 600×600 원본을 1.5배 확대하면 source는 400×400인데 output은 600×600이다.
**재현 또는 검증 절차**
1. 600×600 이미지와 1:1, maxWidth 800, noUpscale 정책을 연다.
2. zoom을 1.5로 바꾸고 적용한다.
3. `calculateCropSourceRect`는 400×400, Dialog는 output 600×600을 전달한다.
4. 요구 결과는 output이 선택 source crop의 pixel 크기를 넘지 않는 것이다.
**영향**
Character·Audio·Series·Community JPEG/PNG crop이 확대되어 품질이 저하되고 파일 계약을 위반한다.
**권장 조치**
output size 계산에 현재 zoom의 source rect를 사용하고 zoom 경계별 실제 canvas draw test를 추가한다.
**판정 기록**
- 2026-07-29 — 계산 결과 `scale=1.5`로 확정.
- 2026-07-30 — `P1-R7`에서 `CropOutputSizeRequest.zoom`을 반영해 zoom 1.5의 600×600 source crop output을 400×400으로 제한했고, focused/broad Vitest와 typecheck로 수정 완료 판정.
### REV-P1-013 — crop Dialog가 pinch/zoom을 지원하지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD `10.4`
- **관련 계약:** 없음
- **소유 Task:** 신규 `P1-R7`
**관찰 내용**
range, 버튼/keyboard zoom과 단일 pointer drag는 있으나 두 pointer 거리 또는 gesture를 처리하는 pinch 입력 경로가 없다.
**영향**
tablet/mobile touch 사용자가 명시된 pinch/zoom 방식으로 crop을 조작할 수 없다.
**권장 조치**
기존 range/keyboard 대안을 유지한 채 두 pointer pinch를 최소 구현하고 pointer 회귀 test와 실제 touch QA 항목을 추가한다.
**판정 기록**
- 2026-07-29 — pointer handler와 테스트 전체 검색에서 pinch 경로 0건으로 확정.
- 2026-07-30 — `P1-R7`에서 native pointer map/ref 기반 two-pointer pinch zoom을 추가하고 pointerup/pointercancel/pointerleave cleanup을 연결했다. Dialog 회귀 test로 pinch zoom 1.5, 기존 drag/range/keyboard 경로를 확인해 수정 완료 판정.
## 7. 확정 항목의 plan·goal 전환
- `REV-P1-011``P1-R6`
- `REV-P1-012~013``P1-R7`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | API 401·crop code/test 대조 |
| 후보 항목 판정 완료 | 충족 | 3건 모두 확정 |
| 확정 항목 plan 반영 | 충족 | `P1-R6`, `P1-R7` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 항목 수정 완료.
**남은 항목:** 없음.
## 9. 수정 후 검증 기록
- 2026-07-30 `P1-R6` — RED `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts`에서 malformed JSON, empty body, schema mismatch 401이 `clearSession` 0회로 3건 실패했다. GREEN 같은 command는 2 files / 21 tests passed. LSP diagnostics는 `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts` 모두 오류 0건이었다. Auth/session focused regression `npm run test:run -- src/features/auth/tests/auth-session.test.tsx src/features/auth/tests/login-page.test.tsx src/app/App.test.tsx src/app/App.protected-auth.test.tsx`는 3 files / 24 tests passed. Broad `npm run test:run -- src/features/auth src/app``REV-P10-001`/`P10-R1`의 stale fallback assertion 6건으로 실패해 기존 Phase 10 후속 범위로 유지한다.
- 2026-07-30 `P1-R7` — RED `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`에서 zoom 1.5 output 600×600과 pinch 미지원으로 2 failed / 9 passed를 확인했다. 1차 review blocker 보강 RED는 210:297 zoom 1.8 output 236×334의 source height 1px 초과와 `touch-action: none` 누락으로 2 failed / 11 passed였고, tiny source 0×0 붕괴 RED는 `npm run test:run -- src/shared/lib/crop-image.test.ts` 1 failed / 6 passed였다. 2차 review blocker 보강 RED는 aspect 2 tiny source 1×0 붕괴로 `npm run test:run -- src/shared/lib/crop-image.test.ts` 1 failed / 6 passed였다. 3차 review blocker 보강 RED는 free aspect tiny source 1×2 output이 source height를 초과해 `npm run test:run -- src/shared/lib/crop-image.test.ts` 2 failed / 5 passed였다. GREEN focused는 2 files / 14 tests passed로 zoom 1.5 output 400×400, 210:297 zoom 1.8 output 236×333, tiny source 1×1, aspect 2 tiny source 1×1, free aspect tiny source 1×1, two-pointer pinch zoom 1.5와 preview `touch-action: none`을 확인했다. Broad crop regression은 23 files / 119 tests passed였다. Surface E2E `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`은 7 tests passed로 실제 브라우저의 Series 생성 crop Dialog open/apply 경로를 확인했다. `npm run typecheck`, `npm run lint`는 exit 0이고, `src/shared/ui`, `tests/e2e` LSP diagnostics는 오류 0건이었다. 실제 tablet touch 장비 QA는 수행하지 못해 릴리스 QA 항목으로 유지한다.

View File

@@ -0,0 +1,167 @@
# Phase 10 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 10 / OpenAPI 2.3.0 후속 vertical slices와 최종 Gate |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `P10-T1~T7` 완료 체크와 실제 OpenAPI 2.3.0 구현·test·문서 상태가 일치하는지 확인한다.
- 기존 문서에 명시된 server fixture 차단은 새 결함과 구분한다.
### 포함 범위
- 코드·테스트: P10 변경 도메인, 공통 오류 fallback, 가격/file 회귀
- 문서: PRD 현재 계약 설명, Phase 10 Task/Gate/추적표
- 계약: OpenAPI version/path/operation/status와 주요 schema
- 수동 검증: 문서 metadata와 JSON 실제 집계 대조
### 제외 범위
- 개발 API 계정/fixture 자체 수정과 애플리케이션 결함 수정
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `EXT-009~011`, `COMMUNITY-012`, PRD `11`, `13`, `14`
- 계약: OpenAPI `3.1.0`, document version `2.3.0`, 25 paths, 37 operations, status `implemented`
- 코드: `upload-audio-content.ts:66`, `upload-audio-content.ts:72`, `upload-audio-content.ts:82`
- 실패 테스트: `App.protected-errors.test.tsx`, `auth-api.test.ts`, `audio-upload.test.ts`
- 문서: `prd.md:9`, `prd.md:597`, `prd.md:598`, `prd.md:801`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
Browser: Playwright Chromium/WebKit/mobile viewport projects
API mode: unit stub/MSW, VITE_API_MODE=mock
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| OpenAPI `jq` 집계 | 성공 | version 2.3.0, 25 paths, 37 HTTP operations, status unique=`implemented` |
| `npm run test:run` | 실패 | 66 files / 290 tests, 6 failed·284 passed |
| focused domain unit | 성공 | shared 27, Character 23, Audio+Series 46, Community+FanTalk+Comments 52 tests passed |
| 7개 spec mock E2E | 실패 | 147 passed, 17 skipped, WebKit timeout 4건 |
| WebKit 실패 4건 단일 worker 재실행 | 성공 | 4 passed / 28.4초; 동일 시나리오 단독 재현 실패 |
| `npm run typecheck`, `npm run lint`, `git diff --check` | 성공 | 모두 exit 0 |
| `npm run build` | 성공 | 249 modules transformed, production build exit 0 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P10-001` | Medium | 수정 완료 | 공통 오류 문구 migration이 불완전해 전체 unit Gate가 실패한다 | `P10-T7` | `P10-R1` |
| `REV-P10-002` | Low | 수정 완료 | PRD의 OpenAPI 집계와 Community metadata 설명이 2.0.0 상태다 | `P10-T1~T7` | `P10-R2` |
## 6. 발견 사항 상세
### REV-P10-001 — `EXT-011` migration과 전체 unit 회귀가 끝나지 않았다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `EXT-011`
- **관련 계약:** 미정의 오류 기본 문구 `알 수 없는 오류가 발생했습니다.`
- **소유 Task:** 신규 `P10-R1`
**관찰 내용**
전체 unit에서 보호 route 2건과 login response 4건이 과거 `API 응답 형식이 올바르지 않습니다.`/feature fallback을 기대해 실패한다. 반대로 XHR upload의 2xx malformed JSON/envelope 구현과 test는 여전히 과거 문구를 사용한다.
**재현 또는 검증 절차**
1. `npm run test:run`을 실행한다.
2. `App.protected-errors.test.tsx` 2건, `auth-api.test.ts` 4건이 실제 공통 문구와 기대 문구 불일치로 실패한다.
3. `audio-upload.test.ts`의 malformed 2xx test는 과거 문구를 기대하며 통과한다.
4. 요구 결과는 fetch/XHR의 network·JSON·envelope·빈 message가 모두 공통 문구이고 전체 unit이 통과하는 것이다.
**영향**
`P10-T7`과 P10 Gate의 “미정의 오류 문구 불일치 0건” 완료 증거가 성립하지 않는다.
**권장 조치**
XHR success parse fallback을 `UNKNOWN_API_ERROR_MESSAGE`로 통일하고 보호 route/login/audio upload의 오래된 assertion을 요구사항 기준으로 갱신한다. focused 뒤 전체 unit과 error-mapping E2E를 실행한다.
**판정 기록**
- 2026-07-29 — 전체 unit 실패와 XHR 구현/test를 함께 대조해 확정.
- 2026-07-30 — XHR success parse fallback과 보호 route/login stale assertion을 `UNKNOWN_API_ERROR_MESSAGE` 기준으로 정렬했다. 전체 unit의 잔여 4건은 개별 통과·병렬 실패로 MSW handler 경합임을 확인해 Vitest file parallelism을 끄고 기본 `npm run test:run`으로 71 files / 335 tests 통과를 확인했다.
### REV-P10-002 — PRD의 현재 API 계약 설명이 실제 JSON과 다르다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** 문서 단일 진실 원천, `COMMUNITY-012`
- **관련 계약:** OpenAPI metadata
- **소유 Task:** 신규 `P10-R2`
**관찰 내용**
PRD는 현재 계약을 version 2.0.0, 15 paths, 23 operations로 쓰고 최종 수정일도 2026-07-28이다. 실제 JSON은 2.3.0, 25 paths, 37 operations다. 성능 절에는 Community 종료 metadata가 아직 없다고 쓰지만 현재 계약·구현은 `totalCount/page/size/hasNext/items`를 사용한다.
**영향**
후속 작업자가 오래된 계약 범위로 구현·리뷰하거나 Community pagination을 미제공으로 오판할 수 있다.
**권장 조치**
현재 집계와 metadata 제공 상태, 최종 수정일만 정정하고 2.0.0 과거 결정·검증 기록은 당시 이력으로 보존한다.
**판정 기록**
- 2026-07-29 — `jq` 실제 집계와 PRD 현재형 문장을 대조해 확정.
- 2026-07-30 — PRD 현재 계약 문장을 OpenAPI 2.3.0, 25 paths, 37 operations와 Community pagination metadata 제공 상태로 정정했다. 2026-07-28 Decision Log의 과거 2.0.0 이력은 보존했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P10-001``P10-R1`
- `REV-P10-002``P10-R2`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | OpenAPI·P10 code/test/docs 대조 |
| 후보 항목 판정 완료 | 충족 | 2건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P10-R1`, `P10-R2` |
| 보류 항목 담당·재개 조건 | 충족 | server fixture는 기존 P10 Gate 상태 유지 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 검증 완료. 자동 Gate 정리는 완료됐고 실제 개발 API 수동 QA는 별도 대기한다.
**남은 항목:** 실제 개발 API credential·고정 fixture 확보 후 Series/FanTalk/파일 정책 수동 QA.
## 9. 수정 후 검증 기록
애플리케이션 수정은 아직 하지 않았다. 병렬 통합 실행에서 timeout 난
WebKit 4건을 `--project=webkit --workers=1 --last-failed`와 단일 worker로
재실행해 4 passed / 28.4초를 확인했다. 따라서 timeout 4건은 별도 기능
결함으로 확정하지 않았지만, 전체 mock E2E Gate의 최초 실행은 exit 0이
아니므로 `P10-GATE` 완료 근거로 사용하지 않는다. `npm run build`는 249
modules transformed와 exit 0으로 통과했다.
### P10-R1~P10-R3 및 Gate 정책 정정 — 2026-07-30
- `P10-R1`: fetch/XHR 미정의 오류 fallback을 `UNKNOWN_API_ERROR_MESSAGE`로 정렬하고 stale unit assertion을 갱신했다. 전체 unit은 이후 72 files / 354 tests passed까지 통과했다.
- `P10-R2`: PRD 현재 계약 설명을 OpenAPI 2.3.0, 25 paths, 37 operations와 Community pagination metadata 제공 상태로 정정했다.
- `P10-R3`: plan 상단, 구현 완료 정의, 현재 Phase 10 리뷰를 자동 Gate 완료와 실제 개발 API 수동 QA 대기 기준으로 정렬했다.
- Gate 정책: `P10-GATE` 자동 범위는 focused unit/mock E2E/server allowlist/typecheck/lint/build/diff 기준으로 정리하고, Series/FanTalk 실제 개발 API 검증은 credential·fixture 준비 후 수동 QA로 분리한다.

View File

@@ -0,0 +1,90 @@
# Phase 2 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 2 / explicit mock·server 경계와 browser fixture 기반 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 기존 `review-phase-2.md`의 수정 완료 판정을 보존하면서 최신 도메인 fixture가 explicit mock 경계를 깨지 않는지 확인한다.
### 포함 범위
- 코드·설정: `src/shared/mocks`, runtime API mode, Playwright mode 분리
- 테스트: mock domain E2E와 server no-fallback 관련 구성
- 문서: `MOCK-001~009`, Phase 2 Gate·회귀 기록
- 수동 검증: 없음
### 제외 범위
- fixture 내용의 각 도메인 요구사항은 해당 Phase report에서 판정
- 실제 개발 API의 데이터 정합성
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `MOCK-001~009`
- 계획: `P2-T1~P2-GATE`, `P2-R1~P2-R16`
- 코드·테스트: `src/shared/mocks`, `playwright.config.ts`, `tests/e2e/error-mapping.spec.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: VITE_API_MODE=mock
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| Phase 10 대상 7개 spec의 `npm run e2e:mock -- ...` | 실행 | sandbox EPERM 뒤 승인된 local webServer로 재실행; 최종 결과는 Phase 10 report와 공유 |
| `npm run typecheck` | 성공 | exit 0 |
| `npm run lint` | 성공 | exit 0 |
| `git diff --check` | 성공 | exit 0 |
## 5. 발견 사항 요약
**확정 발견 사항 없음.** 도메인 mock E2E가 놓친 요구사항은 해당 구현 Phase와 Phase 9 회귀 항목으로 귀속했다.
## 6. 발견 사항 상세
전환할 Phase 2 발견 사항이 없다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | mode·fixture·E2E 구성 대조 |
| 후보 항목 판정 완료 | 충족 | 도메인 소유 문제와 분리 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 없음 |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 없음.
**남은 항목:** 실제 server integration은 `P10-GATE`의 기존 진행 상태를 유지한다.
## 9. 수정 후 검증 기록
수정 goal이 없어 기록 없음.

View File

@@ -0,0 +1,184 @@
# Phase 3 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 3 / Character workspace |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `CHAR-001~018`, Character form/file 요구와 `P3-T1~P3-GATE`, `P10-T1` 결과를 대조한다.
### 포함 범위
- 코드: `src/features/characters`, Character route와 workspace 조합
- 테스트: Character unit/integration와 mock E2E
- 문서: Character 요구사항, OpenAPI Character request/schema, Phase 3 체크
- 수동 검증: form control inventory와 payload 정적 대조
### 제외 범위
- 공통 crop 계산은 Phase 1, cross-domain inactive policy와 field-error 접근성은 Phase 9에서 판정
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `CHAR-003`, `CHAR-013`, `CHAR-018`, Character 생성·수정 폼, `FILE-002`, PRD `10.5`
- 계약: `CharacterCreateRequest`, `CharacterUpdateRequest`
- 코드: `CharacterCreatePage.tsx:37`, `CharacterCreatePage.tsx:52`, `CharacterCreatePage.tsx:75`, `CharacterCreatePage.tsx:88`, `CharacterEditPage.tsx:53`, `CharacterEditPage.tsx:68`, `CharacterEditPage.tsx:93`, `CharacterEditPage.tsx:106`
- 테스트: `CharacterCreatePage.test.tsx`, `CharacterEditPage.test.tsx`, `character-api.test.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: unit injected client/MSW
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/characters` | 성공 | 6 files / 23 tests passed |
| OpenAPI property 출력 | 성공 | optional scalar 10개와 반복 배열 8개 확인 |
| form control/payload 정적 대조 | 실패 재현 | 생성·수정 UI는 기본 4개 text/file + 원작 선택만 제공 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P3-001` | High | 수정 완료 | Character form이 OpenAPI optional scalar·배열을 편집하지 못한다 | `P3-T3` | `P3-R1` |
| `REV-P3-002` | Medium | 수정 완료 | create/update 실패 시 form이 영구 제출 중 상태가 된다 | `P3-T3` | `P3-R2` |
| `REV-P3-003` | Medium | 수정 완료 | Character image가 확장자와 MIME 조합을 검증하지 않는다 | `P3-T3` | `P3-R2` |
## 6. 발견 사항 상세
### REV-P3-001 — Character form의 확정 입력 범위가 누락됐다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** Character 생성·수정 폼, `CHAR-003`, `CHAR-018`
- **관련 계약:** `CharacterCreateRequest`, `CharacterUpdateRequest`
- **소유 Task:** 신규 `P3-R1`
**관찰 내용**
생성·수정 화면은 `name`, `systemPrompt`, `description`, `originalWorkId`, image와 수정의 읽기 전용 region만 제공한다. PRD가 명시한 `age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType` 및 8개 반복 배열 입력이 없다.
**재현 또는 검증 절차**
1. Character 생성 또는 수정 route를 연다.
2. OpenAPI request property와 visible label을 대조한다.
3. 해당 18개 optional 입력을 찾거나 수정할 수 없다.
4. API helper의 type/test는 이 필드를 수용하지만 UI test는 입력 경로를 검증하지 않는다.
**영향**
운영자가 계약에 포함된 캐릭터 프로필·관계·기억 데이터를 생성·수정할 수 없고 `P3-T3` 완료 판정과 불일치한다.
**권장 조치**
현재 request type을 재사용해 scalar와 반복 section을 최소 form component로 추가하고 create/update serializer·dirty state·validation E2E를 보강한다.
**판정 기록**
- 2026-07-29 — PRD·OpenAPI property와 실제 form control을 대조해 확정.
- 2026-07-30 — `P3-R1`에서 create/edit optional scalar·반복 배열 editor와 canonical payload test를 추가해 수정 완료로 판정.
### REV-P3-002 — Character mutation 실패 후 재시도할 수 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD `10.5` 오류·저장 상태
- **관련 계약:** Character mutation 공통 오류 envelope
- **소유 Task:** 신규 `P3-R2`
**관찰 내용**
`setIsSubmitting(true)``await createCharacter/updateCharacter`를 catch/finally 없이 실행한다. 요청이 reject되면 서버 오류가 표시되지 않고 제출 버튼이 계속 비활성화된다.
**영향**
일시적 네트워크·server 오류 뒤 입력을 유지한 재시도가 불가능하다.
**권장 조치**
server/form 오류 상태와 `finally` 복구를 추가하고 create/update rejection focused test를 먼저 작성한다.
**판정 기록**
- 2026-07-29 — 두 submit 경로의 예외 처리를 대조해 확정.
- 2026-07-30 — `P3-R2`에서 create/update rejection retry test를 RED로 추가한 뒤 `try/catch/finally`와 form-level 오류를 적용해 수정 완료로 판정. 검증: `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 3 files / 24 tests passed, `npm run test:run -- src/features/characters` 6 files / 32 tests passed, `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 병렬 실행은 axe timeout 4건으로 실패, focused Chromium 1 passed, timeout 보정 후 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --workers=1` 41 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build` exit 0.
### REV-P3-003 — Character image의 format pair 검증이 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-002`
- **관련 계약:** JPEG/PNG image
- **소유 Task:** 신규 `P3-R2`
**관찰 내용**
`validateImage`는 MIME과 크기만 검사한다. 예를 들어 `profile.gif` 또는 `profile.txt`라는 이름에 `image/png` MIME을 부여하면 통과한다.
**영향**
허용 format과 다른 multipart가 client에서 전송되고 backend 거부가 불필요하게 늦게 발생한다.
**권장 조치**
Community에서 사용하는 extension↔MIME pair 방식의 Character 전용 policy를 추가하고 mismatch test를 작성한다.
**판정 기록**
- 2026-07-29 — create/edit validator의 filename 사용 0건으로 확정.
- 2026-07-30 — Character image policy가 `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png` 조합을 검증하고, create/edit mismatch test가 crop source 0회와 mutation request 0회를 확인해 수정 완료로 판정. 검증 명령과 결과는 `REV-P3-002`의 2026-07-30 기록과 동일하다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P3-001``P3-R1`
- `REV-P3-002~003``P3-R2`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Character code/test/contract 대조 |
| 후보 항목 판정 완료 | 충족 | 3건 확정 |
| 확정 항목 plan 반영 | 충족 | `P3-R1`, `P3-R2` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** `P3-R2`, Phase 9의 inactive·접근성 회귀.
## 9. 수정 후 검증 기록
- 2026-07-30 `REV-P3-001 / P3-R1` 수정 완료 확인
- RED: `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx`에서 신규 3개 test가 optional field label 누락으로 실패했다.
- GREEN: `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 18 tests passed.
- 회귀: `npm run test:run -- src/features/characters` 결과 6 files / 26 tests passed.
- E2E: `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 결과 41 passed / 3 skipped. optional 입력 추가로 keyboard-only helper의 tab 한도만 80회로 조정했다.
- 품질 게이트: `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0.
- 2026-07-30 `REV-P3-001 / P3-R1` 독립 리뷰 보완
- 발견: 수정 상세 응답에 없는 `originalTitle`/`originalLink`가 일반 저장 시 `null`로 전송되어 기존 서버 값을 지울 수 있는 blocker를 확인했다.
- RED: `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx` 결과 2 failed로 이름 수정과 원작 선택 해제 저장에서 `originalTitle: null`, `originalLink: null` 전송을 재현했다.
- 수정: 두 필드에 touched flag를 추가해 edit 기본 저장은 omit하고, 사용자가 입력 후 삭제한 경우에만 `null`을 전송하게 했다.
- 재검증: `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx` 결과 1 file / 7 tests passed. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 18 tests passed. `npm run test:run -- src/features/characters` 결과 6 files / 26 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 결과 41 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0.

View File

@@ -0,0 +1,247 @@
# Phase 4 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 4 / Audio vertical slice |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `AUDIO-001~033`, upload·price·media/file 기준과 Phase 4/P10 후속 결과를 대조한다.
### 포함 범위
- 코드: `src/features/audio-contents`
- 테스트: Audio contract/form/upload/player
- 문서: Audio 요구사항·OpenAPI request·Phase 4 Task/Gate
- 수동 검증: form control과 upload lifecycle 정적 대조
### 제외 범위
- 공통 crop 계산은 Phase 1, inactive workspace와 공통 field 접근성은 Phase 9, 공통 오류 문구 최종 회귀는 Phase 10
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUDIO-014`, `AUDIO-017~019`, `AUDIO-024`, `AUDIO-033`, `AUTH-005`, `FILE-002`
- 코드: `AudioContentForm.tsx:60`, `AudioContentForm.tsx:103`, `AudioContentForm.tsx:171`, `AudioContentForm.tsx:179`, `audio-content-form-helpers.ts:11`, `audio-content-form-helpers.ts:12`, `audio-content-form-helpers.ts:87`, `upload-audio-content.ts:51`, `upload-audio-content.ts:61`, `upload-audio-content.ts:85`
- 테스트: `audio-form.test.tsx`, `audio-form-upload.test.tsx`, `audio-upload.test.ts`, `audio-contract.test.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected upload client/XHR fake/MSW
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/audio-contents src/features/series` | 성공 | 합계 11 files / 46 tests passed |
| 가격 parser `node` 계산 | 실패 재현 | `-1 → 1`, `1.5 → 15` |
| form/upload code 대조 | 실패 재현 | optional control 누락, upload 중 submit 활성, XHR 401 session 처리 없음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-001` | High | 수정 완료 | Audio create form이 OpenAPI optional 설정을 고정값으로만 보낸다 | `P4-T3` | `P4-R1` |
| `REV-P4-002` | High | 수정 완료 | 음수·소수 가격을 거부하지 않고 다른 유효값으로 바꾼다 | `P10-T7` | `P4-R1` |
| `REV-P4-003` | High | 수정 완료 | upload 중 중복 submit이 가능하다 | `P4-T3` | `P4-R2` |
| `REV-P4-004` | High | 수정 완료 | XHR upload 401이 session clear/login 이동을 실행하지 않는다 | `P4-T3` | `P4-R2` |
| `REV-P4-005` | Medium | 수정 완료 | `.m4a + audio/mp4`가 file picker accept에서 누락됐다 | `P4-T3` | `P4-R3` |
| `REV-P4-006` | Medium | 수정 완료 | cover image의 확장자와 MIME 불일치가 통과한다 | `P4-T3` | `P4-R3` |
## 6. 발견 사항 상세
### REV-P4-001 — Audio create form이 확정 optional 설정을 제공하지 않는다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-033`
- **관련 계약:** `AudioContentCreateRequest`
- **소유 Task:** 신규 `P4-R1`
**관찰 내용**
`purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 편집하는 control이 없다. serializer는 전부 고정 default로 보낸다.
**영향**
운영자가 OpenAPI와 PRD에 확정된 발행·구매·댓글·미리보기 설정을 선택할 수 없다.
**권장 조치**
계약 enum/type만 사용한 최소 control을 추가하고 create payload·dirty state·접근성 test를 보강한다.
**판정 기록**
- 2026-07-29 — visible control과 serializer 고정값을 대조해 확정.
- 2026-07-30 — create 전용 optional 설정 control과 serializer를 추가하고 Audio unit/mock E2E/static/build 검증 통과.
### REV-P4-002 — Audio 가격의 금지 입력이 다른 값으로 변환된다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-019`, PRD `14.1` 가격 경계
- **관련 계약:** `price` integer `0..99999`
- **소유 Task:** 신규 `P4-R1`
**관찰 내용**
parser가 숫자가 아닌 모든 문자를 제거한다. `-1``1캔`, `1.5``15캔`이 되어 validation을 통과한다.
**영향**
사용자가 거부돼야 할 값을 입력했는데 의도와 다른 가격이 저장될 수 있다.
**권장 조치**
허용 display suffix/group separator만 정규화하고 부호·소수점은 invalid raw state로 유지해 제출을 차단한다. `-1`, `1.5` form test를 추가한다.
**판정 기록**
- 2026-07-29 — parser 계산으로 확정.
- 2026-07-30 — `-1`, `1.5`, `100000` raw 입력을 변형하지 않고 request 0건으로 차단하도록 수정하고 Audio unit/mock E2E/static/build 검증 통과.
### REV-P4-003 — Audio upload 중 중복 제출이 차단되지 않는다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** Character/Audio form 저장 중 중복 제출, `AUDIO-017`
- **관련 계약:** Audio create POST
- **소유 Task:** 신규 `P4-R2`
**관찰 내용**
submit handler에 uploading guard가 없고 생성 버튼도 `uploadState.status`로 비활성화되지 않는다. 첫 요청 pending 중 다시 submit하면 새 AbortController와 POST를 시작한다.
**영향**
중복 콘텐츠 생성·대용량 중복 전송이 발생할 수 있다.
**권장 조치**
synchronous ref guard와 disabled 상태를 함께 적용하고 pending upload double-submit test를 추가한다.
**판정 기록**
- 2026-07-29 — submit/button 경로에 guard 0건으로 확정.
- 2026-07-30 — synchronous pending ref와 uploading disabled button을 추가해 pending 중 submit 2회를 request 1건으로 차단하고 cancel 후 재제출 가능함을 focused test로 확인.
### REV-P4-004 — XHR upload 401에서 인증 상태가 남는다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-005`
- **관련 계약:** upload 보호 요청 401
- **소유 Task:** 신규 `P4-R2`
**관찰 내용**
XHR adapter는 storage에서 token만 읽고 401을 일반 `ApiError`로 reject한다. 공통 client의 clearSession/onAuthExpired 경로를 사용하지 않는다.
**영향**
대용량 upload 도중 session이 만료되면 login으로 복구되지 않는다.
**권장 조치**
공통 인증 만료 callback을 upload adapter에 주입하거나 동일한 단일 session expiry controller를 사용하고 XHR 401 test를 추가한다.
**판정 기록**
- 2026-07-29 — XHR adapter의 session remove·redirect 호출 0건으로 확정.
- 2026-07-30 — upload adapter에 token reader/session clear/login callback을 주입하고 protected 401 burst에서 clear/login 1회만 실행하도록 수정. malformed 401과 normal 401 회귀 test 및 공통 client-auth 회귀 통과.
### REV-P4-005 — canonical M4A MIME이 file picker에서 빠졌다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-014`, `AUDIO-024`
- **관련 계약:** `.m4a` + `audio/mp4`
- **소유 Task:** 신규 `P4-R3`
**관찰 내용**
validator는 `audio/mp4`를 허용하지만 input `accept``audio/mpeg,audio/aac,audio/x-m4a`만 제공한다.
**영향**
브라우저 file chooser가 정상 M4A 파일을 숨기거나 선택을 방해할 수 있다.
**권장 조치**
`AUDIO_FILE_POLICY.allowedMimeTypes`를 accept source로 재사용해 중복을 제거한다.
**판정 기록**
- 2026-07-29 — validator와 input accept 대조로 확정.
- 2026-07-30 — Audio file picker accept를 `AUDIO_FILE_POLICY.allowedMimeTypes.join(",")`로 변경해 `audio/mp4` 포함을 focused test로 확인.
### REV-P4-006 — Audio cover format pair가 검증되지 않는다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-002`
- **관련 계약:** JPEG/PNG cover
- **소유 Task:** 신규 `P4-R3`
**관찰 내용**
`.png + image/jpeg`, `.jpg + image/png`가 extension allowlist와 MIME allowlist를 각각 통과한다.
**영향**
잘못된 multipart가 backend까지 전송된다.
**권장 조치**
extension별 MIME map과 mismatch test를 추가한다.
**판정 기록**
- 2026-07-29 — `validateAudioCoverFile`이 공통 독립 allowlist만 호출함을 확인해 확정.
- 2026-07-30 — cover extension별 MIME pair 검증을 추가해 `.jpg/.jpeg + image/png`, `.png + image/jpeg``mime` 오류로 차단하고 contract 회귀 test로 확인.
## 7. 확정 항목의 plan·goal 전환
- `REV-P4-001~002``P4-R1`
- `REV-P4-003~004``P4-R2`
- `REV-P4-005~006``P4-R3`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Audio contract/form/upload/file 대조 |
| 후보 항목 판정 완료 | 충족 | 6건 확정 |
| 확정 항목 plan 반영 | 충족 | `P4-R1~R3` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** `P4-R1~R3`, Phase 9 inactive/접근성, Phase 10 오류 회귀.
## 9. 수정 후 검증 기록
- 2026-07-30 — `P4-R1` 완료. 검증: `npm run test:run -- src/features/audio-contents` 8 files / 35 tests passed, `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 21 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.
- 2026-07-30 — `P4-R2` 완료. RED: `audio-form-upload.test.tsx`는 pending upload 중 2회 호출로 실패했고, `audio-upload.test.ts`는 protected 401에서 `clearSession` 0회로 실패했다. GREEN/검증: `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx` 1 file / 6 tests passed, `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts src/shared/api/__tests__/client-auth.test.ts` 2 files / 19 tests passed, `npm run test:run -- src/features/audio-contents` 8 files / 38 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.
- 2026-07-30 — `P4-R3` 완료. RED: `audio-form-upload.test.tsx`는 accept `audio/mp4` 누락으로 실패했고, `audio-contract.test.ts`는 cover mismatch가 `{ ok: true }`로 실패했다. GREEN/검증: `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx` 1 file / 7 tests passed, `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts src/shared/validation/file-media-policy.test.ts` 2 files / 17 tests passed, `npm run test:run -- src/features/audio-contents src/features/community-posts` 12 files / 76 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.

View File

@@ -0,0 +1,184 @@
# Phase 5 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 5 / Series vertical slice와 `P10-T3` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `SERIES-001~018`, Series image/file 계약과 Phase 5/P10 후속 구현을 대조한다.
### 포함 범위
- 코드: `src/features/series`
- 테스트: Series contract/form/route/contents/order
- 문서: Phase 5, `P10-T3`, OpenAPI Series request
- 수동 검증: update payload와 initial state 비교
### 제외 범위
- inactive workspace 교차 정책은 Phase 9, 공통 crop은 Phase 1
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `SERIES-004`, `SERIES-007`, `SERIES-010`, `FILE-002`
- 코드: `SeriesForm.tsx:77`, `SeriesForm.tsx:127`, `series-schema.ts:28`, `series-schema.ts:35`, `series-image-policy.ts:14`
- 테스트: `series-form.test.tsx`, `series-contract.test.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected client/MSW
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/audio-contents src/features/series` | 성공 | 합계 11 files / 46 tests passed |
| edit payload 정적 대조 | 실패 재현 | 초기 state와 같아도 request에 `state` 포함 |
| image policy 대조 | 실패 재현 | extension/MIME 독립 allowlist |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P5-001` | High | 수정 완료 | 변경하지 않은 Series `state`를 항상 전송한다 | `P10-T3` | `P5-R1` |
| `REV-P5-002` | Medium | 수정 완료 | Series mutation schema가 `isActive=true/null`, `state=null`을 허용한다 | `P10-T3` | `P5-R1` |
| `REV-P5-003` | Medium | 수정 완료 | Series image 확장자와 MIME 불일치가 통과한다 | `P10-T3` | `P5-R2` |
## 6. 발견 사항 상세
### REV-P5-001 — 변경 없는 Series state가 update payload에 포함된다
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** `SERIES-004`, PRD `14.1`
- **관련 계약:** `SeriesUpdateRequest.state`
- **소유 Task:** 신규 `P5-R1`
**관찰 내용**
edit form은 초기 state를 local state에 넣고 모든 저장 request에 `state`를 포함한다. 현재 값과 원본을 비교해 생략하는 분기가 없다.
**영향**
요구된 partial update 의미를 위반하고 다른 동시 변경을 불필요하게 덮어쓸 수 있다.
**권장 조치**
원본과 달라진 경우에만 `state` key를 추가하고 unchanged/changed payload test를 분리한다.
**판정 기록**
- 2026-07-29 — `SeriesForm.tsx:127`과 기존 test의 항상 포함 assertion으로 확정.
- 2026-07-30 — `P5-R1`에서 unchanged state 생략 회귀 test와 원본 비교 serializer를 추가해 수정 완료.
### REV-P5-002 — Series schema가 제품 mutation 불변식을 강제하지 않는다
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** `SERIES-002`, `SERIES-010`
- **관련 계약:** `SeriesUpdateRequest`
- **소유 Task:** 신규 `P5-R1`
**관찰 내용**
export된 update schema가 nullable `state`와 boolean/nullable `isActive`를 허용한다. 따라서 API helper 호출자는 `state:null`, `isActive:true/null`을 전송할 수 있다.
**영향**
현재 UI 밖의 후속 호출자가 복원 금지·soft-delete-only 정책을 우회할 수 있다.
**권장 조치**
일반 update와 deactivate request schema를 분리해 `isActive=false`만 별도 adapter에서 허용하고 state는 enum만 허용한다.
**판정 기록**
- 2026-07-29 — schema safe-parse 가능 범위를 PRD 정책과 대조해 확정.
- 2026-07-30 — `P5-R1`에서 일반 update schema와 deactivate `{ isActive:false }` schema를 분리하고 `state:null`, `isActive:true/null` 거부 test로 수정 완료.
### REV-P5-003 — Series image format pair가 검증되지 않는다
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** `FILE-002`, `SERIES-014`
- **관련 계약:** JPEG/PNG image
- **소유 Task:** 신규 `P5-R2`
**관찰 내용**
`.png + image/jpeg` 또는 `.jpg + image/png`가 허용된다.
**영향**
Series 생성·이미지 교체가 잘못된 multipart를 전송할 수 있다.
**권장 조치**
extension별 MIME mapping과 양방향 mismatch test를 추가한다.
**판정 기록**
- 2026-07-29 — Series policy가 독립 allowlist validator만 호출함을 확인해 확정.
- 2026-07-30 — `P5-R2`에서 Series 전용 extension↔MIME pair test와 crop-before-validation test를 추가하고 policy mapping으로 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P5-001~002``P5-R1`
- `REV-P5-003``P5-R2`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Series code/test/contract 대조 |
| 후보 항목 판정 완료 | 충족 | 3건 확정 |
| 확정 항목 plan 반영 | 충족 | `P5-R1`, `P5-R2` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 기존 server fixture 차단은 계획에 이미 기록 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** 기존 P10 server integration, Phase 9 inactive 정책.
## 9. 수정 후 검증 기록
### P5-R1 Series partial update 불변식 복구, 2026-07-30
- 무엇을: Series edit payload에서 원본과 같은 `state`를 생략하고, 일반 update schema와 deactivate `{ isActive:false }` schema를 분리했다.
- 왜: partial update가 변경 없는 상태값을 덮어쓰지 않고, soft delete 이외의 `isActive` mutation과 `state:null`을 boundary에서 막기 위해서다.
- 어떻게:
- RED: `npm run test:run -- src/features/series/tests/series-form.test.tsx src/features/series/tests/series-update-invariants.test.ts`는 2 failed로, unchanged edit payload에 `state: "PROCEEDING"`이 포함되고 `state:null` parse가 성공함을 확인했다.
- GREEN: `SeriesForm`은 원본과 달라진 경우에만 `state`를 request에 넣고, `seriesUpdateRequestSchema`는 enum `state`만 허용한다. `deactivateSeries`와 mock store는 별도 `seriesDeactivateRequestSchema``{ isActive:false }`만 처리한다.
- 검증: focused 2 files / 5 tests 통과, `npm run test:run -- src/features/series`는 6 files / 20 tests 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`도 모두 exit 0이었다.
### P5-R2 Series image format pair 검증, 2026-07-30
- 무엇을: Series image validation에서 `.jpg/.jpeg``image/jpeg`, `.png``image/png`만 허용하도록 extension↔MIME pair를 고정했다.
- 왜: 독립 allowlist만으로는 `.png + image/jpeg`, `.jpg + image/png`가 crop과 multipart upload까지 진행될 수 있기 때문이다.
- 어떻게:
- RED: `npm run test:run -- src/features/series/tests/series-image-policy.test.ts src/features/series/tests/series-form.test.tsx`는 2 failed로, mismatch file이 `{ ok: true }`를 반환하고 crop dialog가 열림을 확인했다.
- GREEN: `series-image-policy.ts`에 Series 전용 extension→MIME mapping을 추가해 mismatch를 `{ ok:false, reason:"mime" }`로 거부하고, 기존 size/extension/MIME allowlist 검사는 유지했다.
- 검증: focused 2 files / 5 tests 통과, `npm run test:run -- src/features/series src/shared/validation/file-media-policy.test.ts`는 8 files / 29 tests 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`도 모두 exit 0이었다.
남은 항목: 기존 P10 server integration과 Phase 9 inactive 정책은 후속 범위로 유지한다.

View File

@@ -0,0 +1,129 @@
# Phase 6 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 6 / Community vertical slice와 `P10-T4` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `COMMUNITY-001~015`, Community pagination/media/price와 Sheet mutation을 대조한다.
### 포함 범위
- 코드: `src/features/community-posts`
- 테스트: Community contract/form/list/sheet
- 문서: Phase 6, `P10-T4`, OpenAPI Community request
- 수동 검증: price raw input과 payload 계산
### 제외 범위
- Comments 내부 pagination은 Phase 8, inactive comment mutation은 Phase 9
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `COMMUNITY-007`, `COMMUNITY-012`, PRD `14.1`
- 코드: `community-post-form-helpers.ts:11`, `community-post-form-helpers.ts:12`, `CommunityPostForm.tsx:99`, `CommunityPostForm.tsx:122`, `CommunityPostForm.tsx:142`
- 테스트: `community-contract.test.ts`, `community-form.test.tsx`, `community-list.test.tsx`, `community-sheet.test.tsx`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected client/MSW
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/community-posts src/features/fan-talks src/features/comments` | 성공 | 합계 9 files / 52 tests passed |
| 가격 parser `node` 계산 | 실패 재현 | `-1 → 1`, `1.5 → 15` |
| pagination contract 대조 | 성공 | server `totalCount/page/size/hasNext/items` 사용 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P6-001` | High | 수정 완료 | Community 가격의 음수·소수를 거부하지 않고 다른 값으로 저장한다 | `P10-T7` | `P6-R1` |
## 6. 발견 사항 상세
### REV-P6-001 — Community 금지 가격 입력이 유효한 다른 값으로 변환된다
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** `COMMUNITY-007`, PRD `14.1`
- **관련 계약:** create `price` integer `0..99999`
- **소유 Task:** 신규 `P6-R1`
**관찰 내용**
입력 parser가 숫자가 아닌 문자를 전부 제거한다. `-1``1캔`, `1.5``15캔`으로 바뀌고 form validation을 통과한다.
**재현 또는 검증 절차**
1. Community create form 가격에 `-1` 또는 `1.5`를 입력한다.
2. input은 각각 `1캔`, `15캔`으로 바뀐다.
3. 필수 필드를 채워 제출하면 거부 대신 해당 양수 정수가 전송된다.
4. 요구 결과는 음수·소수 제출 전 거부다.
**영향**
운영자 의도와 다른 유료 가격이 저장될 수 있다.
**권장 조치**
Audio와 동일한 raw-price validation primitive를 사용해 부호·소수점 입력을 invalid로 유지하고 form-level `-1`, `1.5` request 0건 test를 추가한다.
**판정 기록**
- 2026-07-29 — parser 계산과 submit serializer를 대조해 확정.
- 2026-07-30 — `P6-R1`에서 raw CAN price parser를 shared로 분리하고 Community form이 `-1`, `1.5`를 원문 유지 오류로 차단하도록 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P6-001``P6-R1`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Community code/test/contract 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P6-R1` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** Phase 8 reply pagination, Phase 9 inactive/accessibility.
## 9. 수정 후 검증 기록
### P6-R1 Community raw price validation 복구, 2026-07-30
- 무엇을: Community create 가격 입력에서 음수·소수 raw input을 다른 숫자로 변형하지 않고 제출 전에 차단했다.
- 왜: `-1 → 1캔`, `1.5 → 15캔`처럼 운영자 의도와 다른 유료 가격이 저장될 수 있었기 때문이다.
- 어떻게:
- RED: `npm run test:run -- src/features/community-posts/tests/community-price-validation.test.tsx`는 3 failed로, `-1`/`1.5`가 오류 없이 양수 캔 가격으로 변형됨을 확인했다.
- GREEN: `parseCanPriceInput`/`formatCanPriceInput``src/shared/validation/can-price.ts`에 추가하고 Audio/Community helper가 같은 raw price 규칙을 쓰게 했다. Community form은 빈 가격만 optional로 허용하고, 입력이 있는데 parse 실패하면 inline 오류와 POST 0건으로 막는다.
- 검증: focused `npm run test:run -- src/shared/validation/can-price.test.ts src/features/community-posts/tests/community-price-validation.test.tsx`는 2 files / 15 tests 통과했다. `npm run test:run -- src/shared/validation/can-price.test.ts src/features/community-posts`는 6 files / 51 tests 통과했고, `npm run typecheck`, `npm run lint`, `npm run build`도 모두 exit 0이었다.
남은 항목: Phase 8 reply pagination과 Phase 9 inactive/accessibility는 후속 범위로 유지한다.

View File

@@ -0,0 +1,89 @@
# Phase 7 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 7 / FanTalk vertical slice와 `P10-T5` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `FANTALK-001~012`, 목록·답변 생성/수정·팬 원글 삭제와 단일 답변 client 불변식을 대조한다.
### 포함 범위
- 코드: `src/features/fan-talks`
- 테스트: FanTalk contract/list/reply, mock E2E
- 문서: Phase 7, `P10-T5`, FanTalk OpenAPI operations
- 수동 검증: reply ID mapping과 pending ref guard 정적 대조
### 제외 범위
- 실제 server의 동시 POST 유일성은 기존 `P10-GATE` 진행 범위
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `FANTALK-001~012`
- 계약: FanTalk list GET, reply POST/PUT, root DELETE
- 코드: `fan-talk-api.ts`, `FanTalkListPage.tsx`, `FanTalkReplySheet.tsx`, `FanTalkReplyForm.tsx`
- 테스트: `fan-talk-contract.test.ts`, `fan-talk-list.test.tsx`, `fan-talk-reply.test.tsx`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected client/MSW/mock E2E
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/community-posts src/features/fan-talks src/features/comments` | 성공 | 합계 9 files / 52 tests passed |
| FanTalk code/contract 대조 | 성공 | first `creatorReplies[].fanTalkId`를 PUT replyId로 사용, ref로 중복 submit 차단 |
| mock E2E 통합 실행 | 부분 실패 | FanTalk 시나리오는 통과 또는 capability별 의도된 skip; 전체 WebKit timeout은 다른 Phase spec |
## 5. 발견 사항 요약
**확정 발견 사항 없음.**
## 6. 발견 사항 상세
전환할 Phase 7 발견 사항이 없다. 실제 server 동시 POST 후 활성 답변 1개 보장은 구현 결함 판정이 아니라 기존 `P10-GATE` 미완료 검증으로 유지한다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | list/create/update/delete code·test 대조 |
| 후보 항목 판정 완료 | 충족 | 신규 확정 없음 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 없음 |
| 보류 항목 담당·재개 조건 | 충족 | server 유일성은 `P10-GATE`, 개발 fixture 로그인 복구 후 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 없음.
**남은 항목:** 기존 `P10-GATE` server integration.
## 9. 수정 후 검증 기록
수정 goal이 없어 기록 없음.

View File

@@ -0,0 +1,163 @@
# Phase 8 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 8 / Comments vertical slice와 `P10-T6` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `COMMENT-001~008`, root/direct reply CRUD·권한·pagination과 screen state를 대조한다.
### 포함 범위
- 코드: `src/features/comments`, Audio detail/Community Sheet 소비 경로
- 테스트: comment contract/thread와 mock E2E
- 문서: Phase 8 과거 이력, `P10-T6`, comment OpenAPI operations
- 수동 검증: 20개 초과 reply와 empty state 정적 재현
### 제외 범위
- inactive workspace의 쓰기 차단은 Phase 9에서 교차 판정
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `COMMENT-001~008`, PRD `10.5`
- 계약: root/reply GET `page`, `size`, `totalCount`, `items`
- 코드: `CommentThread.tsx:43`, `CommentThread.tsx:46`, `CommentThread.tsx:127`, `CommentThread.tsx:144`, `CommentThread.tsx:155`
- 테스트: `comment-contract.test.ts`, `comment-thread.test.tsx`, `tests/e2e/comments.spec.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected client/MSW/mock E2E
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/community-posts src/features/fan-talks src/features/comments` | 성공 | 합계 9 files / 52 tests passed |
| reply pagination code 대조 | 실패 재현 | reply GET은 항상 `page=0,size=20`; reply pagination control 0건 |
| empty response code 대조 | 실패 재현 | total 0에서 empty `PageState` 없이 `총 0개`만 표시 |
| mock E2E 통합 실행 | 실패 | 7개 spec 병렬 실행 중 WebKit comments flow 1건 30초 timeout |
| WebKit 실패 4건 단일 worker 재실행 | 성공 | 4 passed / 28.4초; comments flow 포함, 단독 재현 실패 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P8-001` | High | 수정 완료 | 루트별 답글은 첫 20개만 관리할 수 있다 | `P10-T6` | `P8-R1` |
| `REV-P8-002` | Medium | 수정 완료 | 댓글 0건에서 명시적 empty state가 없다 | `P10-T6` | `P8-R1` |
## 6. 발견 사항 상세
### REV-P8-001 — reply pagination이 없어 21번째 이후 답글에 접근할 수 없다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `COMMENT-001`, `COMMENT-006~007`
- **관련 계약:** replies GET `page`, `size`, response `totalCount`
- **소유 Task:** 신규 `P8-R1`
**관찰 내용**
`loadReplies`는 항상 `page:0,size:20`으로 호출하고 받은 items만 표시한다. root 목록에만 `ResourcePagination`이 있고 reply section에는 page state/control이 없다.
**재현 또는 검증 절차**
1. 하나의 root에 활성 답글 21개 이상인 응답을 준비한다.
2. 답글 보기를 연다.
3. 첫 20개만 보이고 다음 page 요청/버튼이 없다.
4. 요구 결과는 서버 pagination으로 모든 직접 답글에 접근하는 것이다.
**영향**
관리자가 21번째 이후 팬·AI 답글을 조회·수정·삭제할 수 없다.
**권장 조치**
root별 `{page,state}`를 관리하고 reply `ResourcePagination`을 추가한다. page 이동, mutation 후 현재 page refetch, 마지막 item 삭제 경계를 test한다.
**판정 기록**
- 2026-07-29 — reply query와 렌더 경로 전체 대조로 확정.
- 2026-07-30 — `CommentThread`에 root별 reply page state와 reply `ResourcePagination`을 추가하고, 21번째 reply 접근 및 마지막 item 삭제 후 유효 page 복귀 회귀 test로 확인했다.
### REV-P8-002 — 댓글 0건 empty state가 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD `10.5`
- **관련 계약:** comment page `totalCount=0,items=[]`
- **소유 Task:** 신규 `P8-R1`
**관찰 내용**
root load가 성공하면 item 수와 무관하게 `총 0개`와 빈 container, pagination만 렌더한다.
**영향**
운영자가 정상 빈 결과인지 렌더 누락인지 명확히 구분하기 어렵다.
**권장 조치**
댓글 전용 empty `PageState`를 추가하고 작성 form은 유지하는 test를 보강한다.
**판정 기록**
- 2026-07-29 — content branch의 empty 분기 0건으로 확정.
- 2026-07-30 — root `totalCount=0`에서 댓글 작성 form은 유지하고 `댓글이 없습니다` empty `PageState`를 표시하도록 회귀 test와 구현을 추가했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P8-001~002``P8-R1`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | comment API/UI/test 대조 |
| 후보 항목 판정 완료 | 충족 | 2건 확정 |
| 확정 항목 plan 반영 | 충족 | `P8-R1` |
| 보류 항목 담당·재개 조건 | 해당 없음 | server integration은 기존 Gate 상태 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** Phase 9 inactive/accessibility, 기존 server integration.
## 9. 수정 후 검증 기록
애플리케이션 수정은 아직 하지 않았다. 병렬 통합 실행에서 timeout 난
WebKit 4건을 `--project=webkit --workers=1 --last-failed`로 다시 실행해
4 passed / 28.4초를 확인했다. Comments flow도 2.1초에 통과했으므로 이
timeout은 재현 가능한 Comments 기능 결함으로 확정하지 않았다.
### P8-R1 수정 완료, 2026-07-30
- 무엇을: `CommentThread`의 direct reply 조회를 root별 `{ page, state }`로 관리하고 reply `ResourcePagination`을 추가했다. Root 댓글 0건에서는 create form을 유지한 채 `댓글이 없습니다` empty `PageState`를 표시한다.
- 왜: `REV-P8-001`은 reply GET이 항상 `page=0,size=20`이라 21번째 이후 답글에 접근할 수 없었고, `REV-P8-002`는 root 0건에서 정상 빈 결과를 명확히 구분하지 못했다.
- 어떻게:
- RED: `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx`는 3 tests 중 2 failed였다. 실패 원인은 reply 영역에 `다음 페이지` 버튼이 없고, root 0건에서 `댓글이 없습니다` empty state 없이 pagination이 렌더되는 것이었다.
- GREEN: 같은 focused test는 3 tests 통과했다. 회귀는 21개 reply의 page 1 요청·표시, 마지막 reply 삭제 후 page 1 재조회와 page 0 복귀, root empty state와 create form 동시 노출을 확인한다.
- 회귀: `npm run test:run -- src/features/comments src/features/audio-contents src/features/community-posts`는 15 files / 87 tests 통과했다. `npm run e2e:mock -- tests/e2e/comments.spec.ts`는 12 tests 중 10 passed / 2 skipped였다. skipped 2건은 기존 WebKit/mobile Safari keyboard-only path 조건이다.
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. build는 252 modules transformed와 `dist/assets/index-B1Jymnar.js` 492.57 kB(gzip 128.23 kB)를 기록했다.
- LSP: `CommentThread.tsx` diagnostics는 clean이었다. `comment-thread.test.tsx` 단일 fresh diagnostics는 3초 timeout됐으나 focused tests와 `typecheck`로 보완했다.

View File

@@ -0,0 +1,164 @@
# Phase 9 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 9 / 교차 회귀·반응형·접근성·보안 |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 도메인별 focused test가 놓친 read-only policy, form 오류 접근성과 통합 Gate 신뢰도를 교차 점검한다.
### 포함 범위
- 코드: Character/Audio/Series/Community/Comments mutation entrypoint, form error markup
- 테스트: 7개 domain mock E2E, 전체 unit, typecheck/lint/build
- 문서: PRD `7.2`, `10.5~10.7`, `14`, `P9-T1~P9-GATE`
- 수동 검증: inactive Character 응답에 대한 화면별 action inventory
### 제외 범위
- 실제 Edge/실기기 최신 2개 major와 실제 개발 API fixture
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `CHAR-007`, PRD `7.2`, `10.5`, `14.1`
- 코드: `CharacterEditPage.tsx:207`, `AudioContentListPage.tsx:96`, `AudioContentDetailPage.tsx:63`, `AudioContentDetailPage.tsx:69`, `AudioContentFormPage.tsx:77`, `SeriesListPage.tsx:89`, `SeriesDetailPage.tsx:63`, `SeriesFormPage.tsx:44`, `SeriesOrderPage.tsx:49`, `CommunityPostSheet.tsx:169`
- form markup: Character/Audio/Series/Community inputs의 `aria-invalid`와 별도 `role=alert`
- 테스트: `tests/e2e/resource-workflows.spec.ts`, `responsive-capabilities.spec.ts`, `accessibility.spec.ts`, domain E2E
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
Browser: Playwright Chromium/WebKit/mobile viewport projects
API mode: VITE_API_MODE=mock
```
### 실행한 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 실패 | 66 files / 290 tests, 6 failed·284 passed; `REV-P10-001` |
| `npm run e2e:mock --` 7개 Phase 10 spec | 실패 | 168 tests: 147 passed, 17 skipped, WebKit timeout 4건 |
| WebKit 실패 spec 단일 worker 재실행 | 성공 | 4 passed / 28.4초; 병렬 timeout은 단독 재현되지 않음 |
| `npm run typecheck` | 성공 | exit 0 |
| `npm run lint` | 성공 | exit 0 |
| `npm run build` | 성공 | 249 modules transformed, production build exit 0 |
| inactive action code inventory | 실패 재현 | banner와 무관하게 여러 직접 mutation route/action 활성 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P9-001` | High | 수정 완료 | 비활성 Character에서 여러 하위 mutation이 계속 가능하다 | `P9-T1` | `P9-R1` |
| `REV-P9-002` | Medium | 수정 완료 | form field 오류가 입력과 연결되지 않고 첫 오류 focus가 없다 | `P9-T2` | `P9-R2` |
## 6. 발견 사항 상세
### REV-P9-001 — inactive workspace의 쓰기 차단이 도메인별로 일관되지 않다
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `CHAR-007`, PRD `7.2`, `14.1`
- **관련 계약:** Character detail `isActive=false`
- **소유 Task:** 신규 `P9-R1`
**관찰 내용**
workspace는 read-only banner만 표시한다. Character edit 직접 route, Audio create/edit/deactivate와 댓글, Series create/edit/link/unlink/order, Community Sheet의 CommentThread는 `character.isActive`를 mutation capability에 결합하지 않는다. Community post mutation과 FanTalk reply entrypoint만 일부 차단한다.
**재현 또는 검증 절차**
1. Character detail 응답을 `isActive=false`로 반환한다.
2. `/ai-characters/:id/edit`, Audio create/edit/detail, Series create/edit/detail/order, Community post Sheet로 진입한다.
3. mutation form/button 또는 comment 작성·삭제가 노출되고 client 요청을 실행할 수 있다.
4. 요구 결과는 조회만 유지하고 모든 하위 mutation 진입점과 직접 route가 차단되는 것이다.
**영향**
frontend read-only 안전장치가 무력화되어 비활성 Character 데이터에 쓰기 요청이 발생한다. backend 검증이 있어도 반복 오류와 운영 오조작을 유발한다.
**권장 조치**
viewport capability와 별도로 `character.isActive`를 결합한 명시적 mutation capability를 각 Page에서 계산하고 form/direct route까지 차단한다. Audio/Series/Comments/Character direct-route E2E를 추가한다.
**판정 기록**
- 2026-07-29 — 모든 workspace consumer의 `isActive` 사용처를 대조해 확정.
- 2026-07-30 — `P9-R1`에서 inactive workspace mutation 차단을 구현하고 focused unit 28 files / 152 tests, 관련 mock E2E 103 passed / 13 skipped, typecheck/lint/build exit 0으로 수정 완료.
### REV-P9-002 — field error 접근성 계약이 구현되지 않았다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD `10.5`, `10.7`
- **관련 계약:** 없음
- **소유 Task:** 신규 `P9-R2`
**관찰 내용**
주요 form은 `aria-invalid`와 별도 `role=alert` 문구는 제공하지만 input의 `aria-describedby`/`aria-errormessage`와 error element ID 연결이 없다. submit validation 뒤 첫 오류 control로 focus를 옮기는 코드도 없다.
**영향**
screen reader와 keyboard 사용자가 어떤 입력에 어떤 오류가 생겼는지, 어디서 수정해야 하는지 빠르게 파악하기 어렵다.
**권장 조치**
form별 stable error ID와 describedby 연결, 제출 후 DOM 순서 첫 invalid focus helper를 최소 공통 규칙으로 적용하고 keyboard/accessible-description test를 추가한다.
**판정 기록**
- 2026-07-29 — Character/Audio/Series/Community form markup과 focus 호출을 검색해 확정.
- 2026-07-30 — `P9-R2`에서 주요 form error ID·description 연결과 첫 invalid focus를 구현하고 focused unit 5 files / 35 tests, `src/features` 35 files / 195 tests, accessibility mock E2E 9 passed / 3 skipped, typecheck/lint/build exit 0으로 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P9-001``P9-R1`
- `REV-P9-002``P9-R2`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 교차 policy·form·Gate 대조 |
| 후보 항목 판정 완료 | 충족 | 2건 수정 완료 |
| 확정 항목 plan 반영 | 충족 | `P9-R1`, `P9-R2` |
| 보류 항목 담당·재개 조건 | 충족 | 실제 browser/server QA는 릴리스 QA와 `P10-GATE` |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 검증 완료.
**남은 항목:** 실제 개발 API fixture가 필요한 server integration은 `P10-GATE` 수동 QA로 분리한다.
## 9. 수정 후 검증 기록
애플리케이션 수정은 아직 하지 않았다. 병렬 통합 실행에서 timeout 난
WebKit 4건을 `--project=webkit --workers=1 --last-failed`와 단일 worker로
다시 실행해 4 passed / 28.4초를 확인했다. 병렬 부하에서의 Gate
불안정성은 남지만 동일 시나리오가 단독 재현되지 않아 별도 앱 결함으로
확정하지 않았다. `npm run build`는 249 modules transformed와 exit 0으로
통과했다.
### P9-R1~P9-R2 수정 후 검증 — 2026-07-30
- `P9-R1`: inactive Character workspace의 하위 mutation 진입점과 직접 route를 read-only guidance로 교체했다. `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments`는 28 files / 152 tests passed, 관련 mock E2E는 103 passed / 13 skipped였다.
- `P9-R2`: 주요 form 오류 문구를 stable ID와 `aria-describedby`/`aria-invalid`로 연결하고 submit 뒤 첫 invalid control focus를 적용했다. focused unit은 5 files / 35 tests passed, `npm run test:run -- src/features`는 35 files / 195 tests passed, `npm run e2e:mock -- tests/e2e/accessibility.spec.ts`는 9 passed / 3 skipped였다.
- 공통 gate: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. 이후 `P9-GATE` fresh 재검증은 `npm run test:run` 72 files / 354 tests passed, exact `npm run e2e:mock` 206 passed / 22 skipped, `npm run e2e` server allowlist 36 passed로 통과했다.

View File

@@ -1,7 +1,7 @@
# 환경 변수
- 개발 서버 API: `VITE_API_BASE_URL=https://test-character-admin.sodalive.net`
- 프로덕션 서버 API: `VITE_API_BASE_URL=https://character-admin.sodalive.net`
- 개발 서버 API: `VITE_API_BASE_URL=https://test-api.sodalive.net`
- 프로덕션 서버 API: `VITE_API_BASE_URL=https://api.sodalive.net`
- API mode: `VITE_API_MODE=server | mock`. 누락 시 `server`이며, `mock`은 개발 환경에서만 허용한다.
- Vite mode별 파일은 `.env.development`, `.env.production`을 사용한다.
- 기본 `npm run dev``server` mode로 실제 개발 API를 사용하고, `npm run dev:mock`만 browser MSW를 시작한다.

View File

@@ -12,7 +12,8 @@
- 대상 `prd.md``plan-task.md`가 있는 기능 문서 디렉터리 아래 `reviews/`를 만들고 모든 리뷰 문서를 그 안에 둔다.
- 리뷰 문서를 기능 문서 디렉터리 바로 아래나 단수형 `review/`에 두지 않는다. 여러 Phase·Task 리뷰가 생겨도 같은 `reviews/`에 누적한다.
- [코드 리뷰 보고서 샘플](../sample/sample-review.md)을 원본 템플릿으로 사용하고, section·필드·상태 의미를 임의로 축소하지 않는다.
- 실제 리뷰 문서 파일명은 범위가 드러나게 작성한다. 예: `review-phase-0-1.md`, `review-auth.md`.
- 새로 생성하는 리뷰 문서 파일명은 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>.md` 패턴을 사용한다. `phase`와 번호 사이에는 하이픈을 넣지 않는다. 예: `phase1-character-management.md`, `phase2-audio-content.md`.
- 이 파일명 규칙은 새 리뷰 문서에만 적용한다. 이미 생성된 리뷰 문서는 이름과 기존 참조 링크를 변경하지 않는다.
- `prd.md``plan-task.md`에서 리뷰 문서를 참조할 때는 `./reviews/<리뷰 파일명>.md` 상대 링크를 사용한다.
## 3. 리뷰 수행 원칙

View File

@@ -10,6 +10,6 @@
- Vitest watch: `npm run test`
- Vitest 단발 실행: `npm run test:run`
- Playwright E2E(server mode): `npm run e2e``playwright.config.ts`의 server mode `testMatch`에 있는 spec만 실행하며, 추가 file filter를 넘기면 교집합만 실행한다.
- Playwright E2E(mock mode): `npm run e2e:mock``playwright.config.ts`의 mock mode `testMatch`에 있는 spec만 실행하며, 추가 file filter를 넘기면 교집합만 실행한다.
- Playwright E2E(mock mode): `npm run e2e:mock` 인자 없이 실행하면 Chromium/mobile Chrome matrix를 분할 실행하며, file filter나 `--project` 인자를 넘기면 `playwright.config.ts`의 mock mode `testMatch` 교집합만 실행한다.
- Mock preview domain rule: 후속 도메인 Phase는 자기 handler, fixture, mock E2E를 같은 Phase에서 소유하고 추가한다.
- Phase 0 Gate 기준: `npm ci`, `npx playwright install chromium webkit`, `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`
- Phase 0 Gate 기준: `npm ci`, `npx playwright install chromium`, `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB