diff --git a/docs/20260806_커뮤니티댓글답글/api-contract.md b/docs/20260806_커뮤니티댓글답글/api-contract.md new file mode 100644 index 0000000..1a8a217 --- /dev/null +++ b/docs/20260806_커뮤니티댓글답글/api-contract.md @@ -0,0 +1,96 @@ +# 커뮤니티 댓글 직접 답글 API Contract + +## 문서 정보 + +| 항목 | 내용 | +|---|---| +| 상태 | 기존 계약 재사용 확정 | +| 작성일 | 2026-08-06 | +| 원본 계약 | [프로젝트 OpenAPI](../20260725_AI캐릭터관리자웹/api-contract.openapi.json) | +| 관련 PRD | [prd.md](./prd.md) | +| 관련 계획 | [plan-task.md](./plan-task.md) | + +## 계약 변경 여부 + +백엔드 API 변경은 없다. 이 문서는 이번 기능이 소비하는 기존 OpenAPI 범위와 +프론트엔드 전송값만 좁게 기록한다. 충돌하면 원본 OpenAPI가 우선한다. + +## 댓글 구조 불변식 + +- `parentId=null` 또는 생략: 원댓글 +- `parentId=원댓글 ID`: 해당 원댓글의 직접 답글 +- 하나의 원댓글 ID를 여러 POST의 `parentId`로 사용할 수 있으며 각 응답은 별도 직접 답글 row가 된다. +- `parentId=답글 ID`인 3단계 작성은 허용하지 않는다. +- parent는 같은 `characterId`·`postId`의 활성 원댓글이어야 한다. + +## Endpoint + +### 직접 답글 목록 + +```http +GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies?page=0&size=20 +Authorization: Bearer {jwt-token} +Accept-Language: ko +``` + +- `commentId`: 답글 영역을 연 원댓글 ID +- 성공: `data={ totalCount, items }` +- 답글 0개도 `totalCount=0`, `items=[]`인 정상 성공이다. +- 여러 직접 답글은 `items`의 독립 row로 반환되며 기존 pagination을 사용한다. + +### 댓글 또는 직접 답글 작성 + +```http +POST /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments +Authorization: Bearer {jwt-token} +Accept-Language: ko +Content-Type: application/json +``` + +직접 답글 request: + +```json +{ + "comment": "답글 내용", + "parentId": 2102, + "isSecret": false +} +``` + +| field | 형식 | 이번 기능의 값 | +|---|---|---| +| `comment` | string, required | trim 후 빈 문자열이 아닌 입력값 | +| `parentId` | nullable int64, optional | 답글 대상 활성 원댓글 ID | +| `isSecret` | boolean, optional | `false` | + +- Community request에는 Audio 전용 `languageCode`를 보내지 않는다. +- 같은 원댓글에 추가 답글을 쓸 때도 같은 endpoint와 원댓글 `parentId`를 사용한다. +- 성공 envelope의 `data`는 `null`이다. +- 성공 후 원댓글 목록과 열린 원댓글의 현재 답글 page를 재조회한다. + +## 오류 응답 + +원본 OpenAPI의 공통 오류 envelope와 다음 status를 그대로 사용한다. + +| Status | 처리 | +|---:|---| +| 400 | invalid target·parent 또는 binding 오류를 화면 alert로 표시 | +| 401 | 공통 session 만료 처리 | +| 403 | 공통 접근 거부 처리 | +| 404 | target 또는 root를 찾을 수 없음 표시 | +| 405, 406, 415, 500 | 서버 message를 우선 표시하고 기존 재시도 정책 적용 | + +도메인별 message key와 validation 상한을 새로 추정하지 않는다. + +## 프론트엔드 연결 + +| 역할 | 기존 구현 | +|---|---| +| target path 선택 | `commentCollectionPath()`의 `community` branch | +| 답글 조회 | `getReplies()` | +| 답글 작성 | `createComment()`의 Community overload | +| request schema | `communityCommentCreateRequestSchema` | +| 답글 상태·pagination | `CommentThread`의 `replies`, `loadReplies()` | +| 성공 후 재조회 | `CommentThread.runMutation()` | + +API, schema, mock handler와 store는 이번 기능에서 변경하지 않는다. diff --git a/docs/20260806_커뮤니티댓글답글/plan-task.md b/docs/20260806_커뮤니티댓글답글/plan-task.md new file mode 100644 index 0000000..7f1f4d7 --- /dev/null +++ b/docs/20260806_커뮤니티댓글답글/plan-task.md @@ -0,0 +1,240 @@ +# 커뮤니티 댓글 직접 답글 구현 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 구현 완료 | +| 작성일 | 2026-08-06 | +| 요구사항 기준 | [prd.md](./prd.md) | +| API 기준 | [api-contract.md](./api-contract.md) | +| 현재 Phase | Phase 1 완료 | +| 현재 활성 Goal | 없음 | + +## 목표 + +활성 커뮤니티 게시글의 답글 0개 원댓글에서도 기존 답글 form을 열어 첫 답글과 +여러 직접 답글을 작성할 수 있게 한다. + +## 현재 상태 + +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | +|---:|---|---:|---|---| +| 1 | 완료 | `3/3` | 없음 | 완료 | + +- Community 답글 GET·POST, form, 여러 직접 답글 조회·작성·재조회 흐름은 이미 구현돼 있다. +- 답글이 하나 이상인 Community root에는 `답글 보기`와 추가 작성 form이 제공된다. +- `replyCount === 0`인 Community root에는 첫 답글 작성 진입만 없다. +- `P1-T1` 구현과 test는 완료됐고, `P1-R1`에서 E2E fixture 검증 결함 후보를 실제 mock 실행 경로와 대조해 오탐으로 판정했다. + +## 범위의 포함·제외 + +### 포함 + +- 활성 Community root의 `replyCount === 0`일 때 `답글 작성` 버튼 표시 +- 기존 답글 영역, form, GET·POST와 mutation 상태 재사용 +- 같은 원댓글에 첫 답글과 여러 직접 답글 작성 +- Community 첫 답글과 Audio·비활성·reply row 경계 회귀 test +- 기존 Comments Chromium mock E2E와 정적 검증 + +### 제외 + +- 새 endpoint, DTO, component, state library 또는 dependency +- form 상시 노출, reply-of-reply, payload 정책 변경 +- 기존 답글 수정·삭제·pagination 리팩터링 +- Audio 전용 `languageCode`의 Community payload 추가 +- optimistic update와 답글 전체 선조회 + +## 기술적 제약 + +- React·TypeScript strict, Vitest·React Testing Library와 기존 Playwright 구성을 사용한다. +- [api-contract.md](./api-contract.md)의 기존 GET·POST만 사용한다. +- `CommentThread`, `CommentItem`, `CommentForm`의 현재 책임 경계를 유지한다. +- `CommentItem`의 기존 `replyActionLabel`, `CommentThread.toggleReplies()`와 reply state를 재사용한다. +- 공통 조건 한 곳에서 Audio와 Community의 첫 답글 진입을 일치시키며 target별 분기를 추가하지 않는다. +- RED → GREEN → REFACTOR 순서와 최소 변경을 지킨다. + +## Phase 1. 커뮤니티 직접 답글 진입 구현·검증 + +**Phase 결과:** 관리자가 활성 Community의 답글 0개 원댓글에서 첫 답글을 +작성하고 같은 원댓글에 여러 직접 답글을 추가하며, 기존 Audio·읽기 전용·2단계 +경계가 유지된다. + +**선행조건:** `CCR-001~006`과 기존 Community 댓글 GET·POST 계약 확정. + +**Phase 완료 조건:** `P1-T1`, `P1-GATE` 완료와 Progress 기록. + +### Task 1.1 커뮤니티 첫 답글 진입 + +**Goal 실행 `P1-T1`:** Community의 답글 0개 원댓글에 기존 답글 영역을 여는 +`답글 작성` action을 추가하고 직접 답글 작성 흐름을 검증한다. + +- **시작 조건:** [prd.md](./prd.md)의 `CCR-001~006`, [api-contract.md](./api-contract.md). +- **완료 증거:** TDD 체크박스, focused·회귀·E2E·정적 검증과 Progress 기록. +- **범위 밖:** API·mock·schema 변경, 새 UI 구조, 관련 없는 Comments 리팩터링. + +**Files:** + +- Modify: `src/features/comments/components/CommentThread.tsx` +- Modify: `src/features/comments/tests/comment-thread.test.tsx` +- Modify: `tests/e2e/comments.spec.ts` +- Test: `src/features/comments/tests/comment-thread.test.tsx`, `tests/e2e/comments.spec.ts` + +**Interfaces:** + +- Consumes: `CommentRecord.replyCount`, `canMutate`, `expandedRootIds`, `toggleReplies()`, `CommentForm`, Community `createComment()` overload. +- Produces: 활성 Audio·Community 원댓글에 공통 적용되는 첫 답글 action 노출 조건. + +**TDD 절차:** + +- [x] **RED: 실패 테스트 작성/실패 확인** — `comment-thread.test.tsx`에 Community `replyCount=0` root의 `답글 작성` 노출, 클릭 후 form, `parentId` POST와 `languageCode` 미전송을 검증하고 `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx`가 버튼 부재로 실패하는지 확인한다. +- [x] **GREEN: 최소 구현/통과 확인** — `CommentThread.tsx`의 기존 optional action label 조건에서 Audio 전용 제한만 제거하고 같은 명령이 exit 0인지 확인한다. +- [x] **REFACTOR: 정리/회귀 확인** — 추가 helper·component 없이 조건을 읽기 쉬운 최소 표현으로 유지하고 focused test와 `npm run test:run -- src/features/comments`가 모두 exit 0인지 확인한다. +- [x] 기존 Community mock E2E에 답글 0개 root의 첫 답글 작성과 같은 root에 추가 직접 답글 작성 journey를 검증한다. +- [x] 검증 결과를 Progress에 기록한다. + +**검증 기준:** + +- **실행 명령:** `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx`; `npm run test:run -- src/features/comments`; `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`. +- **기대 결과:** 모든 명령 exit 0, Community 첫 답글 POST 1회 이상, `parentId`는 원댓글 ID, Community body의 `languageCode` 0건, reply row의 답글 action 0건, 기존 Audio·Comments 회귀 실패 0건. +- **수동 확인:** 활성 Community Sheet에서 답글 0개 root의 `답글 작성` → form 노출 → 첫 답글 등록 → 같은 root 추가 답글 등록을 확인한다. 비활성 workspace와 reply row에는 작성 진입이 없어야 한다. + +### 완료 조건 + +- [x] `P1-T1`의 모든 TDD·검증 체크박스가 완료됐다. +- [x] `CCR-001~006`이 구현 또는 검증 증거에 연결됐다. +- [x] API·mock·schema와 범위 밖 파일 변경이 없다. + +### Task 1.R1 Community E2E fixture 검증 + +**Goal 실행 `P1-R1`:** `CCR-REV-P1-001`의 E2E fixture 분류 오류 후보가 실제 +mock E2E 실행 경로에 영향을 주는지 검증하고 판정한다. + +- **시작 조건:** `P1-T1` 완료, `CCR-REV-P1-001` 확정. +- **완료 증거:** 실제 mock 요청 소유권 확인, 후보를 구분하는 E2E assertion, Chromium·Comments 회귀·정적 검증과 Progress 기록. +- **범위 밖:** 애플리케이션 mock handler·store, API·schema, production 댓글 동작 변경. + +**Files:** + +- Modify: `tests/e2e/comments.spec.ts` +- Test: `tests/e2e/comments.spec.ts` + +**TDD 예외 사유:** 리뷰 후보를 구분하는 assertion이 기존 mock E2E에서도 통과해 +production 또는 fixture 수정이 필요하지 않은 오탐으로 판정됐다. 실패하는 구현 변경이 +없으므로 RED → GREEN 대신 실제 요청 소유권과 기존 동작을 대체 검증했다. + +- [x] root `2102`의 초기 reply region에 root 댓글이 없고, 첫·두 번째 답글이 region에 표시되며 중첩 action이 없는 assertion을 추가했다. +- [x] 기존 `comments-test-support.ts`를 유지한 상태에서 Chromium E2E `3/3` 통과를 두 번 확인했다. +- [x] `VITE_API_MODE=mock`의 Browser MSW Service Worker가 mock 요청을 처리하며 `page.route` fixture 후보가 실제 실행 경로를 소유하지 않음을 확인했다. +- [x] fixture 변경을 폐기하고 Comments 회귀·typecheck·lint·`git diff --check`를 통과했다. +- [x] 검증 결과와 `CCR-REV-P1-001` 오탐 판정을 Progress에 기록했다. + +**검증 기준:** + +- **실행 명령:** `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run test:run -- src/features/comments`; `npm run typecheck`; `npm run lint`; `git diff --check`. +- **기대 결과:** 기존 fixture를 변경하지 않고 모든 명령 exit 0, Chromium `3/3`, 첫·추가 답글이 root `2102` region에만 표시된다. +- **수동 확인:** 기존 `P1-GATE`의 Community 첫·추가 답글 browser QA 결과와 mock E2E의 동일 동작을 대조한다. + +### 검증 방법 + +#### Phase 1 Gate + +**Goal 실행 `P1-GATE`:** 커뮤니티 첫·추가 직접 답글 journey와 Comments 공통 +경계를 최종 판정한다. + +- **시작 조건:** `P1-T1` 완료. +- **완료 증거:** 아래 명령·수동 확인 통과와 Progress 기록. +- **범위 밖:** test 완화, timeout 상향과 관련 없는 수정. + +**실행 명령:** + +```bash +npm run test:run -- src/features/comments +npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium +npm run typecheck +npm run lint +git diff --check +``` + +**기대 결과:** 모든 명령 exit 0, `CCR-001~006` 위반 0건. + +**수동 확인:** 활성·비활성 Community와 활성 Audio에서 action 노출 경계를 +대조한다. Community Sheet를 1280px·320px와 200% zoom에서 열어 수평 overflow +없이 첫·추가 답글을 작성하고 keyboard-only로 form에 진입한다. + +## 실행 순서와 의존성 + +1. `P1-T1` RED +2. `P1-T1` GREEN +3. `P1-T1` REFACTOR·회귀 +4. `P1-GATE` + +- 동시에 하나의 미완료 goal만 운용한다. +- 사용자가 goal 실행을 요청하기 전에는 goal을 생성하지 않는다. + +## 변경 금지 항목 + +- 기존 OpenAPI, API client, request schema, mock handler·store 변경 +- 새 dependency, state library, component 또는 speculative abstraction +- 답글의 답글, optimistic update와 form 상시 노출 +- Audio payload와 기존 수정·삭제·pagination 동작 변경 +- 실패 test 삭제·skip, timeout 상향으로 Gate 통과 +- 기존 Progress와 결정 기록 삭제·덮어쓰기 + +## 의사결정 및 중단 규칙 + +- `replyCount === 0`, `canMutate === true`인 Audio·Community 원댓글에만 `답글 작성`을 표시한다. +- `replyCount > 0` 또는 펼친 원댓글은 기존 `답글 보기` label을 유지한다. +- reply row에는 `onShowReplies`를 전달하지 않으며 3단계 작성 경로를 만들지 않는다. +- API 응답이나 오류가 [api-contract.md](./api-contract.md)와 다르면 추정 수정하지 않고 외부 의존으로 기록한다. +- 범위가 바뀌면 코드보다 PRD Decision Log와 이 계획을 먼저 갱신한다. + +## Progress + +### 2026-08-06 요구사항·설계 + +- **무엇을:** 활성 Community 원댓글의 첫 답글 진입, 여러 직접 답글과 2단계 제한을 요구사항·API 재사용 계약·단일 구현 Task로 정리했다. +- **왜:** Community 답글 조회·작성 흐름은 이미 있으나 `replyCount === 0`이면 진입 action이 없어 첫 답글만 작성할 수 없다. +- **어떻게:** 선행 Audio 답글 문서, 프로젝트 OpenAPI, `CommentThread`, request schema, mock handler·store, unit·E2E를 대조했다. 기존 공통 흐름을 재사용할 수 있어 새 API·컴포넌트·mock을 계획에서 제외했다. 애플리케이션 코드와 test는 변경하지 않았다. + +### 2026-08-06 `P1-T1` 커뮤니티 첫 답글 진입 + +- **무엇을:** 활성 Community의 `replyCount=0` 원댓글에도 기존 `답글 작성` action을 노출하고, 같은 원댓글에 첫 번째와 두 번째 직접 답글을 작성하는 단위·Chromium E2E를 추가했다. reply row의 중첩 답글 action 부재와 Community payload의 `languageCode` 미전송도 검증했다. +- **왜:** 기존 공통 GET·POST·form·재조회 흐름은 완성돼 있었지만 action label 조건이 Audio target만 허용해 Community 첫 답글 진입이 막혀 있었다. +- **어떻게:** RED에서 `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx`를 실행해 `AI 루트 댓글 답글 작성` 버튼 부재로 `1 failed, 7 passed`를 확인했다. GREEN에서 `CommentThread.tsx`의 Audio 전용 조건만 제거한 뒤 focused test `8/8`을 통과했다. REFACTOR·회귀로 `npm run test:run -- src/features/comments`는 `15/15`, `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`은 `3/3`, `npm run typecheck`와 `npm run lint`는 exit 0이었다. API·schema·mock·dependency는 변경하지 않았다. + +### 2026-08-06 `P1-R1` E2E fixture 후보 판정 + +- **무엇을:** `CCR-REV-P1-001`이 지적한 단일 `replyRootId` fixture가 mock E2E의 root `2102` 답글을 오분류하는지 검증했다. +- **왜:** 코드만 보면 `comments-test-support.ts`가 root `2101`만 replies로 처리하지만, 실제 mock E2E가 이 fixture를 사용하는지 확인하지 않으면 오탐 수정으로 범위를 확장할 수 있다. +- **어떻게:** 기존 fixture를 유지한 상태에서 `2102` 초기 reply region에 root 댓글 0건, 첫·두 번째 답글 표시, dialog 내 각 1건, 중첩 action 0건을 추가하고 Chromium E2E `3/3` 통과를 두 번 확인했다. `playwright.config.ts`의 `VITE_API_MODE=mock`과 `src/shared/mocks/browser.ts`의 `setupWorker(...)`를 대조해 Browser MSW가 Service Worker에서 요청을 처리하며 `page.route`가 해당 요청을 소유하지 않음을 확인했다. fixture 변경은 폐기했고 `CCR-REV-P1-001`을 오탐으로 판정했다. + +### 2026-08-06 `P1-GATE` Phase 1 최종 검증 + +- **무엇을:** Community 첫·추가 직접 답글, 2단계·권한 경계, Comments 회귀와 반응형·keyboard·CJK 품질을 최종 판정했다. +- **왜:** 코드와 자동 test 통과만으로는 실제 Sheet의 keyboard 진입, 320px·200% zoom, 한국어 줄바꿈과 reviewer 차단 해소를 증명할 수 없다. +- **어떻게:** `npm run test:run -- src/features/comments`는 `15/15`, `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`은 `3/3`, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `git diff --check`는 exit 0이었다. 실제 Chromium에서 첫·두 번째 답글, input 초기화, 중첩 action 0건, keyboard-only 진입과 1280px·320px·200% zoom의 수평 overflow 0건을 확인했다. 독립 goal·코드 품질·보안·컨텍스트·기능·visual/CJK 리뷰는 최종 PASS였고 [Phase 1 리뷰](./reviews/phase1-community-comment-replies.md)에 근거를 기록했다. + +## Decision Log + +| 날짜 | 결정 | 근거 | 영향 | +|---|---|---|---| +| 2026-08-06 | Audio와 동일한 `답글 작성` 진입을 활성 Community 원댓글에도 적용한다. | 사용자 요청 | `CCR-001~003`, `P1-T1` | +| 2026-08-06 | 한 원댓글에 여러 직접 답글을 허용하고 reply-of-reply는 제외한다. | 사용자 요청 | `CCR-004~005`, `P1-T1`, `P1-GATE` | +| 2026-08-06 | 기존 공통 UI와 Community GET·POST를 재사용하고 API·mock·schema는 변경하지 않는다. | OpenAPI와 코드 확인 | `CCR-002~006`, `P1-T1` | +| 2026-08-06 | 구현은 공통 action 조건의 Audio 전용 제한 제거와 기존 test 보강으로 제한한다. | `CommentThread` 흐름 확인과 최소 변경 원칙 | `P1-T1` Files·Interfaces | +| 2026-08-06 | E2E 전용 route fixture가 특정 root만 replies로 처리하는 결함을 `P1-R1`에서 수정한다. | 최종 코드 품질·컨텍스트 리뷰에서 `2102` 답글이 roots에 저장돼 E2E가 오탐 통과함을 확인 | `CCR-REV-P1-001`, `P1-R1`, `P1-GATE` | +| 2026-08-06 | 정정: `CCR-REV-P1-001`은 mock mode에서 Browser MSW가 요청을 소유해 E2E route fixture 분기가 실행되지 않으므로 오탐이다. fixture를 변경하지 않는다. | 기존 fixture 상태에서 2102 빈 reply·첫·추가 답글 assertion과 Chromium `3/3` 통과, `VITE_API_MODE=mock`·`setupWorker(...)` 확인 | `CCR-REV-P1-001`, `P1-R1`, `P1-GATE` | + +## 발견된 문제 + +- 수정 완료: 답글 0개 Community root의 첫 답글 작성 진입을 `P1-T1`에서 구현하고 `P1-GATE`에서 검증했다. +- 확정: E2E 전용 fixture가 `replyRootId` 하나만 replies로 분류해 다른 root의 직접 답글을 roots에 저장한다. (`CCR-REV-P1-001`, `P1-R1`에서 수정 예정) +- 오탐: `CCR-REV-P1-001` — mock mode에서는 Browser MSW가 요청을 처리해 해당 E2E route fixture 분기가 실행되지 않으며, 기존 fixture 상태에서 root `2102`의 빈 reply·첫·추가 답글 journey가 통과한다. +- 외부 차단: 없음. + +## 최종 보고 형식 + +- 완료 Goal ID +- 변경한 파일과 최소 구현 내용 +- RED·GREEN·REFACTOR 및 Gate 명령과 실제 결과 +- 실행하지 못한 수동·server 검증과 이유 +- 남은 위험 또는 열린 질문 diff --git a/docs/20260806_커뮤니티댓글답글/prd.md b/docs/20260806_커뮤니티댓글답글/prd.md new file mode 100644 index 0000000..10b945a --- /dev/null +++ b/docs/20260806_커뮤니티댓글답글/prd.md @@ -0,0 +1,227 @@ +# 커뮤니티 댓글 직접 답글 PRD + +## 문서 정보 + +| 항목 | 내용 | +|---|---| +| 문서 상태 | 구현 기준 확정 | +| 작성일 | 2026-08-06 | +| 최종 수정일 | 2026-08-06 | +| 대상 기능 | 커뮤니티 게시글 댓글의 직접 답글 작성 진입 | +| 작성자·결정권자 | Codex 작성, 사용자 결정 | +| 상위 제품 기준 | [AI 캐릭터 관리자 웹 PRD](../20260725_AI캐릭터관리자웹/prd.md) | +| 선행 기능 기준 | [오디오 콘텐츠 댓글 답글 PRD](../20260805_오디오콘텐츠댓글답글/prd.md) | +| 관련 API Contract | [api-contract.md](./api-contract.md) | +| 관련 구현 계획 | [plan-task.md](./plan-task.md) | +| 관련 review | [Phase 1 커뮤니티 댓글 직접 답글 리뷰](./reviews/phase1-community-comment-replies.md) | + +### 요구사항 상태 + +| 상태 | 의미 | +|---|---| +| 확정 | 구현과 검증 기준으로 사용한다. | +| 미결 | 제품 결정 전에는 구현하지 않는다. | +| 외부 의존 | 외부 계약이 제공될 때까지 영향 범위를 구현 완료로 표시하지 않는다. | +| 제외 | 현재 기능 범위에 포함하지 않는다. | + +## 1. Overview + +활성 AI 캐릭터의 커뮤니티 게시글 원댓글에 직접 답글을 작성할 수 있게 한다. +원댓글 아래에는 여러 개의 직접 답글을 추가할 수 있지만, 답글에 다시 답글을 +다는 3단계 구조는 허용하지 않는다. 기존 오디오 콘텐츠 댓글과 같은 진입 UI, +답글 영역, 작성 form과 mutation 상태를 재사용한다. + +## 2. Problem Statement + +커뮤니티 답글 조회·작성 API와 UI는 이미 구현돼 있어 답글이 하나 이상인 +원댓글에는 추가 답글을 작성할 수 있다. 그러나 `replyCount=0`인 원댓글에는 +답글 영역을 여는 action이 없어 첫 답글을 작성할 수 없다. + +문제를 해결했다는 판단은 답글 0개인 활성 커뮤니티 원댓글에서 `답글 작성`을 +눌러 첫 답글을 등록하고, 같은 원댓글에 여러 직접 답글을 계속 추가할 수 있는지로 +한다. + +## 3. Goals + +### 3.1 제품 목표 + +- 활성 커뮤니티 게시글의 모든 원댓글에 첫 답글을 작성할 수 있다. +- 하나의 원댓글 아래 여러 직접 답글을 작성·조회할 수 있다. +- 원댓글과 직접 답글로 끝나는 기존 2단계 댓글 구조를 유지한다. + +### 3.2 UX 목표 + +- 답글이 0개인 원댓글에는 `답글 작성`이라는 명확한 진입점을 표시한다. +- 버튼을 누르면 기존 답글 영역과 작성 form을 펼친다. +- 기존 답글이 있는 원댓글은 `답글 보기`로 같은 영역을 열고 추가 답글을 작성한다. +- 기존 loading, 오류, 전송 중, 실패 후 초안 보존 동작을 유지한다. + +## 4. Non-Goals + +- 답글의 답글을 포함한 3단계 이상의 댓글 구조 +- 답글 form 상시 노출 +- 새 endpoint, DTO, 상태관리, 컴포넌트 또는 UI dependency 추가 +- 오디오 콘텐츠 댓글 동작이나 payload 정책 변경 +- 기존 답글 수정·삭제·pagination 정책 변경 +- optimistic update 또는 답글 전체 선조회 + +## 5. Target Users and Permissions + +| 사용자 | 목표 | 주요 작업 | 사용 환경 | +|---|---|---|---| +| ADMIN | AI 캐릭터 명의로 커뮤니티 원댓글에 직접 답글 작성 | 답글 영역 열기, 작성, 재시도 | desktop, tablet, mobile | + +- 인증과 ADMIN 권한은 상위 제품 기준을 따른다. +- 활성 AI 캐릭터 workspace에서만 답글 작성 control을 제공한다. +- 비활성 AI 캐릭터 workspace는 기존처럼 조회 전용이다. +- 원댓글 작성자가 팬인지 AI 캐릭터인지와 관계없이 답글을 작성할 수 있다. + +## 6. 핵심 사용자 흐름 + +1. 관리자가 활성 AI 캐릭터의 커뮤니티 게시글 목록에 진입한다. +2. 게시글 Sheet를 열고 답글이 0개인 원댓글에서 `답글 작성`을 누른다. +3. UI가 해당 원댓글의 직접 답글 GET을 실행하고 답글 영역과 작성 form을 표시한다. +4. 관리자가 내용을 입력해 등록한다. +5. 기존 커뮤니티 댓글 POST에 원댓글 ID를 `parentId`로 보내고 성공 후 원댓글·열린 답글 목록을 재조회한다. +6. 관리자는 같은 form으로 동일 원댓글에 추가 직접 답글을 작성할 수 있다. +7. 실패하면 오류를 표시하고 입력 초안을 유지해 재시도할 수 있다. + +## 7. 정보 구조와 라우팅 + +```text +/ai-characters/:characterId/community-posts + └─ 커뮤니티 게시글 Sheet + └─ 댓글 관리 + └─ 원댓글 + └─ 직접 답글 목록 및 작성 form +``` + +- 새 route와 query parameter를 추가하지 않는다. +- 기존 `CommunityPostSheet`의 `CommentThread` 안에서만 동작한다. +- 답글 pagination 상태는 기존 component의 로컬 상태를 사용한다. + +## 8. 기능 요구사항 + +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| `CCR-001` | 확정 | 활성 Community target의 답글 0개 원댓글에 `답글 작성` 버튼을 표시한다. | `replyCount=0`, `canMutate=true`인 Community root에서 버튼을 찾을 수 있다. | contract 불필요, `P1-T1` | +| `CCR-002` | 확정 | `답글 작성`을 누르면 선택한 원댓글의 기존 직접 답글 영역과 작성 form을 연다. | 버튼 클릭 뒤 해당 원댓글 이름과 연결된 답글 region·textarea·등록 버튼이 표시되고 page 0 GET을 한 번 요청한다. | 답글 GET, `P1-T1` | +| `CCR-003` | 확정 | 첫 답글과 후속 직접 답글은 기존 Community 댓글 POST를 사용한다. | body가 trim된 `comment`, 원댓글 ID `parentId`, `isSecret=false`를 포함하고 `languageCode`는 보내지 않는다. | 댓글 POST, `P1-T1` | +| `CCR-004` | 확정 | 하나의 원댓글에는 여러 직접 답글을 추가할 수 있다. | 답글 등록 성공 후 form을 다시 사용할 수 있고 원댓글·현재 답글 page를 재조회해 추가된 답글을 표시한다. | 답글 GET·댓글 POST, `P1-T1`, `P1-GATE` | +| `CCR-005` | 확정 | 댓글 구조는 원댓글과 직접 답글의 2단계로 제한한다. | reply row에는 답글 action이 없고 답글 ID를 `parentId`로 보내는 작성 경로가 없다. | 댓글 POST, `P1-T1` | +| `CCR-006` | 확정 | 기존 권한과 mutation 상태를 유지한다. | `canMutate=false`이면 첫 답글 작성 진입과 form이 없고, pending 중 중복 POST가 없으며 실패 시 초안 유지·성공 시 초기화된다. | `NullSuccess`, `P1-GATE` | + +## 9. 반응형 기능 범위 + +| 기능 | Desktop | Tablet | Mobile | 비고 | +|---|---:|---:|---:|---| +| `답글 작성`·`답글 보기` 진입 | 지원 | 지원 | 지원 | 기존 댓글 action layout 재사용 | +| 여러 직접 답글 조회·작성 | 지원 | 지원 | 지원 | 기존 page size 20과 pagination 재사용 | + +- 상위 제품의 최소 320px, 200% zoom, keyboard-only와 touch target 기준을 유지한다. +- Sheet 내부에서 수평 overflow 없이 form과 action을 사용할 수 있어야 한다. + +## 10. UI/UX Expectations + +### 10.1 디자인과 component 원칙 + +- `CommentThread`, `CommentItem`, `CommentForm`을 재사용한다. +- 오디오와 커뮤니티에 동일한 action label과 펼침 동작을 사용한다. +- 새 component나 dependency를 추가하지 않는다. +- 기존 답글이 있는 원댓글의 `답글 보기` UI는 유지한다. + +### 10.2 화면 상태 + +- 클릭 직후 기존 답글 loading 상태를 표시한다. +- 빈 답글 응답 뒤에도 작성 form을 표시한다. +- 조회 오류는 기존 재시도 UI를 사용한다. +- 작성 중·성공·실패는 기존 Comments mutation 정책을 사용한다. +- 답글 작성 성공 후 form은 빈 값으로 초기화되고 다시 입력할 수 있다. + +### 10.3 접근성 + +- 버튼의 accessible name은 원댓글 내용과 `답글 작성` 또는 `답글 보기`를 조합해 식별 가능해야 한다. +- form의 visible label과 오류 연결, keyboard focus 표시를 유지한다. +- 답글 region은 원댓글 내용과 `답글`을 조합한 accessible name을 유지한다. +- keyboard-only로 Sheet의 원댓글에서 답글 form까지 진입하고 등록할 수 있어야 한다. + +## 11. API 계약 + +### 11.1 공통 규칙 + +- 이 기능은 API를 변경하지 않는다. +- 정확한 request, response와 오류는 [기능 API Contract](./api-contract.md)를 따른다. +- 원본 OpenAPI는 [프로젝트 OpenAPI](../20260725_AI캐릭터관리자웹/api-contract.openapi.json)다. + +### 11.2 Endpoint 추적 + +| 요구사항 | Method | Path | 계약 상태 | 소유 Goal | +|---|---|---|---|---| +| `CCR-002`, `CCR-004` | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies` | 기존 제공·구현됨 | `P1-T1` | +| `CCR-003~005` | POST | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments` | 기존 제공·구현됨 | `P1-T1` | + +### 11.3 외부 제공 대기 계약 + +없음. 필요한 GET·POST, DTO와 mock handler가 이미 제공돼 있다. + +## 12. 보안과 데이터 취급 + +- 기존 Bearer 인증, ADMIN 권한과 `characterId`·`postId` target 격리를 유지한다. +- `parentId`는 현재 Community target에서 응답받은 활성 원댓글 ID만 사용한다. +- 댓글 본문과 인증 정보는 console, 분석 이벤트와 영구 저장소에 기록하지 않는다. +- 401·403은 공통 인증·인가 정책을 따른다. +- 클라이언트 validation은 서버의 target·parent 소유권 검증을 대체하지 않는다. + +## 13. 성능과 품질 요구사항 + +- 답글 action을 누를 때 선택한 원댓글의 답글 page 0만 기존 방식으로 조회한다. +- 답글 page size 20과 기존 pagination을 유지하며 전체 답글을 선조회하지 않는다. +- 새 dependency, 캐시 계층과 optimistic update를 추가하지 않는다. +- Vitest focused test, Comments 회귀, Chromium mock E2E, typecheck와 lint를 통과한다. +- server 404나 network error를 mock으로 자동 전환하지 않는다. + +## 14. 성공 기준 + +### 14.1 기능 수용 기준 + +- [x] 답글 0개인 활성 Community root에서 첫 답글을 작성한다. (`CCR-001~003`) +- [x] 같은 원댓글에 여러 직접 답글을 작성·조회한다. (`CCR-004`) +- [x] reply row와 비활성 workspace의 2단계·권한 경계가 유지된다. (`CCR-005~006`) +- [x] 실패·재시도와 중복 제출 방지가 회귀하지 않는다. (`CCR-006`) + +### 14.2 UI/UX 수용 기준 + +- [x] 버튼·답글 region·form의 accessible name과 label이 연결된다. +- [x] 320px·200% zoom에서 수평 overflow 없이 답글을 작성한다. +- [x] keyboard-only로 답글 form에 진입하고 등록할 수 있다. + +### 14.3 추적성 완료 기준 + +- [x] 모든 확정 요구사항이 API 또는 contract 불필요 판정, `P1-T1`, `P1-GATE`와 연결된다. +- [x] 구현·검증 결과가 [plan-task.md](./plan-task.md)의 Progress에 기록된다. +- [x] 완료된 Phase의 리뷰가 `reviews/` 아래에 기록된다. + +## 15. Open Questions + +없음. + +## 16. 요구사항 추적표 + +| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 | +|---|---|---:|---|---|---| +| `CCR-001~006` | [api-contract.md](./api-contract.md) | 1 | `P1-T1`, `P1-GATE` | `comment-thread.test.tsx`, `comments.spec.ts` | 활성 Community 첫·추가 답글, 비활성·2단계·320px·keyboard 경계 | + +## 17. Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal | +|---|---|---|---|---|---| +| 2026-08-06 | `CCR-DEC-001` | 확정 | 오디오 콘텐츠와 동일한 첫 답글 진입을 활성 커뮤니티 원댓글에도 적용한다. | 사용자 요청 | `CCR-001~003`, `P1-T1` | +| 2026-08-06 | `CCR-DEC-002` | 확정 | 댓글 트리는 원댓글 아래 여러 직접 답글을 허용하되 답글의 답글은 허용하지 않는다. | 사용자 요청의 “1단계 추가, 여러 개” 조건 | `CCR-004~005`, [api-contract.md](./api-contract.md) | +| 2026-08-06 | `CCR-DEC-003` | 확정 | 새 API·컴포넌트 없이 기존 Community GET·POST와 Comments UI를 재사용한다. | OpenAPI와 구현 확인 | `CCR-002~006`, `P1-T1` | + +## 18. 변경 관리 + +- 범위가 바뀌면 이 문서의 Decision Log와 요구사항을 먼저 갱신한다. +- API가 바뀌면 [api-contract.md](./api-contract.md)와 원본 OpenAPI의 제공 버전을 확인한다. +- 구현 범위가 바뀌면 코드보다 [plan-task.md](./plan-task.md)를 먼저 갱신한다. +- 기존 Progress, review와 검증 기록은 삭제하거나 덮어쓰지 않는다. diff --git a/docs/20260806_커뮤니티댓글답글/reviews/phase1-community-comment-replies.md b/docs/20260806_커뮤니티댓글답글/reviews/phase1-community-comment-replies.md new file mode 100644 index 0000000..7e568bd --- /dev/null +++ b/docs/20260806_커뮤니티댓글답글/reviews/phase1-community-comment-replies.md @@ -0,0 +1,170 @@ +# 커뮤니티 댓글 직접 답글 Phase 1 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 1 / `P1-T1`, `P1-R1`, `P1-GATE` | +| 기준 commit 또는 working tree | `e82e209300d2c30843b6a2ef2c9e126ade6bba63` 기반 working tree | +| 리뷰 일자 | 2026-08-06 | +| 리뷰어 | Sisyphus, 독립 goal·품질·보안·컨텍스트·visual QA reviewer | +| 기준 문서 | [prd.md](../prd.md), [api-contract.md](../api-contract.md), [plan-task.md](../plan-task.md) | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- `CCR-001~006`과 Community 첫·추가 직접 답글 journey가 구현됐는지 확인한다. +- API·schema·application mock·dependency 변경 없이 기존 2단계 댓글 경계와 권한을 유지하는지 확인한다. +- TDD, 자동 Gate, 실제 Chromium과 문서 기록이 완료 조건과 일치하는지 판정한다. + +### 포함 범위 + +- 코드: `src/features/comments/components/CommentThread.tsx` +- 테스트: `src/features/comments/tests/comment-thread.test.tsx`, `tests/e2e/comments.spec.ts` +- 문서: `CCR-001~006`, Community 댓글 API Contract, `P1-T1`, `P1-R1`, `P1-GATE` +- 수동 검증: Chromium mock mode, keyboard-only, 1280px, 320px, 200% zoom, CJK·수평 overflow + +### 제외 범위 + +- 실제 개발 API integration, 새 endpoint·schema·mock store, 답글 수정·삭제·pagination 정책 변경 +- 3단계 댓글, optimistic update, form 상시 노출 + +## 3. 판정 기준 + +### 심각도 + +| 심각도 | 기준 | +|---|---| +| Blocker | 보안·데이터 손실 위험, 핵심 journey 불능, 완료 판정 무효 | +| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 | +| Medium | 제한 조건의 기능·접근성·복구 문제 | +| Low | 비핵심 유지보수성·문서 정합성 문제 | + +### 상태 + +| 상태 | 의미 | 후속 처리 | +|---|---|---| +| 후보 | 근거를 발견했지만 판정 전 | 재현 후 상태 변경 | +| 확정 | 코드·test·문서로 문제 확인 | 회귀 Task 전환 | +| 오탐 | 실제 실행 경로나 요구사항 위반이 아님 | 판정 근거를 보존하고 종료 | +| 보류 | 외부 계약·환경·제품 결정 필요 | 담당·재개 조건 기록 | +| 수정 완료 | 수정과 관련 검증 완료 | 검증 결과 누적 | + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: `CCR-001~006` +- API Contract: 직접 답글 GET, Community 댓글 POST, 2단계 불변식 +- 계획: `P1-T1`, `P1-R1`, `P1-GATE` +- 코드: `CommentThread.tsx`의 `replyActionLabel`, `toggleReplies()`, `createReply()` +- 테스트: `CommentThread creates a first Community reply...`, `Community sheet comments keep two-level controls usable at 320px` + +### 실행 환경 + +```text +OS: macOS +Node: v24.12.0 +npm: 11.7.0 +Browser/viewport: Playwright Chromium, 1280x900, 320x640, CSS zoom 200% +환경 변수: VITE_API_MODE=mock +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx` | 성공 | `8/8` | +| `npm run test:run -- src/features/comments` | 성공 | `15/15` | +| `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium` | 성공 | `3/3`; 2102 빈 reply, 첫·두 답글, payload, 2단계 경계 | +| `npm run typecheck` | 성공 | exit 0 | +| `npm run lint` | 성공 | exit 0 | +| `npm run build:dev` | 성공 | Vite build exit 0; 기존 500kB chunk warning만 발생 | +| `git diff --check` | 성공 | 출력 없음 | +| 실제 Chromium keyboard journey | 성공 | 답글 action·textarea keyboard 진입, 첫·두 답글 표시, input 초기화, 중첩 action 0건 | +| 1280px·320px·200% visual QA | 성공 | 수평 overflow 없음, CJK clipping·고아줄 없음, 독립 visual reviewer PASS | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `CCR-REV-P1-001` | High | 오탐 | E2E route fixture가 root 2102 답글을 잘못 분류한다 | `P1-R1` | 없음 | + +확정 발견 사항 없음. + +## 6. 발견 사항 상세 + +### CCR-REV-P1-001 — E2E route fixture root 분류 후보 + +- **심각도:** High +- **상태:** 오탐 +- **관련 요구사항:** `CCR-002`, `CCR-004~005` +- **관련 계약:** Community 직접 답글 GET·POST, 2단계 불변식 +- **소유 Task:** `P1-R1` + +**관찰 내용** + +`tests/e2e/comments-test-support.ts`는 단일 `replyRootId`만 replies로 분류하지만, +필수 mock E2E에서는 이 Playwright route fixture가 응답을 소유하지 않는다. + +**근거** + +- `playwright.config.ts`는 mock E2E를 `VITE_API_MODE=mock`으로 실행한다. +- 앱은 렌더 전에 `src/shared/mocks/browser.ts`의 `setupWorker(...)`를 시작한다. +- Browser MSW handler·store는 `commentId`와 `parentId`로 root 2102 답글을 분리한다. +- 기존 E2E route fixture를 변경하지 않은 상태에서 2102 초기 reply region의 root 댓글 0건, 첫·두 답글 각 1건, 중첩 action 0건과 Chromium `3/3`을 반복 확인했다. +- 별도 브라우저 probe에서 `page.route` 호출 0회와 Community mock 요청 9회를 관찰했다. + +**재현 또는 검증 절차** + +1. `VITE_API_MODE=mock`으로 `comments.spec.ts` Chromium을 실행한다. +2. root 2102의 답글 영역을 열고 다른 root 댓글이 없음을 확인한다. +3. 같은 root에 첫·두 번째 답글을 등록하고 Sheet를 다시 연다. +4. 두 답글이 region에 각 1건 표시되고 중첩 답글 action이 없음을 확인한다. + +**영향** + +필수 mock E2E와 제품 동작에는 영향이 없다. Server-mode 전용 test route helper의 +일반화는 이번 기능 범위와 실행 경로 밖이다. + +**권장 조치** + +없음. 실행되지 않는 fixture를 speculative하게 변경하지 않는다. + +**판정 기록** + +- 2026-08-06 — 코드 형태만 근거로 확정 후보로 분류했다. +- 2026-08-06 — mock 요청 소유권, 기존 fixture 상태의 E2E, 실제 브라우저를 대조해 오탐으로 정정했다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. `CCR-REV-P1-001`은 `P1-R1`에서 오탐으로 판정됐다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | 요구사항·계약·코드·test·실제 Chromium·visual QA 확인 | +| 후보 항목 판정 완료 | 충족 | `CCR-REV-P1-001` 오탐 판정 | +| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 | +| 검증 명령과 결과 기록 | 충족 | 자동·수동 검증 표와 `plan-task.md` Progress 기록 | + +**최종 결론:** 확정 발견 사항 없음 + +**남은 항목:** 실제 개발 API integration은 이번 mock 기능 Gate 범위 밖이다. + +## 9. 수정 후 검증 기록 + +### 1차 리뷰 후보 검증 — 2026-08-06 + +- 무엇을: `CCR-REV-P1-001`의 실제 mock E2E 영향 여부를 검증했다. +- 왜: 실행되지 않는 route fixture를 수정하면 범위를 불필요하게 확장할 수 있다. +- 어떻게: + - 기존 fixture 상태의 Chromium E2E — `3/3` 성공 + - Comments Vitest — `15/15` 성공 + - typecheck·lint·`git diff --check` — exit 0 + - 실제 Chromium·visual QA — 첫·추가 답글, keyboard, 1280px·320px·200% PASS +- 남은 항목: 없음 diff --git a/src/features/comments/components/CommentThread.tsx b/src/features/comments/components/CommentThread.tsx index 0ae01e1..c00cb89 100644 --- a/src/features/comments/components/CommentThread.tsx +++ b/src/features/comments/components/CommentThread.tsx @@ -167,7 +167,7 @@ export function CommentThread({ apiClient, canMutate = true, target }: { readonl