docs(ai-character): 리소스 관리 문서 정리
This commit is contained in:
@@ -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
@@ -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는 테스트 시간을 크게 늘리므로 현재 릴리스 범위에서 제외한다. |
|
||||
|
||||
241
docs/20260725_AI캐릭터관리자웹/reviews/phase0-project-foundation.md
Normal file
241
docs/20260725_AI캐릭터관리자웹/reviews/phase0-project-foundation.md
Normal 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 가용성은 자동 범위 밖이다.
|
||||
@@ -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 전환 없음.
|
||||
852
docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md
Normal file
852
docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md
Normal 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가 남았다.
|
||||
146
docs/20260725_AI캐릭터관리자웹/reviews/phase2-mock-preview.md
Normal file
146
docs/20260725_AI캐릭터관리자웹/reviews/phase2-mock-preview.md
Normal 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를 계속 분리한다.
|
||||
350
docs/20260725_AI캐릭터관리자웹/reviews/phase3-character-workspace.md
Normal file
350
docs/20260725_AI캐릭터관리자웹/reviews/phase3-character-workspace.md
Normal 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` 완료 후 회귀가 필요하다.
|
||||
361
docs/20260725_AI캐릭터관리자웹/reviews/phase4-audio-content.md
Normal file
361
docs/20260725_AI캐릭터관리자웹/reviews/phase4-audio-content.md
Normal 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` 회귀가 남는다.
|
||||
283
docs/20260725_AI캐릭터관리자웹/reviews/phase5-series-management.md
Normal file
283
docs/20260725_AI캐릭터관리자웹/reviews/phase5-series-management.md
Normal 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` 회귀가 남는다.
|
||||
291
docs/20260725_AI캐릭터관리자웹/reviews/phase6-community-posts.md
Normal file
291
docs/20260725_AI캐릭터관리자웹/reviews/phase6-community-posts.md
Normal 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 계측은 수동 범위다.
|
||||
189
docs/20260725_AI캐릭터관리자웹/reviews/phase7-fan-talk.md
Normal file
189
docs/20260725_AI캐릭터관리자웹/reviews/phase7-fan-talk.md
Normal 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` 회귀가 남는다.
|
||||
286
docs/20260725_AI캐릭터관리자웹/reviews/phase8-comments.md
Normal file
286
docs/20260725_AI캐릭터관리자웹/reviews/phase8-comments.md
Normal 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` 회귀가 남는다.
|
||||
828
docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md
Normal file
828
docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md
Normal 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가 남았다.
|
||||
90
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-0-20260729.md
Normal file
90
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-0-20260729.md
Normal 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이 없어 기록 없음.
|
||||
189
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-1-20260729.md
Normal file
189
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-1-20260729.md
Normal 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 항목으로 유지한다.
|
||||
167
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-10-20260729.md
Normal file
167
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-10-20260729.md
Normal 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로 분리한다.
|
||||
90
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2-20260729.md
Normal file
90
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2-20260729.md
Normal 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이 없어 기록 없음.
|
||||
184
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-3-20260729.md
Normal file
184
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-3-20260729.md
Normal 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.
|
||||
247
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-4-20260729.md
Normal file
247
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-4-20260729.md
Normal 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` 통과.
|
||||
184
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-5-20260729.md
Normal file
184
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-5-20260729.md
Normal 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 정책은 후속 범위로 유지한다.
|
||||
129
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-6-20260729.md
Normal file
129
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-6-20260729.md
Normal 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는 후속 범위로 유지한다.
|
||||
89
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-7-20260729.md
Normal file
89
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-7-20260729.md
Normal 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이 없어 기록 없음.
|
||||
163
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-8-20260729.md
Normal file
163
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-8-20260729.md
Normal 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`로 보완했다.
|
||||
164
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-9-20260729.md
Normal file
164
docs/20260725_AI캐릭터관리자웹/reviews/review-phase-9-20260729.md
Normal 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로 통과했다.
|
||||
@@ -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를 시작한다.
|
||||
|
||||
@@ -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. 리뷰 수행 원칙
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user