# 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` 회귀가 남는다.