feat(ai-character): 수정 요청 변경 필드만 전송
This commit is contained in:
213
docs/20260806_수정요청변경필드만전송/api-contract.md
Normal file
213
docs/20260806_수정요청변경필드만전송/api-contract.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 수정 요청 변경 필드 전송 API Contract
|
||||
|
||||
## 1. 공통 계약
|
||||
|
||||
이 문서는 기존 [AI 캐릭터 관리자 OpenAPI](../20260725_AI캐릭터관리자웹/api-contract.openapi.json)의 endpoint·DTO를 바꾸지 않고, 프론트엔드가 수정 request를 구성하는 규칙을 구체화한다. 이 문서와 정식 OpenAPI가 충돌하면 정식 OpenAPI의 field type·nullable·response·error 계약을 우선하고 이 문서는 payload 선택 규칙만 소유한다.
|
||||
|
||||
### 1.1 유지되는 항목
|
||||
|
||||
- 기존 `PUT` method와 path를 유지한다.
|
||||
- 기존 bearer 인증, `Accept-Language`, 성공 envelope, 오류 status·key를 유지한다.
|
||||
- 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈는 `multipart/form-data`를 유지한다.
|
||||
- multipart JSON part 이름은 `request`, MIME은 `application/json`이다.
|
||||
- FanTalk 답글은 `application/json` body를 유지한다.
|
||||
|
||||
### 1.2 변경 판정
|
||||
|
||||
1. 상세·목록 응답으로 form을 초기화할 때 수정 기준값을 보존한다.
|
||||
2. 현재 form 값과 기준값에 같은 기존 직렬화 규칙을 적용한다.
|
||||
3. 문자열 trim, 빈 optional 값의 `null` 변환, 배열·객체 배열 변환 후 필드별 값을 비교한다.
|
||||
4. 값이 다른 field만 request object에 포함한다.
|
||||
5. 사용자가 값을 바꾼 뒤 기준값으로 되돌리면 해당 field를 생략한다.
|
||||
|
||||
비교 대상은 각 기능의 update DTO field다. route ID, 조회 전용 field, 서버 계산값과 수정 화면에 없는 field를 request에 복사하지 않는다.
|
||||
|
||||
### 1.3 생략, `null`, falsy 값
|
||||
|
||||
| 표현 | 의미 | 예시 |
|
||||
|---|---|---|
|
||||
| key 생략 | 미변경 | `{ "title": "새 제목" }`에는 `detail` 변경 없음 |
|
||||
| `null` | 해당 DTO가 허용하는 기존 값 삭제 | `{ "writer": null }` |
|
||||
| `false` | boolean 값을 false로 변경 | `{ "isAdult": false }` |
|
||||
| `0` | 숫자 값을 0으로 변경 | `{ "price": 0 }` |
|
||||
| 배열·객체 배열 | 해당 필드 전체의 새 값 | `{ "publishedDaysOfWeek": ["RANDOM"] }` |
|
||||
|
||||
`false`, `0`, 빈값 삭제용 `null`은 falsy 값이라는 이유로 생략하지 않는다.
|
||||
|
||||
### 1.4 변경 없음
|
||||
|
||||
- 변경 field와 교체 file이 모두 0개면 저장 control은 native `disabled` 상태다.
|
||||
- disabled 상태에서는 API helper를 호출하지 않는다.
|
||||
- 빈 JSON 또는 빈 multipart mutation을 전송하지 않는다.
|
||||
|
||||
### 1.5 파일 part
|
||||
|
||||
| 상태 | 파일 part | `request` JSON |
|
||||
|---|---|---|
|
||||
| 텍스트 field만 변경 | 생략 | 변경 field만 포함 |
|
||||
| 파일과 field 변경 | 새 파일 1개 | 변경 field만 포함 |
|
||||
| 파일만 변경 | 새 파일 1개 | `{}` |
|
||||
| 변경 없음 | 요청 자체 없음 | 요청 자체 없음 |
|
||||
|
||||
기존 파일을 선택하지 않으면 서버의 현재 파일을 유지한다. 파일 삭제 기능은 이 계약에 추가하지 않는다.
|
||||
|
||||
## 2. 기능별 계약
|
||||
|
||||
### 2.1 AI 캐릭터
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}`
|
||||
|
||||
- Content-Type: `multipart/form-data`
|
||||
- optional file part: `image`
|
||||
- JSON part: `request`
|
||||
- 비교 가능 field: `name`, `systemPrompt`, `description`, `age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `originalTitle`, `originalLink`, `originalWorkId`, `characterType`, `tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`
|
||||
- 금지 field: `region`, 일반 수정의 `isActive`
|
||||
- 기존 예외: 원작 미선택·선택 해제의 `originalWorkId`는 현재 serializer 계약대로 key를 생략하며, 원작 연결 해제 기능은 추가하지 않는다.
|
||||
|
||||
이름만 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "루나 수정"
|
||||
}
|
||||
```
|
||||
|
||||
태그를 모두 삭제:
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": null
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 오디오 콘텐츠
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
|
||||
|
||||
- Content-Type: `multipart/form-data`
|
||||
- optional file part: `coverImage`
|
||||
- JSON part: `request`
|
||||
- 수정 화면 비교 field: `title`, `detail`, `tags`, `price`
|
||||
- 일반 수정 금지 field: `isActive`
|
||||
- 화면에 없는 `isAdult`, `isPointAvailable`, `isCommentAvailable`은 상세 응답에서 복사하지 않는다.
|
||||
|
||||
상세 설명만 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "수정한 상세 설명"
|
||||
}
|
||||
```
|
||||
|
||||
가격만 무료로 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"price": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 커뮤니티 게시글
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`
|
||||
|
||||
- Content-Type: `multipart/form-data`
|
||||
- optional file part: `postImage`
|
||||
- JSON part: `request`
|
||||
- 수정 저장 비교 field: `content`, `isCommentAvailable`, `isAdult`
|
||||
- 수정 저장 금지 field: `isFixed`, `isActive`
|
||||
- `isFixed` 전환과 soft delete는 기존 전용 action payload를 유지한다.
|
||||
|
||||
내용만 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "수정한 게시글 내용"
|
||||
}
|
||||
```
|
||||
|
||||
댓글 허용만 끄기:
|
||||
|
||||
```json
|
||||
{
|
||||
"isCommentAvailable": false
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 시리즈
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
|
||||
|
||||
- Content-Type: `multipart/form-data`
|
||||
- optional file part: `image`
|
||||
- JSON part: `request`
|
||||
- 비교 field: `title`, `introduction`, `publishedDaysOfWeek`, `genreId`, `isAdult`, `state`, `writer`, `studio`
|
||||
- 일반 수정 금지 field: `isActive`, create-only `keyword`
|
||||
|
||||
제목만 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "달빛 상담 시리즈 수정"
|
||||
}
|
||||
```
|
||||
|
||||
작가를 삭제:
|
||||
|
||||
```json
|
||||
{
|
||||
"writer": null
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 FanTalk 답글
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`
|
||||
|
||||
- Content-Type: `application/json`
|
||||
- 비교 field: `content`
|
||||
- 일반 답글 수정 금지 field: `isActive`
|
||||
|
||||
답글 변경:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "수정한 답글입니다."
|
||||
}
|
||||
```
|
||||
|
||||
`content`가 기존 답글과 같으면 수정 button은 disabled이고 request를 보내지 않는다.
|
||||
|
||||
## 3. 응답과 오류
|
||||
|
||||
응답과 오류 계약은 변경하지 않는다.
|
||||
|
||||
| 기능 | 성공 data |
|
||||
|---|---|
|
||||
| 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈 | 기존 `null` success data |
|
||||
| FanTalk 답글 | 기존 `FanTalkListItem` update response |
|
||||
|
||||
- validation, 401, 403, 404, 406, 415, 500 처리는 정식 OpenAPI와 기존 공통 API client를 따른다.
|
||||
- 부분 request 도입을 이유로 새로운 status, `errorProperty` 또는 자동 retry를 만들지 않는다.
|
||||
|
||||
## 4. Contract 검증 예시
|
||||
|
||||
| 시나리오 | 필수 assertion |
|
||||
|---|---|
|
||||
| title만 변경 | request key가 `title` 하나다. |
|
||||
| detail만 변경 | request key가 `detail` 하나다. |
|
||||
| boolean을 false로 변경 | 해당 key와 `false`가 존재한다. |
|
||||
| nullable field 삭제 | 해당 key와 `null`이 존재한다. |
|
||||
| 변경 후 원복 | 저장 disabled, mutation 0건이다. |
|
||||
| 파일만 변경 | 파일 part 1개, request `{}`다. |
|
||||
| 파일 미변경 | 파일 part가 없다. |
|
||||
|
||||
## 5. 범위 밖 mutation 회귀 계약
|
||||
|
||||
다음 기존 요청은 payload 최적화 대상이 아니며 현재 contract를 유지한다.
|
||||
|
||||
- 캐릭터·오디오 콘텐츠·시리즈 비활성화: `{ "isActive": false }`
|
||||
- 커뮤니티 게시글 고정 전환: `{ "isFixed": boolean }`
|
||||
- 커뮤니티 게시글 soft delete: 기존 `{ "isActive": false, "isFixed": false }`
|
||||
- 시리즈 순서 변경: `{ "ids": number[] }`
|
||||
- FanTalk 원글 삭제: body 없는 `DELETE`
|
||||
619
docs/20260806_수정요청변경필드만전송/plan-task.md
Normal file
619
docs/20260806_수정요청변경필드만전송/plan-task.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# 수정 요청 변경 필드 전송 구현 계획
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 구현·검증 완료 |
|
||||
| 작성일 | 2026-08-06 |
|
||||
| 요구사항 기준 | [prd.md](./prd.md) |
|
||||
| API 기준 | [api-contract.md](./api-contract.md) |
|
||||
| 현재 Phase | Phase 1 변경 필드 전송 통합 |
|
||||
| 현재 활성 Goal | 없음 |
|
||||
|
||||
## 목표
|
||||
|
||||
AI 캐릭터, 오디오 콘텐츠, 커뮤니티 게시글, 시리즈, FanTalk 답글 수정 시 최종 변경 필드와 교체 파일만 전송하고, 변경이 없으면 저장과 API 요청을 차단한다.
|
||||
|
||||
## 현재 상태
|
||||
|
||||
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||
|---:|---|---:|---|---|
|
||||
| 1 | 완료 | `7/7` | 없음 | 없음 |
|
||||
|
||||
- 동시에 하나의 미완료 goal만 운용한다.
|
||||
- 완료된 Task와 검증 기록은 삭제하거나 되돌리지 않는다. 후속 수정은 회귀 수정 Task와 새 goal ID를 추가한다.
|
||||
- 사용자가 token budget을 지정하지 않았으므로 goal에 token budget을 설정하지 않는다.
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
|
||||
- AI 캐릭터, 오디오 콘텐츠, 커뮤니티 게시글, 시리즈, FanTalk 답글의 일반 수정 payload
|
||||
- 현재 form 값과 조회 기준값의 field별 비교, nullable 삭제와 미변경 생략 구분
|
||||
- 이미지·cover·post image만 변경한 multipart의 빈 `request: {}`
|
||||
- 변경 없음·변경 후 원복 상태의 native disabled 저장 control과 mutation 0건
|
||||
- 기능별 contract·component test와 mock Chromium E2E 회귀, typecheck·lint·build
|
||||
|
||||
### 제외
|
||||
|
||||
- 생성 payload, backend DTO와 HTTP method 변경
|
||||
- 캐릭터·오디오 콘텐츠·시리즈 비활성화 payload
|
||||
- 커뮤니티 게시글 고정 전환·soft delete, 시리즈 순서 변경, FanTalk 원글 삭제
|
||||
- 수정 화면 신규 field, image 삭제, 원작 연결 해제 기능
|
||||
- 새 dependency, 전역 form store, 범용 deep-diff abstraction
|
||||
|
||||
## 기술적 제약
|
||||
|
||||
- 기술 스택: React 19, TypeScript 6, Zod 4, Vitest 4, Testing Library, Playwright 1.61
|
||||
- 코드 스타일: TypeScript `strict`를 유지하고 `as any`, `@ts-ignore`, `@ts-expect-error`로 오류를 숨기지 않는다.
|
||||
- 아키텍처: API helper의 transport·schema 책임은 유지하고, 변경 판정은 각 feature의 form serializer 또는 component에 둔다.
|
||||
- 데이터: 현재 form과 기준 DTO에 같은 trim·nullable·array serialization을 적용하고 `false`, `0`, `null`을 유효한 변경값으로 보존한다.
|
||||
- 파일: multipart `request` part는 항상 유지한다. 파일만 변경하면 `{}`, 파일 미변경이면 file part를 생략한다.
|
||||
- UX: payload와 저장 disabled가 서로 다른 판정을 사용하지 않도록 같은 request 결과에서 `hasChanges`를 계산한다.
|
||||
- 보안: 인증·권한·resource ownership·민감정보 비기록 정책을 변경하지 않는다.
|
||||
- 호환성: 기존 기능별 desktop·tablet·mobile capability와 지원 browser를 유지한다.
|
||||
- 의존성: 새 package를 추가하지 않고 언어·플랫폼 기능과 기존 Zod schema를 사용한다.
|
||||
- 계약: 제공되지 않은 field, endpoint, response, 오류 status/key를 만들지 않는다.
|
||||
- mock: 기존 explicit mock mode만 사용하고 production 자동 fallback을 추가하지 않는다.
|
||||
- 구현: 각 Task는 RED → GREEN → REFACTOR 순서로 진행하고 실제 결과를 Progress에 누적한다.
|
||||
|
||||
## Phase 1. 변경 필드 전송 통합
|
||||
|
||||
**Phase 결과:** 다섯 수정 기능에서 단일 field 변경, nullable 삭제, file-only 변경과 무변경 상태가 동일한 계약으로 동작한다.
|
||||
|
||||
**선행조건:** [prd.md](./prd.md)의 `DIFF-001~005`와 [api-contract.md](./api-contract.md) 확정.
|
||||
|
||||
**Phase 완료 조건:** `P1-T1`~`P1-T5`와 `P1-GATE` 완료, 검증 결과와 실제 request 증거를 Progress에 누적.
|
||||
|
||||
### 구현 항목
|
||||
|
||||
#### Task 1.1 AI 캐릭터 변경 field 직렬화
|
||||
|
||||
**Goal 실행 `P1-T1`:** 캐릭터 수정 request가 변경된 profile·optional·repeated field만 포함하고 무변경 저장을 차단한다.
|
||||
|
||||
- **시작 조건:** `CHAR-001`, `DIFF-001~005`, `DATA-001~002`, API Contract §1·§2.1 확정.
|
||||
- **완료 증거:** direct field·nullable·repeated·image-only·무변경 test, focused 회귀, Progress 기록.
|
||||
- **범위 밖:** create form, region 수정, `isActive`, 원작 연결 해제 backend 의미 변경.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/characters/components/character-optional-field-serialization.ts`
|
||||
- Modify: `src/features/characters/pages/CharacterEditPage.tsx`
|
||||
- Test: `src/features/characters/tests/CharacterEditPage.test.tsx`
|
||||
- Test: `src/features/characters/tests/character-api.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `CharacterDetail`, `CharacterOptionalFieldsValue`, `UpdateCharacterParams["request"]`, 기존 `serializeCharacterRequest`.
|
||||
- Produces: 현재 optional field와 초기 optional field를 비교해 변경 key만 반환하는 `toUpdateCharacterOptionalRequest` 계약, direct field와 합쳐진 변경 전용 request, 동일 request 기반 `hasChanges`.
|
||||
- 보존: 원작 미선택의 `originalWorkId` key 생략, file part 이름 `image`, `region`·일반 수정 `isActive` 제외.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — `CharacterEditPage.test.tsx`에 name 단일 변경, optional field `null` 삭제, 반복 field 변경, 최초·원복 disabled, image-only `{}` 시나리오를 추가하고 focused 명령에서 현재 전체 field payload 또는 enabled 저장 때문에 실패하는지 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — optional serializer가 초기값과 달라진 key만 만들고 page가 direct field와 file을 같은 방식으로 판정하도록 최소 수정해 focused 명령을 통과시킨다.
|
||||
- [x] **REFACTOR: 정리/회귀 확인** — payload 계산 중복만 제거하고 create serializer·API multipart helper는 유지한 채 Character 전체 회귀를 실행한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts`; `npm run test:run -- src/features/characters`; `npm run typecheck`; `npm run lint`.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 신규 5개 변경 감지 test를 포함한 실행 test 전부 통과, name 단일 변경 key 1개, 무변경 `PUT` 0건, type·lint 오류 0건.
|
||||
- **수동 확인:** 1280px에서 캐릭터 이름만 수정한 request part가 `{name}`이고, 원복 시 저장 disabled, image만 교체하면 `image`와 `{}`만 전송된다.
|
||||
|
||||
- [x] TDD 단계와 검증 기준의 실제 결과를 Progress에 기록한다.
|
||||
|
||||
#### Task 1.2 오디오 콘텐츠 변경 field 직렬화
|
||||
|
||||
**Goal 실행 `P1-T2`:** 오디오 콘텐츠 수정 request가 변경된 title·detail·tags·price와 새 cover image만 포함한다.
|
||||
|
||||
- **시작 조건:** `P1-T1` 완료, `AUDIO-001`, `DIFF-001~005`, API Contract §1·§2.2 확정.
|
||||
- **완료 증거:** 단일 field·price 0·cover-only·무변경 test, 기존 비활성화 회귀, Progress 기록.
|
||||
- **범위 밖:** create options, audio 원본 교체, theme·release date 수정, 비활성화 payload.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/audio-contents/components/audio-content-form-helpers.ts`
|
||||
- Modify: `src/features/audio-contents/components/AudioContentForm.tsx`
|
||||
- Test: `src/features/audio-contents/tests/audio-form-update.test.tsx`
|
||||
- Test: `src/features/audio-contents/tests/audio-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `AudioContentDetail`, `AudioContentUpdateRequest`, 기존 `toUpdateRequest`와 `createAudioContentUpdateBody`.
|
||||
- Produces: 직렬화된 현재 값과 `audio` 기준값을 비교해 변경 key만 반환하는 `toUpdateRequest`, 같은 request와 `coverImage` 기반 `hasChanges`.
|
||||
- 보존: file part `coverImage`, `request` part, 일반 update `isActive` 금지, price `0` 허용.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — detail 단일 변경, price `0`, cover-only `{}`, 최초·원복 disabled와 화면에 없는 boolean 미전송 test를 작성하고 현재 전체 payload 때문에 실패하는지 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — 기존 `toUpdateRequest`가 실제 수정 화면 field만 비교해 반환하고 edit button이 같은 결과로 disabled되도록 수정한다.
|
||||
- [x] **REFACTOR: 정리/회귀 확인** — create path와 비활성화 schema를 건드리지 않고 update serializer의 중복만 정리한 뒤 Audio 전체 회귀를 실행한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form-update.test.tsx src/features/audio-contents/tests/audio-contract.test.ts`; `npm run test:run -- src/features/audio-contents`; `npm run typecheck`; `npm run lint`.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 신규 5개 변경 감지 test를 포함한 실행 test 전부 통과, detail 단일 변경 key 1개, cover-only request `{}`, 무변경 `PUT` 0건.
|
||||
- **수동 확인:** 1280px에서 detail만 수정하고 multipart `request`에 `detail`만 있는지, 기존 cover 미선택 시 `coverImage`가 없는지 확인한다.
|
||||
|
||||
- [x] TDD 단계와 검증 기준의 실제 결과를 Progress에 기록한다.
|
||||
|
||||
#### Task 1.3 커뮤니티 게시글 변경 field 직렬화
|
||||
|
||||
**Goal 실행 `P1-T3`:** 커뮤니티 게시글 수정 저장이 변경된 content·comment permission·adult flag와 새 image만 포함한다.
|
||||
|
||||
- **시작 조건:** `P1-T2` 완료, `COMM-001`, `DIFF-001~005`, API Contract §1·§2.3 확정.
|
||||
- **완료 증거:** 단일 field·boolean false·image-only·무변경 test, 고정·soft delete 회귀, Progress 기록.
|
||||
- **범위 밖:** create form, audio file, `isFixed` 전환, soft delete 계약.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/community-posts/components/CommunityPostSheet.tsx`
|
||||
- Test: `src/features/community-posts/tests/community-sheet.test.tsx`
|
||||
- Test: `src/features/community-posts/tests/community-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `CommunityPostListItem`, `CommunityPostUpdateRequest`, `updateCommunityPost`.
|
||||
- Produces: Sheet의 현재 수정 field와 `post` 기준값을 비교한 request, 같은 request와 `postImage` 기반 `hasChanges`.
|
||||
- 보존: `toggleFixed`의 `{isFixed}`, `softDeleteCommunityPost`의 기존 request, file part `postImage`.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — content 단일 변경, boolean true→false, image-only `{}`, 최초·원복 disabled test를 작성하고 현재 `isFixed`와 미변경 field가 함께 전송되는 실패를 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — Sheet의 일반 수정 저장 request만 field별 비교하고 저장 button이 같은 `hasChanges`를 사용하도록 수정한다.
|
||||
- [x] **REFACTOR: 정리/회귀 확인** — 고정·비활성화 action의 전용 함수와 pending guard를 유지하고 일반 수정 계산만 읽기 쉽게 정리한 뒤 Community 전체 회귀를 실행한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts`; `npm run test:run -- src/features/community-posts`; `npm run typecheck`; `npm run lint`.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 신규 4개 변경 감지 test를 포함한 실행 test 전부 통과, content 단일 변경 key 1개, 일반 수정 `isFixed` 0건, 무변경 `PUT` 0건.
|
||||
- **수동 확인:** 지원 viewport에서 Sheet를 열어 content만 수정했을 때 request에 content만 있고 고정 button과 비활성화 button 동작이 유지되는지 확인한다.
|
||||
|
||||
- [x] TDD 단계와 검증 기준의 실제 결과를 Progress에 기록한다.
|
||||
|
||||
#### Task 1.4 시리즈 변경 field 직렬화
|
||||
|
||||
**Goal 실행 `P1-T4`:** 시리즈 수정 request가 변경된 기본·enum·nullable field와 새 image만 포함한다.
|
||||
|
||||
- **시작 조건:** `P1-T3` 완료, `SERIES-001`, `DIFF-001~005`, API Contract §1·§2.4 확정.
|
||||
- **완료 증거:** title 단일 변경, nullable 삭제, 배열·enum 변경, image-only·무변경 test, 비활성화 회귀, Progress 기록.
|
||||
- **범위 밖:** create-only keyword, 연결 콘텐츠, 순서 변경, 비활성화 payload.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/series/components/SeriesForm.tsx`
|
||||
- Test: `src/features/series/tests/series-form.test.tsx`
|
||||
- Test: `src/features/series/tests/series-contract.test.ts`
|
||||
- Test: `src/features/series/tests/series-update-invariants.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `SeriesListItem`, `SeriesUpdateRequest`, 기존 `textOrNull`, `editedState`, `updateSeries`.
|
||||
- Produces: 모든 edit field에 `editedState`와 같은 원본 비교를 적용한 update request, 같은 request와 `image` 기반 `hasChanges`.
|
||||
- 보존: array field 변경 시 전체 새 배열, writer·studio 삭제 시 `null`, 일반 update의 `isActive` 금지.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — title 단일 변경, writer 삭제 `null`, published days 변경, image-only `{}`, 최초·원복 disabled test를 작성하고 state 외 미변경 field가 전송되는 실패를 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — 기존 `editedState` 패턴을 edit field 전체에 적용해 request를 만들고 form disabled·dirty 판정이 같은 결과를 사용하게 한다.
|
||||
- [x] **REFACTOR: 정리/회귀 확인** — field 의미를 감추는 범용 abstraction 없이 local serializer 하나로 중복만 줄이고 Series 전체 회귀를 실행한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/series/tests/series-form.test.tsx src/features/series/tests/series-contract.test.ts src/features/series/tests/series-update-invariants.test.ts`; `npm run test:run -- src/features/series`; `npm run typecheck`; `npm run lint`.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 신규 5개 변경 감지 test를 포함한 실행 test 전부 통과, title 단일 변경 key 1개, nullable 삭제 `null` 보존, 무변경 `PUT` 0건.
|
||||
- **수동 확인:** 1280px에서 title만 수정, writer 삭제, image-only 교체를 각각 실행해 request key와 file part를 확인한다.
|
||||
|
||||
- [x] TDD 단계와 검증 기준의 실제 결과를 Progress에 기록한다.
|
||||
|
||||
#### Task 1.5 FanTalk 답글 무변경 수정 차단
|
||||
|
||||
**Goal 실행 `P1-T5`:** FanTalk 답글은 기존 답글과 다른 content가 있을 때만 수정 request를 전송한다.
|
||||
|
||||
- **시작 조건:** `P1-T4` 완료, `FANTALK-001`, `DIFF-001`, `DIFF-004`, API Contract §1.4·§2.5 확정.
|
||||
- **완료 증거:** 최초 disabled, 변경 enabled·`{content}`, 원복 disabled·`PUT` 0건 test, FanTalk 회귀, Progress 기록.
|
||||
- **범위 밖:** 답글 생성, FanTalk 원글 삭제, `isActive`, 답글 trim 정책 변경.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/fan-talks/components/FanTalkReplySheet.tsx`
|
||||
- Modify: `src/features/fan-talks/components/FanTalkReplyForm.tsx`
|
||||
- Test: `src/features/fan-talks/tests/fan-talk-reply.test.tsx`
|
||||
- Test: `src/features/fan-talks/tests/fan-talk-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `FanTalkCreatorReply.content`, `FanTalkReplyForm`, `updateFanTalkReply`.
|
||||
- Produces: edit mode에서 `content !== existingReply.content`를 나타내는 submit disabled prop, 변경 시 기존 `{content}` request.
|
||||
- 보존: create mode의 빈값 validation, pending single-flight, update response와 원글 DELETE.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — 기존 답글 open 직후 수정 button disabled, content 변경 후 enabled·PUT 1건, 원복 후 disabled·PUT 0건 test를 작성하고 현재 항상 enabled인 실패를 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — Sheet가 edit 변경 여부를 계산해 Form submit button에 전달하고 기존 update body는 유지한다.
|
||||
- [x] **REFACTOR: 정리/회귀 확인** — create와 edit의 disabled 이유를 명확히 유지하고 새 serializer나 API helper 없이 FanTalk 전체 회귀를 실행한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/fan-talks/tests/fan-talk-reply.test.tsx src/features/fan-talks/tests/fan-talk-contract.test.ts`; `npm run test:run -- src/features/fan-talks`; `npm run typecheck`; `npm run lint`.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 신규 3개 변경 감지 test를 포함한 실행 test 전부 통과, 변경 update body `{content}` key 1개, 무변경·원복 `PUT` 0건.
|
||||
- **수동 확인:** FanTalk 답글 Sheet를 열면 수정 button이 disabled이고, 내용 변경 시 enabled, 원복 시 다시 disabled인지 확인한다.
|
||||
|
||||
- [x] TDD 단계와 검증 기준의 실제 결과를 Progress에 기록한다.
|
||||
|
||||
#### Task R1.1 커뮤니티 content 직렬화 비교 일치
|
||||
|
||||
**Goal 실행 `P1-R1`:** `REV-P1-001`을 수정해 커뮤니티 게시글의 기존 원문 직렬화와 변경 판정을 일치시키고 공백 변경 누락을 방지한다.
|
||||
|
||||
- **시작 조건:** `REV-P1-001` 확정, `P1-T3`·`P1-GATE` 완료, `DIFF-001~004`, API Contract §1.2·§2.3 확정.
|
||||
- **완료 증거:** 공백 변경 실패 재현 test, 최소 수정 후 focused·Community 전체·Phase Gate 통과, Progress와 review 수정 검증 기록.
|
||||
- **범위 밖:** 커뮤니티 생성 payload, content trim 정책 신설, 고정 전환, soft delete, 다른 기능 serializer 변경.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/features/community-posts/components/CommunityPostSheet.tsx`
|
||||
- Test: `src/features/community-posts/tests/community-sheet.test.tsx`
|
||||
- Modify: `docs/20260806_수정요청변경필드만전송/plan-task.md`
|
||||
- Add: `docs/20260806_수정요청변경필드만전송/reviews/phase1-changed-field-requests.md`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: 기존 Community update의 raw `content`, `CommunityPostListItem.content`, `CommunityPostUpdateRequest`.
|
||||
- Produces: 현재 `content`와 기준 `post.content`에 동일한 원문 직렬화 규칙을 적용한 변경 판정.
|
||||
- 보존: 전송하는 `content` 원문, file-only `{}`, `isFixed`·soft delete 전용 request.
|
||||
|
||||
**TDD 절차:**
|
||||
|
||||
- [x] **RED: 실패 테스트 작성/실패 확인** — 기존 content 앞뒤에 공백을 추가하면 저장이 활성화되고 원문 `{content}`가 전송돼야 하는 test가 현재 trim 비교로 실패하는지 확인한다.
|
||||
- [x] **GREEN: 최소 구현/통과 확인** — `content` 비교의 `trim()`만 제거해 기존 update 직렬화와 일치시키고 focused test를 통과시킨다.
|
||||
- [x] **REFACTOR: 회귀 확인** — 별도 abstraction 없이 Community 전체와 Phase Gate를 실행한다.
|
||||
- [x] review 상태와 Progress에 실제 명령·결과를 누적한다.
|
||||
|
||||
**검증 기준:**
|
||||
|
||||
- **실행 명령:** `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts`; `npm run test:run -- src/features/community-posts`; Phase 1 Gate 전체 명령.
|
||||
- **기대 결과:** 모든 명령 `exit 0`, 공백 변경 request에 raw `content`만 존재, 기존 227개 회귀와 정적 Gate 오류 0건.
|
||||
- **수동 확인:** 자동 component test로 동일 payload와 disabled 상태를 검증하며 별도 Network 확인은 대체 사유를 review에 기록한다.
|
||||
|
||||
#### Task R1.2 PRD 성공 기준 상태 동기화
|
||||
|
||||
**Goal 실행 `P1-R2`:** `REV-P1-002`를 수정해 검증 완료된 PRD 성공 기준과 Phase 1 완료 기록을 일치시킨다.
|
||||
|
||||
- **시작 조건:** `REV-P1-002` 확정, `P1-R1`과 Phase Gate 완료.
|
||||
- **완료 증거:** PRD §14의 검증 완료 항목 체크, plan·review 기록, 문서 정적 검사.
|
||||
- **범위 밖:** 요구사항·API Contract 의미 변경, 검증하지 않은 항목 완료 처리, 코드 변경.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/20260806_수정요청변경필드만전송/prd.md`
|
||||
- Modify: `docs/20260806_수정요청변경필드만전송/plan-task.md`
|
||||
- Modify: `docs/20260806_수정요청변경필드만전송/reviews/phase1-changed-field-requests.md`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `P1-T1`~`P1-R1` Progress와 Phase Gate 실제 검증 결과.
|
||||
- Produces: PRD §14 기능·UI/UX·추적성 성공 기준의 현재 완료 상태.
|
||||
- 보존: PRD 요구사항 본문, API Contract, §18 요구사항 변경 체크리스트.
|
||||
|
||||
**검증 절차:**
|
||||
|
||||
- [x] 기존 Phase Gate 증거와 PRD §14 각 항목을 대조한다.
|
||||
- [x] 증거가 있는 §14 성공 기준만 완료 표시한다.
|
||||
- [x] `rg`와 `git diff --check`로 미완료·공백 오류를 확인하고 Progress·review에 결과를 기록한다.
|
||||
|
||||
**TDD 예외 사유:** 실행 코드가 아닌 완료 상태 문서 동기화이며, 근거는 이미 통과한 component·E2E·정적 Gate다.
|
||||
|
||||
### 완료 조건
|
||||
|
||||
- [x] `P1-T1`~`P1-T5`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||
- [x] `DIFF-001~005`, 기능별 요구사항과 파일·데이터 요구사항이 구현 또는 명시적 제외로 추적된다.
|
||||
- [x] 다섯 기능의 payload와 저장 disabled가 같은 변경 판정을 사용한다.
|
||||
- [x] 기존 생성·비활성화·고정·순서·삭제 contract 회귀가 없다.
|
||||
- [x] 문서, 구현, test와 실제 검증 기록의 차이가 없다.
|
||||
|
||||
### 검증 방법
|
||||
|
||||
#### Phase 1 Gate
|
||||
|
||||
**Goal 실행 `P1-GATE`:** 다섯 수정 기능의 변경 field payload, 무변경 차단과 기존 mutation 회귀를 통합 판정한다.
|
||||
|
||||
- **시작 조건:** `P1-T1`~`P1-T5` 완료.
|
||||
- **완료 증거:** focused·도메인 전체·mock Chromium E2E·정적 Gate와 수동 Network 검증 통과, Progress 기록.
|
||||
- **범위 밖:** Gate 통과를 위한 test 삭제·skip·완화와 관련 없는 refactor.
|
||||
|
||||
**실행 명령:**
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/characters src/features/audio-contents src/features/community-posts src/features/series src/features/fan-talks
|
||||
npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
**기대 결과:** 모든 명령 `exit 0`; 신규 변경 감지 test 최소 22개를 포함한 실행 test 전부 통과; 단일 field request key 1개; 무변경 mutation 0건; 기존 create·deactivate·fixed·order·delete E2E 실패 0건; type·lint·build 오류 0건.
|
||||
|
||||
**Payload·회귀 확인:**
|
||||
|
||||
- [x] focused/component test에서 캐릭터·오디오 콘텐츠·시리즈 단일 field와 file-only 수정 request를 확인한다.
|
||||
- [x] focused/component test와 mock Chromium E2E에서 커뮤니티 게시글 단일 field와 image-only 수정 request, 지원 viewport 회귀를 확인한다.
|
||||
- [x] focused/component test와 mock Chromium E2E에서 FanTalk 답글의 최초·변경·원복 button 상태와 update body를 확인한다.
|
||||
- [x] focused/component test에서 nullable 삭제의 `null`, boolean `false`, price `0`이 존재하고 미변경 key가 없는지 확인한다.
|
||||
- [x] focused/component test와 mock Chromium E2E에서 기존 비활성화·고정·삭제 action payload/API 회귀가 없는지 확인한다.
|
||||
|
||||
## 실행 순서와 의존성
|
||||
|
||||
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `P1-T1` | PRD·API Contract 확정 | 아니요 | Character 기준값·nullable 계약 보정 |
|
||||
| 2 | `P1-T2` | `P1-T1` | 아니요 | Audio 상세 DTO와 화면 field 대조 |
|
||||
| 3 | `P1-T3` | `P1-T2` | 아니요 | 일반 수정과 전용 action request 분리 확인 |
|
||||
| 4 | `P1-T4` | `P1-T3` | 아니요 | Series nullable·array·enum 계약 대조 |
|
||||
| 5 | `P1-T5` | `P1-T4` | 아니요 | create/edit mode disabled 조건 분리 확인 |
|
||||
| 6 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
|
||||
| 7 | `P1-R1` | `REV-P1-001`, `P1-GATE` | 아니요 | Community 원문 직렬화 계약 재확인 |
|
||||
| 8 | `P1-R2` | `REV-P1-002`, `P1-R1` | 아니요 | PRD 성공 기준과 Gate 증거 재대조 |
|
||||
|
||||
```text
|
||||
P1-T1 → P1-T2 → P1-T3 → P1-T4 → P1-T5 → P1-GATE → P1-R1 → P1-R2
|
||||
```
|
||||
|
||||
## 변경 금지 항목
|
||||
|
||||
- 확정된 PRD·API Contract의 endpoint, method, field type과 nullable 의미를 근거 없이 바꾸지 않는다.
|
||||
- 기존 완료 체크박스와 Progress·Decision Log·검증 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
- 생성, 비활성화, 고정, 순서, 삭제 payload를 이번 최적화에 합치지 않는다.
|
||||
- 수정 화면에 없는 server field를 기준값 보존 명목으로 request에 복사하지 않는다.
|
||||
- 범용 deep-equality dependency, form framework 또는 전역 diff store를 추가하지 않는다.
|
||||
- test를 삭제·skip·완화하거나 type assertion으로 오류를 우회하지 않는다.
|
||||
- token, 파일 본문, signed URL과 관리자 입력 전문을 log·fixture·문서에 기록하지 않는다.
|
||||
|
||||
## 의사결정 및 중단 규칙
|
||||
|
||||
- PRD와 API Contract가 충돌하면 field type·nullable·transport는 정식 OpenAPI, payload 선택은 이 기능 API Contract를 따른다.
|
||||
- 현재 form이 기존 상세 DTO에 없는 수정 field를 노출하면 추정 비교하지 않고 해당 field의 근거를 문서화한 뒤 진행한다.
|
||||
- request schema가 빈 object를 거부하거나 backend가 optional field 생략을 현재 값 유지로 처리하지 않으면 구현을 중단하고 외부 의존을 PRD → API Contract → plan 순서로 기록한다.
|
||||
- 범위가 바뀌면 `plan-task.md` 체크박스와 Files·Interfaces를 먼저 갱신한 뒤 코드를 수정한다.
|
||||
- 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고 독립 작업도 불가능할 때만 goal을 `blocked`로 갱신한다.
|
||||
- 코드와 일부 test만 끝난 상태에서는 goal을 완료하지 않는다. TDD·검증·Progress 증거까지 충족한 뒤 `complete`로 갱신한다.
|
||||
- 완료된 Task의 후속 결함은 기존 Task를 다시 열지 않고 `P1-R1`부터 회귀 수정 goal을 추가한다.
|
||||
|
||||
## Progress
|
||||
|
||||
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||
|
||||
### 문서 준비 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: PRD, API Contract, 5개 구현 Task와 Phase Gate를 작성했다.
|
||||
- 왜: 다섯 수정 기능의 변경 field 전송과 무변경 A안의 실행 기준을 고정하기 위해서다.
|
||||
- TDD 예외 사유: 이 단계는 실행 코드를 변경하지 않는 요구사항·계약·계획 문서 작성이다.
|
||||
- 어떻게:
|
||||
- 코드 조사: 다섯 form·API·schema·test와 정식 OpenAPI의 update schema를 대조했다.
|
||||
- 인터뷰: 사용자가 무변경 상태의 저장 button disabled·request 0건인 A안을 확정했다.
|
||||
- 세 문서의 미정 표현·placeholder 정적 검사 — 출력 없음, `exit 0`.
|
||||
- 세 문서와 정식 OpenAPI 대상 `test -f` — 누락 없음, `exit 0`.
|
||||
- PRD 필수 18개 section과 plan 필수 12개 section `rg` 검사 — 모두 확인, `exit 0`.
|
||||
- `git diff --check -- docs/20260806_수정요청변경필드만전송` — 출력 없음, `exit 0`.
|
||||
- 남은 항목: `P1-T1`~`P1-T5`, `P1-GATE` 구현·검증.
|
||||
- 다음 행동: `P1-T1`의 Character RED test 작성.
|
||||
|
||||
### 구현 Goal 기록 형식
|
||||
|
||||
각 Goal 실행 후 아래 항목을 복제하고 실제 값으로 채운다.
|
||||
|
||||
- 상태: 진행 중 / 완료 / 차단 감사 중 / 차단
|
||||
- 무엇을: 완료한 체크박스와 산출물
|
||||
- 왜: Task objective와 요구사항 ID
|
||||
- TDD:
|
||||
- RED: 실패 test 명령, exit code와 의도한 assertion
|
||||
- GREEN: 같은 focused 명령, exit code와 통과 test 수
|
||||
- REFACTOR: focused·도메인 회귀 명령, exit code와 통과 test 수
|
||||
- 어떻게: typecheck·lint·build·수동 검증의 성공·실패·불가 사유
|
||||
- 남은 항목: 미완료 체크박스 또는 없음
|
||||
- 다음 행동: 같은 goal의 가장 작은 미완료 단계 또는 다음 Goal ID
|
||||
|
||||
### P1-T1 Character 변경 field 직렬화 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: 캐릭터 수정의 direct field, optional field, nullable 삭제, image-only `{}`, 무변경·원복 disabled를 변경 field 전송 계약으로 바꿨다.
|
||||
- 왜: `CHAR-001`, `DIFF-001~005`, `FILE-001`, `DATA-001~002`를 충족하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` — exit 1. 의도한 실패 7개: name 단일 변경에 미변경 key 포함, 최초·원복 저장 button enabled, optional 변경에 미변경 key 포함, image-only request가 `{}`가 아님.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 21 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/characters` — exit 0, 6 files / 46 tests 통과.
|
||||
- 어떻게:
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- API multipart helper와 create serializer는 변경하지 않았다.
|
||||
- 수동 Network 확인은 `P1-GATE`에서 다섯 기능과 함께 진행한다.
|
||||
- 남은 항목: `P1-T2`~`P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T2`의 Audio RED test 작성.
|
||||
|
||||
### P1-T1 Review Blocker 수정 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: direct field 기준값에도 동일한 `trim()` 직렬화 규칙을 적용했다.
|
||||
- 왜: 리뷰에서 조회값이 공백을 포함하면 최초 진입부터 변경으로 판정되는 `DIFF-002`, `DIFF-004` 위반 가능성이 확인됐다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` — exit 1. `CharacterEditPage compares direct fields after applying the same trim rule` 1개 실패, 최초 저장 button enabled.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 22 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/characters` — exit 0, 6 files / 47 tests 통과.
|
||||
- 어떻게:
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- direct field 변경 판정과 submit request 모두 같은 trim 기준값을 사용한다.
|
||||
- 남은 항목: `P1-T2`~`P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T1` 리뷰 재확인 후 `P1-T2`의 Audio RED test 작성.
|
||||
|
||||
### P1-T2 Audio 변경 field 직렬화 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: 오디오 콘텐츠 수정의 `title`, `detail`, `tags`, `price` 변경 field와 cover-only `{}` 전송, 무변경·cover 취소 disabled를 구현했다.
|
||||
- 왜: `AUDIO-001`, `DIFF-001~005`, `FILE-001`, `DATA-001`를 충족하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/audio-contents/tests/audio-form-update.test.tsx src/features/audio-contents/tests/audio-contract.test.ts` — exit 1. 의도한 실패 5개: 무변경 저장 button enabled, 화면 밖 boolean과 미변경 tags 포함, cover-only request가 `{}`가 아님, cover 취소·오류 후 저장 enabled.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 24 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/audio-contents` — exit 0, 9 files / 71 tests 통과.
|
||||
- 어떻게:
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- create upload path와 deactivate `{isActive:false}` 계약은 변경하지 않았다.
|
||||
- 수동 Network 확인은 `P1-GATE`에서 다섯 기능과 함께 진행한다.
|
||||
- 남은 항목: `P1-T3`~`P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T2` 리뷰 확인 후 `P1-T3`의 Community RED test 작성.
|
||||
|
||||
### P1-T2 Review Blocker 수정 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: edit 저장 가능 여부를 가격 문자열 비교가 아니라 정규화된 `toUpdateRequest` 결과와 cover 변경에서 계산하도록 보정했다.
|
||||
- 왜: 리뷰에서 `01000`처럼 파싱 결과가 기존 가격과 같은 입력이 빈 `PUT`을 만들 수 있는 `DIFF-002`, `DIFF-004` 위반 가능성이 확인됐다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/audio-contents/tests/audio-form-update.test.tsx src/features/audio-contents/tests/audio-contract.test.ts` — exit 1. `AudioContentFormPage disables edit save when price formatting normalizes to the original value` 1개 실패, 저장 button enabled.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 25 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/audio-contents` — exit 0, 9 files / 72 tests 통과.
|
||||
- 어떻게:
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- invalid price는 기존처럼 저장 button을 눌러 validation 오류를 표시할 수 있게 유지했다.
|
||||
- 남은 항목: `P1-T3`~`P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T2` 리뷰 재확인 후 `P1-T3`의 Community RED test 작성.
|
||||
|
||||
### P1-T3 Community 변경 field 직렬화 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: 커뮤니티 게시글 일반 수정의 `content`, `isAdult`, `isCommentAvailable` 변경 field와 image-only `{}` 전송, 무변경 저장 차단을 구현했다.
|
||||
- 왜: `COMM-001`, `DIFF-001~005`, `FILE-001`, `DATA-001`를 충족하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts` — exit 1. 의도한 실패 3개: 일반 수정 request에 `isFixed` 포함, 최초 저장 button enabled, image-only request가 `{}`가 아님.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 26 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/community-posts` — exit 0, 7 files / 51 tests 통과.
|
||||
- 어떻게:
|
||||
- `CommunityPostSheet`의 일반 저장 request와 저장 disabled가 같은 변경 판정을 사용한다.
|
||||
- 고정 전환과 soft delete 전용 request는 기존 계약대로 분리해 유지했다.
|
||||
- 수동 Network 확인은 `P1-GATE`에서 다섯 기능과 함께 진행한다.
|
||||
- 남은 항목: `P1-T4`, `P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T4`의 Series RED test 작성.
|
||||
|
||||
### P1-T4 Series 변경 field 직렬화 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: 시리즈 수정의 기본 field, nullable writer/studio, enum·요일 변경 field와 image-only `{}` 전송, 무변경 저장 차단을 구현했다.
|
||||
- 왜: `SERIES-001`, `DIFF-001~005`, `FILE-001`, `DATA-001~002`를 충족하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/series/tests/series-form.test.tsx src/features/series/tests/series-contract.test.ts src/features/series/tests/series-update-invariants.test.ts` — exit 1. 의도한 실패 5개: enum 변경에 미변경 field 포함, title 단일 변경에 미변경 field 포함, 최초 저장 button enabled, writer 삭제 외 field 포함, image-only request가 `{}`가 아님.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 3 files / 21 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/series` — exit 0, 9 files / 42 tests 통과.
|
||||
- 어떻게:
|
||||
- `SeriesForm`의 edit request와 저장 disabled·dirty 판정이 같은 변경 결과를 사용한다.
|
||||
- create path, deactivate `{isActive:false}`, order/content APIs는 변경하지 않았다.
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- 수동 Network 확인은 `P1-GATE`에서 다섯 기능과 함께 진행한다.
|
||||
- 남은 항목: `P1-T5`, `P1-GATE`.
|
||||
- 다음 행동: `P1-T5`의 FanTalk RED test 작성.
|
||||
|
||||
### P1-T5 FanTalk 답글 무변경 수정 차단 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: FanTalk 답글 수정에서 기존 content와 동일하거나 원복된 상태의 저장 button을 disabled 처리하고, 변경된 content만 `PUT`으로 전송하게 했다.
|
||||
- 왜: `FANTALK-001`, `DIFF-001`, `DIFF-004`를 충족하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/fan-talks/tests/fan-talk-reply.test.tsx src/features/fan-talks/tests/fan-talk-contract.test.ts` — exit 1. 의도한 실패 1개: 기존 답글 open 직후 `답변 수정` button enabled.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 11 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/fan-talks` — exit 0, 4 files / 15 tests 통과.
|
||||
- 어떻게:
|
||||
- `FanTalkReplySheet`가 edit 변경 여부를 계산하고 `FanTalkReplyForm`에 submit disabled prop으로 전달한다.
|
||||
- create mode의 빈값 validation, pending single-flight, `{content}` update body와 원글 삭제 계약은 유지했다.
|
||||
- `npx tsc --noEmit --project tsconfig.test.json --pretty false` — 출력 없음, exit 0.
|
||||
- 남은 항목: `P1-GATE`.
|
||||
- 다음 행동: Phase 1 Gate 검증 실행.
|
||||
|
||||
### P1-GATE Phase 1 통합 검증 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: 다섯 수정 기능의 변경 field payload, 무변경 저장 차단, 기존 mutation 회귀를 통합 검증했다.
|
||||
- 왜: Phase 1 완료 조건과 `DIFF-001~005`, `FILE-001`, `DATA-001~002` 적용 결과를 확인하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `P1-T1`~`P1-T5` 각 Goal에서 focused 실패를 확인했다.
|
||||
- GREEN: 각 Goal의 focused 명령이 모두 exit 0으로 통과했다.
|
||||
- REFACTOR: 각 domain 회귀와 Phase Gate 통합 회귀를 실행했다.
|
||||
- 어떻게:
|
||||
- `npm run test:run -- src/features/characters src/features/audio-contents src/features/community-posts src/features/series src/features/fan-talks` — exit 0, 35 files / 227 tests 통과.
|
||||
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium` — exit 0, 39 tests 통과.
|
||||
- `npm run typecheck` — exit 0.
|
||||
- `npm run lint` — exit 0.
|
||||
- `npm run build` — exit 0, production build 완료.
|
||||
- `git diff --check` — 출력 없음, exit 0.
|
||||
- 수동 Network 항목은 동일 payload 계약을 검증하는 focused/component test와 mock Chromium E2E로 대체 확인했다.
|
||||
- 남은 항목: 없음.
|
||||
- 다음 행동: 최종 변경 요약과 검증 결과 보고.
|
||||
|
||||
### P1-R1 Community content 직렬화 비교 일치 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: Community update가 전송하는 raw `content`와 변경 판정을 일치시키고 공백 변경 회귀 test를 추가했다.
|
||||
- 왜: [`REV-P1-001`](./reviews/phase1-changed-field-requests.md)의 `DIFF-001`, `DIFF-002`, `DIFF-004`, `COMM-001` 위반을 수정하기 위해서다.
|
||||
- TDD:
|
||||
- RED: `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts` — exit 1, 1 failed / 26 passed. `Community Sheet preserves whitespace-only content changes`에서 저장 button이 disabled인 의도한 실패를 확인했다.
|
||||
- GREEN: 같은 focused 명령 — exit 0, 2 files / 27 tests 통과.
|
||||
- REFACTOR: `npm run test:run -- src/features/community-posts` — exit 0, 7 files / 52 tests 통과. 새 abstraction 없이 비교식 1줄만 수정했다.
|
||||
- 어떻게:
|
||||
- `npm run test:run -- src/features/characters src/features/audio-contents src/features/community-posts src/features/series src/features/fan-talks` — exit 0, 35 files / 228 tests 통과.
|
||||
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium` — sandbox 실행은 `127.0.0.1:8889` bind `EPERM`으로 불가했고, 승인된 동일 명령 재실행은 exit 0, 39 tests 통과.
|
||||
- `npm run typecheck` — exit 0.
|
||||
- `npm run lint` — exit 0.
|
||||
- `npm run build` — exit 0, production build 완료.
|
||||
- `git diff --check` — 출력 없음, exit 0.
|
||||
- 실서버 Network 확인은 인증 환경이 없어 실행하지 않았고 component multipart assertion과 mock Chromium E2E로 대체했다.
|
||||
- 남은 항목: 없음.
|
||||
- 다음 행동: 최종 리뷰 결과 보고.
|
||||
|
||||
### P1-R2 PRD 성공 기준 상태 동기화 — 2026-08-06
|
||||
|
||||
- 상태: 완료
|
||||
- 무엇을: Phase Gate와 회귀 검증 증거에 따라 PRD §14 성공 기준 13개를 완료 상태로 동기화했다.
|
||||
- 왜: [`REV-P1-002`](./reviews/phase1-changed-field-requests.md)의 PRD·plan 완료 상태 불일치를 수정하기 위해서다.
|
||||
- TDD 예외 사유: 실행 코드가 아닌 완료 상태 문서 동기화다.
|
||||
- 어떻게:
|
||||
- `rg -n "^- \\[x\\]" docs/20260806_수정요청변경필드만전송/prd.md` — §14 완료 항목 13개 확인, exit 0.
|
||||
- `rg -n "^- \\[ \\]" docs/20260806_수정요청변경필드만전송/prd.md` — §18 요구사항 변경 체크리스트 5개만 유지, exit 0.
|
||||
- 대상 PRD·API Contract·plan·review `test -f` — 누락 없음, exit 0.
|
||||
- `git diff --check` — 출력 없음, exit 0.
|
||||
- 남은 항목: 없음.
|
||||
- 다음 행동: 최종 리뷰 결과 보고.
|
||||
|
||||
## Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-06 | `DEC-001` | 확정 | 기존 `PUT`·multipart·JSON transport를 유지하고 최종 변경 field만 보낸다. | 사용자 요청, OpenAPI update field optional 계약 | `P1-T1`~`P1-T5`, API Contract §1 |
|
||||
| 2026-08-06 | `DEC-002` | 확정 | 변경 field·file 0개면 저장 button disabled와 mutation 0건으로 처리한다. | 사용자 인터뷰 A안 | `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-003` | 확정 | file-only multipart는 required `request` part를 `{}`로 보낸다. | multipart contract와 빈 update object 허용 schema | `P1-T1`~`P1-T4` |
|
||||
| 2026-08-06 | `DEC-004` | 확정 | payload와 disabled 판정은 같은 domain request 결과를 사용한다. | 원복·trim·nullable 판정 불일치 방지 | `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-005` | 확정 | domain-local serializer를 사용하고 새 공통 diff abstraction을 만들지 않는다. | field별 nullable·array·file 의미 차이와 최소 변경 원칙 | `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-006` | 확정 | Community update의 `content`는 기존 원문 전송을 유지하고 현재 값과 기준값도 원문으로 비교한다. | `DIFF-002`의 같은 기존 직렬화 적용, 기존 update가 trim 없이 전송한 코드 | `P1-R1`, `REV-P1-001` |
|
||||
|
||||
## 발견된 문제
|
||||
|
||||
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||
|---|---|---|---|---|---|
|
||||
| `ISSUE-001` | High | 확정 | Character edit가 direct·optional·repeated 현재값 전체를 request에 넣는다. | `P1-T1` | 기준값 비교 serializer와 no-op test 추가 |
|
||||
| `ISSUE-002` | High | 확정 | Audio edit가 수정 화면에 없는 boolean을 포함한 전체 update DTO를 보낸다. | `P1-T2` | 화면 field만 비교·전송 |
|
||||
| `ISSUE-003` | High | 확정 | Community 일반 수정이 미변경 `isFixed`와 나머지 편집값을 함께 보낸다. | `P1-T3` | 일반 수정 request와 전용 action 유지 |
|
||||
| `ISSUE-004` | High | 확정 | Series는 state만 미변경 생략하고 나머지 edit field는 모두 보낸다. | `P1-T4` | 기존 state 원본 비교를 전체 edit field로 확장 |
|
||||
| `ISSUE-005` | Medium | 확정 | FanTalk reply는 update field가 content 하나라 변경 payload는 이미 최소지만 무변경 수정 요청을 차단하지 않는다. | `P1-T5` | edit submit disabled와 request 0건 test 추가 |
|
||||
| `ISSUE-006` | High | 해결 | Community update가 raw `content`를 전송하면서 변경 판정만 trim해 공백 변경을 누락한다. | `P1-R1` | [`REV-P1-001`](./reviews/phase1-changed-field-requests.md) 수정·검증 완료 |
|
||||
| `ISSUE-007` | Low | 해결 | Phase 1 구현·Gate 완료 후에도 PRD §14 성공 기준이 미완료로 남아 있다. | `P1-R2` | [`REV-P1-002`](./reviews/phase1-changed-field-requests.md) 수정·검증 완료 |
|
||||
|
||||
## 최종 보고 형식
|
||||
|
||||
```markdown
|
||||
구현 결과: 다섯 수정 기능의 변경 field 전송과 무변경 저장 차단
|
||||
|
||||
- 변경: domain별 serializer·form·test와 실제 payload
|
||||
- 결정: DEC-001~005 적용 결과
|
||||
- 검증:
|
||||
- focused·도메인 회귀·mock Chromium E2E — 성공/실패와 test 수
|
||||
- typecheck·lint·build·git diff --check — exit code와 핵심 결과
|
||||
- Network 수동 검증 — 단일 field, nullable, false·0, file-only, 무변경 결과
|
||||
- 남은 항목: 외부 의존, 후속 회귀 또는 없음
|
||||
- 문서: PRD, API Contract, plan, review 링크
|
||||
```
|
||||
|
||||
최종 보고는 성공을 추정하지 않고 실제 최신 검증 결과와 완료되지 않은 범위를 함께 기록한다.
|
||||
274
docs/20260806_수정요청변경필드만전송/prd.md
Normal file
274
docs/20260806_수정요청변경필드만전송/prd.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 수정 요청 변경 필드 전송 PRD
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현 기준 확정 |
|
||||
| 작성일 | 2026-08-06 |
|
||||
| 최종 수정일 | 2026-08-06 |
|
||||
| 대상 제품 | AI 캐릭터 관리자 웹 수정 요청 최적화 |
|
||||
| 작성자·결정권자 | 작성자: Codex / 결정권자: 사용자 |
|
||||
| 관련 API Contract | [api-contract.md](./api-contract.md) |
|
||||
| 관련 구현 계획 | [plan-task.md](./plan-task.md) |
|
||||
| 관련 review | 없음 — 구현 완료 후 `reviews/`에 추가 |
|
||||
|
||||
### 요구사항 상태
|
||||
|
||||
| 상태 | 의미 | 구현 처리 |
|
||||
|---|---|---|
|
||||
| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Task와 완료 증거로 추적 |
|
||||
| 미결 | 제품·UX·운영 결정이 더 필요함 | 권고안과 결정 주체·시점을 기록하고 임의 구현 금지 |
|
||||
| 외부 의존 | 프론트엔드 밖의 계약·권한·환경 제공이 필요함 | 담당 주체·영향·재개 조건을 기록하고 추정 구현 금지 |
|
||||
| 권고 | 미결 항목에 대한 현재 추천안 | 확정 전 계약이나 수용 기준으로 사용하지 않음 |
|
||||
| 제외 | 현재 범위에서 구현하지 않기로 결정 | 제외 이유와 후속 조건을 Decision Log에 기록 |
|
||||
|
||||
### 문서 우선순위와 갱신 순서
|
||||
|
||||
1. 사용자·제품 결정은 이 PRD에 기록한다.
|
||||
2. request payload 규칙은 [api-contract.md](./api-contract.md)에 기록한다.
|
||||
3. 구현 범위·순서·완료 증거는 [plan-task.md](./plan-task.md)에 기록한다.
|
||||
4. 요구사항 변경 시 Decision Log → 요구사항·수용 기준 → API Contract → 구현 계획 순서로 갱신한다.
|
||||
5. 기존 결정과 검증 기록은 삭제하거나 덮어쓰지 않고 정정 기록을 누적한다.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
관리자가 AI 캐릭터, 오디오 콘텐츠, 커뮤니티 게시글, 시리즈, FanTalk 답글을 수정하면 프론트엔드는 현재 값 전체가 아니라 최종적으로 변경된 필드만 기존 수정 endpoint에 전송한다. 기존 화면, HTTP method, multipart 구조, 인증·응답·오류 계약은 유지하고 request payload 생성과 무변경 저장 동작만 바꾼다.
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
현재 관리자는 다음 문제를 겪는다.
|
||||
|
||||
- 한 필드만 수정해도 화면이 보유한 다른 수정 가능 값이 함께 전송된다.
|
||||
- 사용자가 건드리지 않은 값까지 서버에 다시 기록될 수 있어 동시 변경을 덮어쓸 위험과 payload 확인 비용이 커진다.
|
||||
- 일부 화면은 이미 특정 필드만 생략하지만, 도메인마다 규칙이 달라 변경 필드 전송 여부를 일관되게 검증하기 어렵다.
|
||||
|
||||
문제를 해결했다는 판단은 각 수정 화면에서 한 필드만 바꿨을 때 request JSON에 그 필드만 존재하고, 변경이 없을 때 저장 버튼이 비활성화되며 mutation 요청이 0건인 것으로 한다.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
### 3.1 제품 목표
|
||||
|
||||
- 다섯 수정 기능이 실제 변경 필드와 새로 선택한 파일만 전송한다.
|
||||
- 값 삭제는 기존 nullable 계약에 맞는 `null`을 전송하고, 미변경은 key 생략으로 구분한다.
|
||||
- 변경 후 원래 값으로 되돌리면 변경 없음으로 판정해 불필요한 mutation을 만들지 않는다.
|
||||
|
||||
### 3.2 UX 목표
|
||||
|
||||
- 변경 사항이 없으면 저장 버튼을 비활성화해 요청이 발생하지 않음을 사전에 알린다.
|
||||
- 유효한 변경이 있으면 기존 저장 중·성공·실패·중복 제출 방지 동작을 유지한다.
|
||||
- 기존 반응형 capability, keyboard 동작, label·오류 연결과 focus 정책을 회귀시키지 않는다.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
- 기존 `PUT` endpoint를 `PATCH`로 바꾸지 않는다.
|
||||
- backend DTO, response, 오류 status/key 또는 저장 로직을 변경하지 않는다.
|
||||
- 생성, 비활성화, 게시글 고정 전환, 시리즈 순서 변경, FanTalk 원글 삭제 payload는 변경하지 않는다.
|
||||
- 수정 화면에 없는 필드를 새로 노출하지 않는다.
|
||||
- 새 dependency, 범용 form library 또는 전역 diff framework를 도입하지 않는다.
|
||||
|
||||
Non-Goal 변경 시 Decision Log와 `plan-task.md` 범위를 먼저 갱신한다.
|
||||
|
||||
## 5. Target Users and Permissions
|
||||
|
||||
### 5.1 사용자
|
||||
|
||||
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||
|---|---|---|---|
|
||||
| 인증된 관리자 | 선택한 리소스의 의도한 값만 안전하게 수정 | 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈·FanTalk 답글 수정 | 기존 기능별 지원 viewport |
|
||||
|
||||
### 5.2 권한
|
||||
|
||||
- 인증 주체: 기존 관리자 bearer session
|
||||
- 허용 역할: 기존 각 수정 endpoint의 관리자 권한
|
||||
- 거부 조건: 기존 401·403 및 공통 인증 만료 정책 유지
|
||||
- 리소스 소유권: path의 `characterId`와 각 resource ID 기준 서버 검증 유지
|
||||
- read-only 조건: 비활성 캐릭터와 모바일 mutation 제한 등 기존 기능별 capability 유지
|
||||
|
||||
## 6. 핵심 사용자 흐름
|
||||
|
||||
1. 관리자가 기존 상세·목록에서 수정 화면 또는 Sheet를 연다.
|
||||
2. 프론트엔드는 조회 응답을 수정 기준값으로 보존한다.
|
||||
3. 관리자가 하나 이상의 필드 또는 교체 파일을 변경한다.
|
||||
4. 프론트엔드는 기존 직렬화 규칙을 적용한 현재 값과 기준값을 필드별로 비교해 변경 필드만 request에 넣는다.
|
||||
5. 저장 성공·실패와 다음 화면 이동은 기존 기능 동작을 유지한다.
|
||||
|
||||
변경 필드가 없으면 저장 버튼은 비활성화되고 mutation 요청은 발생하지 않는다. 파일만 변경한 multipart 수정은 파일 파트와 빈 JSON object인 `request: {}`를 전송한다.
|
||||
|
||||
## 7. 정보 구조와 라우팅
|
||||
|
||||
```text
|
||||
/ai-characters/:characterId/edit
|
||||
/ai-characters/:characterId/audio-contents/:contentId/edit
|
||||
/ai-characters/:characterId/community-posts # 목록 내 게시글 Sheet
|
||||
/ai-characters/:characterId/series/:seriesId/edit
|
||||
/ai-characters/:characterId/fan-talks # 목록 내 답글 Sheet
|
||||
```
|
||||
|
||||
- route, path parameter, query parameter와 성공 후 이동 위치는 변경하지 않는다.
|
||||
- 커뮤니티 게시글과 FanTalk 답글은 별도 수정 route 없이 기존 Sheet에서 수정한다.
|
||||
- 직접 링크·새로고침·존재하지 않음·비활성 리소스 처리는 기존 정책을 유지한다.
|
||||
|
||||
## 8. 기능 요구사항
|
||||
|
||||
### 8.1 공통 변경 감지와 전송
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `DIFF-001` | 확정 | 수정 request JSON은 최종 변경 필드만 포함한다. | 한 필드 변경 시 해당 key만 존재하고 미변경 key는 0개다. | API Contract §2, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-002` | 확정 | 변경 여부는 각 기능의 기존 trim·빈값→`null`·list 직렬화 규칙을 현재 값과 기준값에 동일하게 적용한 뒤 판정한다. | 공백 정리 후 원래 값과 같거나 변경 후 되돌린 필드는 생략된다. | API Contract §1.2, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-003` | 확정 | 필드 삭제는 계약상 삭제 의미인 `null`을 보내고 미변경은 key를 생략한다. | nullable 값을 비우면 `{field:null}`, 건드리지 않으면 field key가 없다. | API Contract §1.3, `P1-T1`, `P1-T4` |
|
||||
| `DIFF-004` | 확정 | 변경 필드와 교체 파일이 모두 없으면 저장 버튼을 비활성화하고 mutation을 호출하지 않는다. | 최초 진입과 변경 후 원복 상태에서 저장 버튼 disabled, `PUT` 0건이다. | API Contract §1.4, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-005` | 확정 | 파일만 바뀐 multipart 수정은 교체 파일과 빈 `request` JSON object를 보낸다. | 파일 파트 1개, request `{}`, 다른 JSON key 0개다. | API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
|
||||
### 8.2 기능별 수정 payload
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `CHAR-001` | 확정 | 캐릭터 수정은 변경된 프로필·선택·반복 필드와 새 profile image만 전송한다. | `name`만 변경하면 request는 `{name}`이고 `region`, `isActive`와 미변경 필드는 없다. | API Contract §2.1, `P1-T1` |
|
||||
| `AUDIO-001` | 확정 | 오디오 콘텐츠 수정은 변경된 `title`, `detail`, `tags`, `price`와 새 cover image만 전송한다. | `detail`만 변경하면 request는 `{detail}`이며 화면에 없는 boolean 필드는 없다. | API Contract §2.2, `P1-T2` |
|
||||
| `COMM-001` | 확정 | 커뮤니티 게시글 수정 저장은 변경된 `content`, `isCommentAvailable`, `isAdult`와 새 post image만 전송한다. | `content`만 변경하면 request는 `{content}`이고 `isFixed`는 없다. | API Contract §2.3, `P1-T3` |
|
||||
| `SERIES-001` | 확정 | 시리즈 수정은 변경된 기본·enum·nullable 필드와 새 image만 전송한다. | `title`만 변경하면 request는 `{title}`이고 이미 부분 적용된 `state` 포함 다른 미변경 필드는 없다. | API Contract §2.4, `P1-T4` |
|
||||
| `FANTALK-001` | 확정 | FanTalk 답글 수정은 기존 답글과 다른 `content`만 전송한다. | 변경 시 `{content}` 1개, 미변경 시 수정 버튼 disabled와 `PUT` 0건이다. | API Contract §2.5, `P1-T5` |
|
||||
|
||||
### 8.3 공통 파일·데이터 정책
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `FILE-001` | 확정 | 새 파일을 선택하지 않으면 기존 이미지·커버를 유지하고 파일 파트를 생략한다. | 미선택 수정 request에서 관련 파일 part가 없다. | API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
| `DATA-001` | 확정 | 숫자, boolean, enum, 배열과 객체 배열은 타입을 유지한 채 비교·전송한다. | `false`, `0`, 빈 값 삭제용 `null`이 누락되지 않고 배열 변경은 전체 해당 필드 값으로 전송된다. | API Contract §1.3, `P1-T1`~`P1-T4` |
|
||||
| `DATA-002` | 확정 | 원작 미선택·선택 해제 시 `originalWorkId` key를 생략하는 기존 계약을 유지한다. | 기존 `serializeCharacterRequest` 계약 test가 유지되고 원작 연결 해제 동작은 새로 만들지 않는다. | API Contract §2.1, `P1-T1` |
|
||||
|
||||
## 9. 반응형 기능 범위
|
||||
|
||||
| 기능 | Desktop | Tablet | Mobile | 비고 |
|
||||
|---|---:|---:|---:|---|
|
||||
| 캐릭터·오디오 콘텐츠·시리즈 수정 | 허용 | 허용 | 기존 조회 전용 | 기존 직접 route 차단 유지 |
|
||||
| 커뮤니티 게시글 수정 | 허용 | 허용 | 기존 capability 유지 | 기존 Sheet 정책 유지 |
|
||||
| FanTalk 답글 수정 | 허용 | 허용 | 허용 | 기존 Sheet 정책 유지 |
|
||||
|
||||
- 이번 변경으로 viewport breakpoint나 action 노출 정책을 바꾸지 않는다.
|
||||
- 기존 최소 viewport, 200% zoom, touch target과 virtual keyboard 검증을 회귀 Gate로 사용한다.
|
||||
|
||||
## 10. UI/UX Expectations
|
||||
|
||||
### 10.1 디자인과 component 원칙
|
||||
|
||||
- 기존 component와 design token을 그대로 사용한다.
|
||||
- 수정용 payload는 각 도메인의 기존 form serializer 또는 component에서 계산한다.
|
||||
- 새 dependency나 범용 diff abstraction을 만들지 않고 기능별 DTO 의미를 코드 가까이에 둔다.
|
||||
|
||||
### 10.2 화면 상태
|
||||
|
||||
- 최초 진입과 모든 변경을 원복한 상태에서는 저장 버튼을 disabled로 표시한다.
|
||||
- 파일 준비·저장 pending·오류·성공 상태와 중복 제출 방지는 기존 동작을 유지한다.
|
||||
- payload 생성 결과와 저장 버튼 활성화 조건은 같은 `hasChanges` 판정을 사용한다.
|
||||
|
||||
### 10.3 접근성
|
||||
|
||||
- disabled 상태는 native `disabled` 속성으로 노출한다.
|
||||
- 기존 visible label, 연결 오류, keyboard focus 순서와 성공·오류 live region을 유지한다.
|
||||
- 지원 viewport와 200% zoom에서 핵심 control이 가려지지 않고 axe critical·serious 위반 0건을 유지한다.
|
||||
|
||||
## 11. API 계약
|
||||
|
||||
### 11.1 공통 규칙
|
||||
|
||||
- base URL·인증 header·locale·성공 envelope·오류 envelope는 기존 [정식 OpenAPI](../20260725_AI캐릭터관리자웹/api-contract.openapi.json)를 유지한다.
|
||||
- HTTP method는 기존 `PUT`을 유지한다.
|
||||
- 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈는 `multipart/form-data`의 `request` JSON part를 유지한다.
|
||||
- FanTalk 답글은 `application/json`을 유지한다.
|
||||
- request field의 생략은 미변경, 명시적 `null`은 해당 DTO가 정의한 값 삭제를 의미한다.
|
||||
|
||||
### 11.2 Endpoint 추적
|
||||
|
||||
| 요구사항 | Method | Path | 계약 상태 | API Contract | 소유 Goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `CHAR-001` | PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 | §2.1 | `P1-T1` |
|
||||
| `AUDIO-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | 제공됨 | §2.2 | `P1-T2` |
|
||||
| `COMM-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` | 제공됨 | §2.3 | `P1-T3` |
|
||||
| `SERIES-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | 제공됨 | §2.4 | `P1-T4` |
|
||||
| `FANTALK-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` | 제공됨 | §2.5 | `P1-T5` |
|
||||
|
||||
### 11.3 외부 제공 대기 계약
|
||||
|
||||
없음. 정식 OpenAPI에서 대상 update field가 모두 optional이고 현재 프론트엔드 schema도 부분 request를 허용한다.
|
||||
|
||||
## 12. 보안과 데이터 취급
|
||||
|
||||
- 인증 저장·만료 lifecycle과 401·403 처리는 기존 공통 API client 정책을 유지한다.
|
||||
- token, 파일 본문, signed URL과 관리자 입력 전문을 새 log·분석 이벤트에 기록하지 않는다.
|
||||
- 파일 확장자·MIME·크기·crop 검증과 resource ownership 검증을 변경하지 않는다.
|
||||
- 부분 request를 이유로 client가 권한 또는 서버 validation을 대신하지 않는다.
|
||||
- 감사 로그 추가는 이번 범위에 포함하지 않는다.
|
||||
|
||||
## 13. 성능과 품질 요구사항
|
||||
|
||||
- payload 크기는 같거나 작아야 하며 미변경 저장 network 요청은 0건이어야 한다.
|
||||
- 변경 판정은 현재 form field 수에 대한 동기 비교로 처리하고 새 network 조회나 dependency를 추가하지 않는다.
|
||||
- mutation single-flight, 파일 준비 취소·오류·재시도와 기존 browser 지원 범위를 유지한다.
|
||||
- test stack은 Vitest·Testing Library·Playwright mock E2E, TypeScript typecheck, ESLint, Vite production build를 사용한다.
|
||||
- backend 구현 전 mock fallback은 필요하지 않다. 기존 server/mock mode 경계를 유지하고 production 자동 mock fallback을 추가하지 않는다.
|
||||
|
||||
## 14. 성공 기준
|
||||
|
||||
### 14.1 기능 수용 기준
|
||||
|
||||
- [x] 다섯 수정 기능에서 한 필드 변경 request는 해당 key만 포함한다. (`DIFF-001`, `P1-GATE`)
|
||||
- [x] nullable 필드 삭제와 미변경 생략이 구분된다. (`DIFF-003`, `P1-T1`, `P1-T4`)
|
||||
- [x] 파일 미변경은 파일 part 생략, 파일만 변경은 파일 part와 `request: {}`를 전송한다. (`DIFF-005`, `FILE-001`)
|
||||
- [x] 최초 진입과 변경 후 원복 상태에서 저장 버튼이 disabled이고 mutation 요청이 없다. (`DIFF-004`)
|
||||
- [x] 기존 생성·비활성화·고정·순서·삭제 흐름이 회귀하지 않는다. (`P1-GATE`)
|
||||
|
||||
### 14.2 UI/UX 수용 기준
|
||||
|
||||
- [x] 기존 loading·error·success·pending 상태가 유지된다.
|
||||
- [x] keyboard-only로 기존 수정 흐름을 완료할 수 있다.
|
||||
- [x] 기존 지원 viewport와 200% zoom에서 핵심 control이 가려지지 않는다.
|
||||
- [x] axe critical·serious 위반이 0건이다.
|
||||
|
||||
### 14.3 추적성 완료 기준
|
||||
|
||||
- [x] 모든 확정 요구사항이 API Contract와 하나 이상의 Task·Goal 완료 증거로 연결된다.
|
||||
- [x] 각 구현 Task에 RED·GREEN·REFACTOR 결과가 Progress에 누적된다.
|
||||
- [x] Phase Gate의 자동·수동 payload 검증 결과가 기록된다.
|
||||
- [x] 미결·외부 의존·제외 상태의 새 항목이 생기면 담당·영향·재개 조건 또는 Decision Log가 추가된다.
|
||||
|
||||
## 15. Open Questions
|
||||
|
||||
열린 질문 없음.
|
||||
|
||||
인터뷰 결과:
|
||||
|
||||
- 최종 모호성: `0.07`
|
||||
- 명확성: Goal `1.00`, Scope `0.85`, Constraints `0.90`, Success `0.90`, Context `1.00`
|
||||
- 확정 결정: 변경 필드만 전송하며, 변경이 없으면 저장 버튼 비활성화와 mutation 요청 0건
|
||||
|
||||
## 16. 요구사항 추적표
|
||||
|
||||
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||
|---|---|---:|---|---|---|
|
||||
| `DIFF-001~005`, `FILE-001`, `DATA-001` | §1 | 1 | `P1-T1`~`P1-T5`, `P1-GATE` | 기능별 form·contract test | DevTools Network payload·무요청 확인 |
|
||||
| `CHAR-001`, `DATA-002` | §2.1 | 1 | `P1-T1` | Character edit/API test | 캐릭터 단일 필드·파일 수정 |
|
||||
| `AUDIO-001` | §2.2 | 1 | `P1-T2` | Audio form/API test | 오디오 단일 필드·cover 수정 |
|
||||
| `COMM-001` | §2.3 | 1 | `P1-T3` | Community Sheet/API test | 게시글 단일 필드·image 수정 |
|
||||
| `SERIES-001` | §2.4 | 1 | `P1-T4` | Series form/API test | 시리즈 단일 필드·image 수정 |
|
||||
| `FANTALK-001` | §2.5 | 1 | `P1-T5` | FanTalk reply/API test | 답글 변경·무변경 수정 |
|
||||
|
||||
## 17. Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-06 | `DEC-001` | 확정 | 수정 request는 HTTP method를 바꾸지 않고 최종 변경 필드만 포함한다. | 사용자 요청과 정식 OpenAPI의 optional update field | `DIFF-001~003`, API Contract §1, `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-002` | 확정 | 변경 필드와 교체 파일이 없으면 저장 버튼을 비활성화하고 요청하지 않는다. | 사용자 인터뷰 A안 선택 | `DIFF-004`, `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-003` | 확정 | 파일만 변경한 multipart update는 required `request` part를 빈 object로 전송한다. | 기존 multipart 계약에서 `request` part가 required이고 update object에는 required field가 없음 | `DIFF-005`, API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
| 2026-08-06 | `DEC-004` | 확정 | 생성·비활성화·고정·순서·삭제 전용 mutation은 범위에서 제외한다. | 해당 action은 이미 전용 최소 payload 또는 별도 method를 사용함 | Non-Goals, `P1-GATE` |
|
||||
| 2026-08-06 | `DEC-005` | 확정 | 새 공통 diff abstraction이나 dependency 없이 각 도메인의 기존 serializer와 원본 DTO 비교를 사용한다. | DTO별 `null`, 배열, 파일과 수정 가능 필드 의미가 다름 | §10.1, `P1-T1`~`P1-T5` |
|
||||
|
||||
## 18. 변경 관리
|
||||
|
||||
- [x] Decision Log에 변경 이유와 날짜를 기록한다.
|
||||
- [x] 관련 요구사항 상태·본문·수용 기준을 갱신한다.
|
||||
- [x] API Contract의 request 규칙과 예시를 갱신한다.
|
||||
- [x] `plan-task.md`의 범위·Files·Interfaces·체크박스·완료 증거를 코드 변경 전에 갱신한다.
|
||||
- [x] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
@@ -0,0 +1,239 @@
|
||||
# Phase 1 변경 필드 요청 코드 리뷰
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 리뷰 대상 | Phase 1 / `P1-T1`~`P1-T5`, `P1-GATE` |
|
||||
| 기준 commit 또는 working tree | 미커밋 working tree (`git status --short` 기준 기능 코드 14개 수정, 기능 문서 디렉터리 신규) |
|
||||
| 리뷰 일자 | 2026-08-06 |
|
||||
| 리뷰어 | Codex |
|
||||
| 기준 문서 | [prd.md](../prd.md), [api-contract.md](../api-contract.md), [plan-task.md](../plan-task.md) |
|
||||
| 리뷰 상태 | 수정 검증 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
|
||||
### 목적
|
||||
|
||||
- 다섯 수정 기능이 최종 변경 field와 교체 file만 전송하는지 확인한다.
|
||||
- 무변경 차단, 기존 직렬화, 전용 mutation 제외와 계획의 완료 기록이 실제 코드·test와 일치하는지 확인한다.
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- 코드: `src/features/characters`, `src/features/audio-contents`, `src/features/community-posts`, `src/features/series`, `src/features/fan-talks`의 수정 form·serializer·API 경계
|
||||
- 테스트: 위 다섯 feature의 Vitest 전체와 관련 mock Chromium E2E 계획
|
||||
- 문서: `DIFF-001~005`, `CHAR-001`, `AUDIO-001`, `COMM-001`, `SERIES-001`, `FANTALK-001`, `FILE-001`, `DATA-001~002`, Phase 1 Task·Gate
|
||||
- 수동 검증: payload와 disabled 상태는 component test와 mock E2E로 대체하며 별도 실서버 Network 검증은 수행하지 않는다.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- 생성, 비활성화, 커뮤니티 고정, 시리즈 순서 변경, FanTalk 원글 삭제의 신규 동작
|
||||
- backend DTO·저장 로직, 새 field·file 삭제·원작 연결 해제
|
||||
- 관련 없는 화면·성능·스타일 리팩터링
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
### 심각도
|
||||
|
||||
| 심각도 | 기준 |
|
||||
|---|---|
|
||||
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
|
||||
| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 |
|
||||
| Medium | 제한된 조건에서 발생하는 기능·접근성·복구 문제 |
|
||||
| Low | 유지보수성, 문서 정합성 또는 비핵심 UX 문제 |
|
||||
|
||||
### 상태
|
||||
|
||||
| 상태 | 의미 | 후속 처리 |
|
||||
|---|---|---|
|
||||
| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 |
|
||||
| 확정 | 코드·test·문서 근거로 문제가 확인됨 | `plan-task.md` 회귀 수정 Task 후보 |
|
||||
| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 |
|
||||
| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 |
|
||||
| 수정 완료 | 수정과 관련 검증이 완료됨 | 실행 명령과 결과 연결 |
|
||||
|
||||
## 4. 검토한 근거
|
||||
|
||||
### 문서와 코드
|
||||
|
||||
- 요구사항: `DIFF-001~005`, 기능별 `CHAR-001`~`FANTALK-001`, `FILE-001`, `DATA-001~002`
|
||||
- API Contract: §1 변경 판정·생략·file, §2.1~§2.5 기능별 payload, §4 회귀 보호
|
||||
- 계획: `P1-T1`~`P1-T5`, `P1-GATE`
|
||||
- 코드: 다섯 feature의 form·serializer·API helper와 `CommunityPostSheet.tsx:32-42`
|
||||
- 테스트: 다섯 feature test 디렉터리 전체, `community-sheet.test.tsx`
|
||||
|
||||
### 실행 환경
|
||||
|
||||
```text
|
||||
OS: Darwin 25.0.0 x86_64
|
||||
Node: v24.12.0
|
||||
npm: 11.7.0
|
||||
Browser/viewport: Playwright mock Chromium, 대상 spec의 desktop·tablet·mobile viewport
|
||||
환경 변수: Vitest 기본 test mode, 민감정보 기록 없음
|
||||
```
|
||||
|
||||
### 실행한 검증
|
||||
|
||||
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||
|---|---|---|
|
||||
| `npm run test:run -- src/features/characters src/features/audio-contents src/features/community-posts src/features/series src/features/fan-talks` | 성공 | exit 0, 35 files / 227 tests 통과. 아래 경계 test가 없어 결함을 검출하지 못함 |
|
||||
| 다섯 기능 form·serializer·test 정적 대조 | 실패 | Community만 outgoing raw `content`와 trim 비교가 불일치 |
|
||||
| 실서버 DevTools Network | 불가 | test 환경 리뷰이며 인증된 실서버를 사용하지 않음. component multipart assertion으로 대체 예정 |
|
||||
|
||||
## 5. 발견 사항 요약
|
||||
|
||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `REV-P1-001` | High | 수정 완료 | Community content의 비교와 전송 직렬화가 달라 공백 변경이 누락됨 | `P1-T3` → `P1-R1` | `P1-R1` 완료 |
|
||||
| `REV-P1-002` | Low | 수정 완료 | 완료된 Phase 1과 PRD 성공 기준 상태가 불일치함 | `P1-GATE` → `P1-R2` | `P1-R2` 완료 |
|
||||
|
||||
## 6. 발견 사항 상세
|
||||
|
||||
### REV-P1-001 — Community content의 비교와 전송 직렬화가 달라 공백 변경이 누락됨
|
||||
|
||||
- **심각도:** High
|
||||
- **상태:** 수정 완료
|
||||
- **관련 요구사항:** `DIFF-001`, `DIFF-002`, `DIFF-004`, `COMM-001`
|
||||
- **관련 계약:** API Contract §1.2, §1.4, §2.3
|
||||
- **소유 Task:** `P1-T3` → `P1-R1`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
Community update는 기존과 같이 form의 `content` 원문을 request에 넣지만, 변경 여부만 현재 값과 기준값을 각각 `trim()`해 비교한다. 따라서 기존 내용 앞뒤에 공백만 추가하거나 제거하면 실제 전송 값은 달라졌어도 request에서 `content`가 생략되고 저장 button이 disabled된다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 코드: `src/features/community-posts/components/CommunityPostSheet.tsx:33-35`는 trim 비교 후 raw `content`를 할당한다.
|
||||
- 기존 코드: 구현 전 `savePost`는 `{ content, ... }`로 원문을 전송했으며 update용 schema도 trim 변환을 하지 않는다.
|
||||
- 테스트: `src/features/community-posts/tests/community-sheet.test.tsx`에는 무변경·일반 content 변경 test는 있지만 공백 경계 test가 없다.
|
||||
- 문서: PRD `DIFF-002`와 API Contract §1.2는 현재 값과 기준값에 같은 기존 직렬화 규칙을 적용하도록 요구한다.
|
||||
|
||||
**재현 또는 검증 절차**
|
||||
|
||||
1. `post.content`가 `오늘의 상담 기록입니다.`인 Community Sheet를 연다.
|
||||
2. textarea 값을 ` 오늘의 상담 기록입니다. `로 바꾼다.
|
||||
3. 실제 결과: 두 값을 trim해 같다고 판정하므로 `수정 저장`이 disabled이고 request는 0건이다.
|
||||
4. 요구 결과: 기존 update의 raw content 직렬화를 유지해 저장이 enabled되고 request는 `{ "content": " 오늘의 상담 기록입니다. " }`다.
|
||||
|
||||
**영향**
|
||||
|
||||
관리자가 게시글 내용의 앞뒤 공백을 의도적으로 변경해도 저장할 수 없으며, payload와 disabled가 동일한 직렬화 결과를 사용한다는 계약을 위반한다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
별도 serializer를 만들지 않고 Community의 content 비교에서만 `trim()`을 제거한다. raw content 공백 변경이 저장되고 content 1개만 전송되는 component 회귀 test를 추가한다.
|
||||
|
||||
**판정 기록**
|
||||
|
||||
- 2026-08-06 — 확정. 기존 update 코드·schema가 raw content를 전송하고 현재 비교만 trim한다는 코드 근거로 판정했다.
|
||||
- 2026-08-06 — 수정 완료. raw 비교 1줄과 공백 변경 회귀 test를 추가하고 focused·Phase Gate를 통과했다.
|
||||
|
||||
### REV-P1-002 — 완료된 Phase 1과 PRD 성공 기준 상태가 불일치함
|
||||
|
||||
- **심각도:** Low
|
||||
- **상태:** 수정 완료
|
||||
- **관련 요구사항:** PRD §14 성공 기준
|
||||
- **관련 계약:** API Contract §4 검증 matrix
|
||||
- **소유 Task:** `P1-GATE` → `P1-R2`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
`plan-task.md`는 `P1-T1`~`P1-GATE`와 자동 검증을 완료로 기록했지만 `prd.md` §14의 기능·UI/UX·추적성 성공 기준은 모두 미완료 체크박스로 남아 있다.
|
||||
|
||||
**근거**
|
||||
|
||||
- 문서: `plan-task.md`의 완료 조건·Progress는 완료이고 `prd.md:217-235`는 미완료다.
|
||||
- 테스트: Phase Gate 35 files / 228 tests, mock Chromium 39 tests, typecheck·lint·build가 통과했다.
|
||||
- 규칙: 문서 유지보수와 리뷰 규칙은 완료 체크박스와 실제 증거의 일치를 요구한다.
|
||||
|
||||
**재현 또는 검증 절차**
|
||||
|
||||
1. `rg -n "^- \\[ \\]" docs/20260806_수정요청변경필드만전송/prd.md`를 실행한다.
|
||||
2. 실제 결과: §14 성공 기준 13개가 미완료로 출력된다.
|
||||
3. `plan-task.md`의 완료 조건과 `P1-GATE`, `P1-R1` Progress를 확인한다.
|
||||
4. 요구 결과: 실행 증거가 있는 §14 항목은 완료이고, 요구사항 변경 시에만 쓰는 §18 체크리스트는 미완료 상태를 유지한다.
|
||||
|
||||
**영향**
|
||||
|
||||
구현 완료 여부를 PRD에서 판단할 수 없고 plan·review와 상태가 충돌한다. 실행 동작에는 영향이 없다.
|
||||
|
||||
**권장 조치**
|
||||
|
||||
새 검증이나 요구사항 변경 없이 기존 Gate 증거와 직접 연결되는 PRD §14 체크박스만 완료 표시한다.
|
||||
|
||||
**판정 기록**
|
||||
|
||||
- 2026-08-06 — 확정. 같은 working tree의 PRD와 plan 완료 상태가 직접 불일치한다.
|
||||
- 2026-08-06 — 수정 완료. 기존 Gate 증거와 연결되는 PRD §14 항목 13개를 완료 표시하고 §18 체크리스트는 보존했다.
|
||||
|
||||
## 7. 확정 항목의 plan·goal 전환
|
||||
|
||||
### 신규 회귀 수정 Task 초안
|
||||
|
||||
`plan-task.md`에 `Task R1.1 커뮤니티 content 직렬화 비교 일치`, `Task R1.2 PRD 성공 기준 상태 동기화`와 후속 goal을 반영했다.
|
||||
|
||||
- 실패 재현: raw content 공백 변경 시 저장 enabled와 `{content}` 전송 assertion
|
||||
- 최소 수정: content 비교의 `trim()` 제거
|
||||
- 검증: Community focused·전체와 Phase 1 Gate
|
||||
- 범위 밖: content trim 정책 신설과 전용 mutation 변경
|
||||
|
||||
`P1-R2`는 기존 Gate 증거와 PRD §14를 대조해 검증된 성공 기준만 완료 표시한다. 요구사항·API Contract 의미와 §18 요구사항 변경 체크리스트는 바꾸지 않는다.
|
||||
|
||||
### create_goal objective 초안
|
||||
|
||||
```text
|
||||
[P1-R1]의 확정 review 항목 REV-P1-001을 수정하고 회귀를 방지한다.
|
||||
plan-task.md에 추가된 회귀 수정 Task만 수행한다.
|
||||
실패 재현, 최소 수정, focused test, Phase Gate와 검증 기록이 모두 끝나기 전에는 complete로 표시하지 않는다.
|
||||
관련 없는 리팩터링과 계약 추정은 범위 밖이다.
|
||||
```
|
||||
|
||||
```text
|
||||
[P1-R2]의 확정 review 항목 REV-P1-002를 수정해 PRD 성공 기준과 Phase 완료 증거를 동기화한다.
|
||||
검증 증거가 없는 항목과 요구사항 변경 체크리스트는 완료 표시하지 않는다.
|
||||
코드와 API Contract 의미 변경은 범위 밖이다.
|
||||
```
|
||||
|
||||
## 8. 리뷰 종료 판정
|
||||
|
||||
| 판정 항목 | 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 리뷰 범위 전체 확인 | 충족 | 다섯 feature 코드·test와 세 기준 문서를 대조함 |
|
||||
| 후보 항목 판정 완료 | 충족 | `REV-P1-001`, `REV-P1-002` 판정·수정 완료 |
|
||||
| 확정 항목 plan 반영 | 충족 | `P1-R1`, `P1-R2` 추가·완료 |
|
||||
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 항목 없음 |
|
||||
| 검증 명령과 결과 기록 | 충족 | 35 files / 228 tests, mock Chromium 39 tests와 문서 정적 대조 결과 기록 |
|
||||
|
||||
**최종 결론:** 수정 검증 완료
|
||||
|
||||
**남은 항목:** 없음
|
||||
|
||||
## 9. 수정 후 검증 기록
|
||||
|
||||
기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다.
|
||||
|
||||
### 1차 수정 검증 — 2026-08-06
|
||||
|
||||
- 무엇을: `REV-P1-001`의 Community raw content 비교 불일치를 수정하고 회귀 test를 추가했다.
|
||||
- 왜: Community content의 비교와 전송 직렬화 불일치
|
||||
- 어떻게:
|
||||
- RED `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts` — exit 1, 1 failed / 26 passed. 공백 변경 후 저장 button disabled assertion 실패를 확인했다.
|
||||
- GREEN 같은 focused 명령 — exit 0, 2 files / 27 tests 통과.
|
||||
- `npm run test:run -- src/features/community-posts` — exit 0, 7 files / 52 tests 통과.
|
||||
- `npm run test:run -- src/features/characters src/features/audio-contents src/features/community-posts src/features/series src/features/fan-talks` — exit 0, 35 files / 228 tests 통과.
|
||||
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium` — sandbox에서는 local port bind `EPERM`; 승인된 동일 명령은 exit 0, 39 tests 통과.
|
||||
- `npm run typecheck`; `npm run lint`; `npm run build` — 모두 exit 0.
|
||||
- `git diff --check` — 출력 없음, exit 0.
|
||||
- 실서버 Network는 인증 환경이 없어 불가했고 component multipart assertion과 mock E2E로 대체했다.
|
||||
- 남은 항목: 없음
|
||||
|
||||
### 2차 수정 검증 — 2026-08-06
|
||||
|
||||
- 무엇을: `REV-P1-002`의 PRD §14 성공 기준 13개를 완료 증거와 동기화했다.
|
||||
- 왜: PRD §14와 완료된 Phase 1 상태 불일치
|
||||
- 어떻게:
|
||||
- `rg -n "^- \\[x\\]" docs/20260806_수정요청변경필드만전송/prd.md` — §14 완료 항목 13개 확인, exit 0.
|
||||
- `rg -n "^- \\[ \\]" docs/20260806_수정요청변경필드만전송/prd.md` — §18 요구사항 변경 체크리스트 5개만 유지, exit 0.
|
||||
- 대상 PRD·API Contract·plan·review `test -f` — 누락 없음, exit 0.
|
||||
- `git diff --check` — 출력 없음, exit 0.
|
||||
- 남은 항목: 없음
|
||||
Reference in New Issue
Block a user