feat(ai-character): 커뮤니티 생성 미디어 흐름 개선
This commit is contained in:
@@ -10,12 +10,12 @@
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 2026-08-04 `P5-R6` Series form 이미지 순서·keyword chip UX 자동 검증 완료. `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` 자동 보완 완료, 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기 |
|
||||
| 상태 | 2026-08-04 `P6-R5` FileField 이미지 preview·Community 생성 media flow·공용 CAN 가격 계약 구현, 표준 배율 browser QA와 최종 review 완료. `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` 자동 보완 완료, 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기 |
|
||||
| 최초 작성일 | 2026-07-25 |
|
||||
| 재작성일 | 2026-07-26 |
|
||||
| 요구사항 기준 | [prd.md](./prd.md) |
|
||||
| API 기준 | [api-contract.openapi.json](./api-contract.openapi.json) |
|
||||
| 현재 활성 Goal | `P5-R6` 자동 검증 완료 (`P5-R5` 완료) |
|
||||
| 현재 활성 Goal | `P6-R5` 완료 |
|
||||
|
||||
## 목표
|
||||
|
||||
@@ -3118,6 +3118,35 @@ focus가 Sheet trigger로 복귀하는지 본다.
|
||||
- 리뷰어 게이트: 1차 review에서 width 판정 전 release 순서 blocker를 지적받아 보완했고, delta review에서 `APPROVED` 판정을 받았다.
|
||||
- E2E/수동: 개발 중 E2E와 반복 GIF browser 수동 확인은 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다.
|
||||
|
||||
### Task R6.5 — 업로드 이미지 미리보기와 Community 생성 media flow 정렬
|
||||
|
||||
**Goal 실행 `P6-R5`:** 공통 `FileField`에 실제 업로드 이미지 미리보기를 제공하고 Community 생성 화면을 이미지 → 조건부 오디오 → 내용 순서와 Audio 동일 가격 계약으로 정렬한다.
|
||||
|
||||
- **시작 조건:** PRD `COMMUNITY-003`, `COMMUNITY-007`, `COMMUNITY-013`, `COMMUNITY-016`, `FILE-016`; 사용자 승인 설계.
|
||||
- **완료 증거:** 이미지 preview URL 생성·교체·해제, 비이미지 filename-only, Community media 순서·audio 조건부 표시/제거, 기본 가격 0·blank 거부·항상 price 전송의 RED/GREEN, Audio 가격 회귀, Chromium mock browser와 visual QA.
|
||||
- **Files:** Create: `src/shared/ui/can-price-field.tsx`, `src/shared/ui/__tests__/can-price-field.test.tsx`; Modify: `src/app/protected-admin-shell.tsx`, `src/app/App.protected-shell.test.tsx`, `src/shared/ui/file-field.tsx`, `src/shared/ui/page-state.tsx`, `src/shared/ui/__tests__/file-field.test.tsx`, `src/shared/ui/__tests__/page-state.test.tsx`, `src/features/audio-contents/components/AudioContentForm.tsx`, `src/features/audio-contents/components/audio-content-form-helpers.ts`, `src/features/audio-contents/tests/audio-form-create-red.test.tsx`, `src/features/audio-contents/tests/audio-form-update.test.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/community-posts/components/CommunityPostForm.tsx`, `src/features/community-posts/components/community-post-form-helpers.ts`, `src/features/community-posts/tests/community-form.test.tsx`, `src/features/community-posts/tests/community-price-validation.test.tsx`, `src/features/community-posts/tests/community-contract.test.ts`, `tests/e2e/community.spec.ts`, `DESIGN.md`, `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`.
|
||||
- **Interfaces:** `FileField`는 image File일 때만 object URL preview를 소유하고 교체·제거·unmount에서 revoke한다. `CanPriceField({value,onChange,error,errorId})`는 Audio와 Community의 native number/min=0/step=1/단위·오류 연결을 공유하고 range validation은 domain submit이 소유한다. Community는 최종 `postImage`가 있을 때만 audio input을 렌더하며 image 제거·교체 시작 시 `audioFile`을 제거한다.
|
||||
- **범위 밖:** Community 수정 Sheet, multipart append 순서, API schema의 optional price, 비이미지 preview, 새 dependency, GIF resize·crop·re-encode.
|
||||
|
||||
- [x] **RED:** `FileField previews image values and revokes object URLs on replacement, clear, and unmount`; `FileField keeps non-image values filename-only without creating an object URL`을 실패시킨다.
|
||||
- [x] **GREEN:** image value에만 bounded object-contain preview를 렌더하고 URL lifecycle을 `FileField` 한 곳에서 소유한다.
|
||||
- [x] **RED:** `CanPriceField renders the Audio numeric CAN price contract`; `CanPriceField connects its inline error and emits Audio-compatible values`를 import 부재로 실패시킨다.
|
||||
- [x] **GREEN:** 공통 numeric CAN field를 추가하고 Audio inline price markup을 교체하되 paid option reset과 request를 보존한다.
|
||||
- [x] **RED:** Community 최종 preview/media-first order, image 전 audio 부재·image 변경 시 stale audio 제거, 기본 0·blank 거부·항상 price 전송을 실패시킨다.
|
||||
- [x] **GREEN:** Community form state·render order·request를 최소 수정하고 multipart serializer 순서는 변경하지 않는다.
|
||||
- [x] **REFACTOR:** 불필요해진 domain price formatter wrapper를 제거하고 shared/domain focused test, full Gate, 375/768/1280 browser·visual QA와 독립 review를 통과한다.
|
||||
- **실행 명령:** `npm run test:run -- src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/can-price-field.test.tsx`; `npm run test:run -- src/features/audio-contents src/features/community-posts`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `npm run e2e:mock -- tests/e2e/community.spec.ts`; `git diff --check`.
|
||||
- **기대 결과:** 모든 명령 exit 0, crop/GIF 적용 전 최종 preview 0건, image 없는 audio input·stale audio multipart 0건, Community valid request의 price 누락 0건, Audio 가격 회귀 0건, object URL 미해제 0건.
|
||||
- **수동 확인:** mock mode 375px read-only 정책, 768px·1280px Community 생성의 initial/crop/applied/replacement/error 상태를 keyboard·가로 overflow·Korean text 기준으로 확인하고 QA browser/server/temp fixture를 모두 정리한다. 내부 관리자 화면이므로 별도 browser zoom 검증은 제외한다.
|
||||
|
||||
**P6-R5 진행 기록 (2026-08-04):**
|
||||
|
||||
- RED/GREEN: FileField preview·Blob URL·native input 동기화 11개, 공용 CAN field와 Audio/Community 음수·소수·상한·blank·`00` zero reset, Community media 순서·조건부 audio·stale state·request 회귀를 실패 확인 후 최소 수정했다.
|
||||
- 자동 Gate: `npm run test:run` — 83 files / 458 tests passed; `npm run lint`, `npm run typecheck`, `npm run build:dev`, `npm run build:prod`, `git diff --check` — exit 0. build의 500kB chunk warning은 기존 비차단 경고다.
|
||||
- 브라우저: `npm run e2e:mock -- tests/e2e/community.spec.ts` — Chromium/mobile Chrome 14/14 passed. 375px mobile read-only, 768px·1280px final GIF preview·conditional audio·제거·blank 가격·keyboard·overflow·axe를 확인했다. 사용자 결정에 따라 browser zoom 검증은 제외했다.
|
||||
- 리뷰: 목표·품질 review에서 문서 추적성과 Audio raw `00` zero reset blocker를 발견했다. `parsePrice(value) === 0`으로 보완해 Audio focused 9 files / 69 tests를 통과했고, 문서 상태·파일 지도·Decision Log를 정렬했다.
|
||||
- 최종 browser/visual: 768px·1280px Chromium/mobile Chrome에서 PNG crop/applied/replacement/error와 기존 GIF/audio/price 상태를 포함해 Community E2E 14/14를 통과했다. 4개 project/viewport evidence directory의 32개 표준 배율 PNG를 기능·디자인 시스템 및 CJK reviewer가 전수 확인해 PASS/HIGH, blocker 0건으로 판정했다. `캔`은 PRD의 정상 CAN 단위이며, 사용자 결정에 따라 browser zoom 검증은 제외했다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7. FanTalk vertical slice
|
||||
|
||||
@@ -305,19 +305,20 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
|---|---|---|
|
||||
| COMMUNITY-001 | 확정 | 선택 캐릭터의 게시글 목록 기반 조회·등록·수정·고정·비활성화를 제공한다. |
|
||||
| COMMUNITY-002 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
|
||||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. 생성 UI는 게시글 이미지, 오디오 파일, 내용 순서로 배치하고, 오디오 파일은 적용 완료된 이미지가 있을 때만 표시·선택할 수 있다. 이미지를 제거하거나 새 이미지 선택을 시작하면 기존 오디오 선택도 제거한다. |
|
||||
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
|
||||
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false`와 `isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
|
||||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 `0..99999` 정수 “캔” 단위를 사용한다. |
|
||||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 기본값 `0`, 빈 값 불가, `0..99999` 정수 “캔” 단위를 사용한다. 생성 UI는 가격을 항상 request에 포함한다. |
|
||||
| COMMUNITY-008 | 확정 | 커뮤니티 목록 API는 `isActive=true`인 게시글만 반환한다. request에는 활성 상태 query를 추가하지 않고 item에도 client-side 활성 filter를 적용하지 않는다. |
|
||||
| COMMUNITY-009 | 확정 | 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다. |
|
||||
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 서버의 active-only 목록에서 해당 게시글이 제외돼야 한다. |
|
||||
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioUrl`을 사용한다. |
|
||||
| COMMUNITY-012 | 확정 | 목록 GET은 `timezone` 없이 `page`, `size`를 사용하고 `data.totalCount`, `data.page`, `data.size`, `data.hasNext`, `data.items[]`를 소비한다. 전체 건수와 다음 page 여부는 서버 metadata를 그대로 사용한다. |
|
||||
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용하고 request에 필수 `content`, `isCommentAvailable`, `isAdult`와 optional `price`만 보낸다. |
|
||||
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용한다. OpenAPI의 `price`는 optional이지만 생성 UI는 필수 `content`, `isCommentAvailable`, `isAdult`, `price`를 항상 보내며 기본 가격은 `0`이다. |
|
||||
| COMMUNITY-014 | 확정 | 수정 multipart는 optional `postImage`와 필수 `request`만 허용한다. 수정에서 가격·첨부 audio 교체는 제공하지 않고, 고정은 `isFixed`, soft delete는 `isActive=false`로 처리한다. |
|
||||
| COMMUNITY-015 | 확정 | 생성·수정·고정·soft delete 성공은 `data=null`이므로 목록을 무효화·재조회하고 mutation 응답에 게시글 DTO가 있다고 가정하지 않는다. |
|
||||
| COMMUNITY-016 | 확정 | 생성 화면은 crop 적용 또는 GIF 검증이 끝난 실제 업로드 대상 이미지의 미리보기를 파일명과 함께 표시한다. crop 전 원본이나 준비 중 이미지를 최종 미리보기로 표시하지 않는다. |
|
||||
|
||||
### 8.6 FanTalk
|
||||
|
||||
@@ -361,13 +362,14 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
| FILE-006 | 확정 | 오디오 콘텐츠 cover는 `1:1` 고정 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px다. |
|
||||
| FILE-007 | 확정 | 커뮤니티 JPEG/PNG·시리즈·오디오 콘텐츠에서 새 image를 선택하면 업로드 전에 crop UI를 반드시 거친다. 커뮤니티 GIF는 예외다. |
|
||||
| FILE-008 | 확정 | crop UI는 pointer drag/pinch와 keyboard 방향키·확대/축소, 초기화, 결과 미리보기, 취소, 적용을 제공한다. cropper가 직접 조작을 제공하므로 별도 방향·zoom 버튼은 두지 않는다. |
|
||||
| FILE-009 | 확정 | optional 교체 파일 미전송은 기존 media 유지다. crop 취소도 기존 media를 변경하지 않는다. 기존 media 자체 제거는 별도 remove contract가 없어 범위 밖이다. |
|
||||
| FILE-009 | 확정 | 수정 화면에서 optional 교체 파일 미전송은 서버에 저장된 기존 media 유지다. 수정 crop 취소도 저장된 기존 media를 변경하지 않으며 기존 media 자체 제거는 별도 remove contract가 없어 범위 밖이다. Community 생성 폼의 아직 저장되지 않은 로컬 이미지는 `COMMUNITY-003`에 따라 새 이미지 선택 시작 시 제거되고 crop 취소 후 복원하지 않는다. |
|
||||
| FILE-010 | 확정 | 캐릭터 image는 JPEG/PNG만 허용하고 `1:1` 고정 aspect ratio로 크롭하며 결과의 최대 가로·세로는 800px다. |
|
||||
| FILE-011 | 확정 | 커뮤니티 GIF는 crop하지 않는다. crop Dialog를 열지 않고 원본 비율과 animation을 유지한 File을 등록한다. |
|
||||
| FILE-012 | 확정 | JPEG/PNG crop 결과는 선택된 원본 crop 영역의 pixel 크기보다 확대하지 않는다. resource별 800px/1,000px 값은 최대 출력 폭이며 작은 원본은 가능한 원본 크기로 출력한다. |
|
||||
| FILE-013 | 확정 | 커뮤니티 첨부 audio는 오디오 콘텐츠와 동일하게 MP3(`.mp3`, `audio/mpeg`), AAC(`.aac`, `audio/aac`), M4A(`.m4a`, `audio/mp4` 또는 `audio/x-m4a`), 최대 `1,024,000,000 bytes`, 재생 길이 제한 없음 정책을 사용하고 WAV는 거부한다. `audio/x-m4a`는 `.m4a`와 실제 container/codec 검증이 일치할 때만 허용한다. |
|
||||
| FILE-014 | 확정 | 커뮤니티 GIF의 원본 가로가 800px을 초과하면 등록을 거부한다. client에서 제출 전에 차단하고 server도 같은 제한을 검증한다. GIF를 축소·crop·재인코딩하지 않는다. |
|
||||
| FILE-015 | 확정 | Series crop 결과의 세로 pixel은 `round(width × 297 ÷ 210)`으로 계산한다. 최대 폭에서는 1,000×1,414px이며 비율 검증은 계산된 세로값 기준 1px 이내 오차를 허용한다. |
|
||||
| FILE-016 | 확정 | 공통 FileField는 controlled value가 이미지 File이면 실제 업로드 대상의 미리보기를 표시하고 교체·제거·unmount 시 preview Blob URL을 해제한다. 오디오 등 비이미지 File은 기존 파일명 표시를 유지한다. |
|
||||
|
||||
### 8.9 개발 전용 Mock Preview
|
||||
|
||||
@@ -938,3 +940,4 @@ vertical slice와 남은 수동 QA에서 검증한다. `EXT-006`은 현재 확
|
||||
| 2026-08-03 | 공통 `AdminAudioPlayer`의 native media 동작과 단일 재생·오류 계약은 유지하고, Plyr·Media Chrome의 audio-only control 배치를 참고한 compact 가로형 UI로 개선한다. 플레이어 내부 image·video 영역과 새 외부 라이브러리·waveform은 추가하지 않으며 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일하게 적용한다. |
|
||||
| 2026-08-04 | Audio form은 tags를 chip으로 입력하고 Enter/comma 추가와 remove button 삭제를 제공하되 payload는 comma-separated string으로 유지한다. 가격은 숫자만 표시·저장하는 `0..99999` 정수이며 0은 무료다. 가격이 0보다 클 때만 purchase option, preview 생성·시간, point 사용 가능 여부를 보이고, 0 전환 시 `purchaseOption=BOTH`, `isGeneratePreview=false`, `isPointAvailable=false`, preview times=`null`로 초기화한다. `limited`는 문서화되지 않은 NullableInt32이므로 UI를 제거하고 `null`을 보내며, `languageCode` UI는 제거하고 `null`, 독립 `isOnlyRental` UI는 제거하고 `false`를 보낸다. preview 시작·종료는 오디오 duration offset으로 text 입력하며 완전한 `HH:MM:SS`만 안내하고 request에는 입력한 `HH:mm:ss` 값을 그대로 보낸다. 예약 일시는 예약 공개에서만 native `datetime-local`로 렌더링하고 custom `showPicker()`는 사용하지 않는다. native radio/checkbox는 하나의 선택 카드 시각 문법을 사용한다. |
|
||||
| 2026-08-04 | Series 생성 form은 image 입력을 최상단에 배치하고 keyword를 Audio tags와 같은 공용 `TagInput` chip UI로 입력한다. Enter/comma로 확정하고 remove button으로 삭제하며 server request는 기존 comma-separated `keyword` 문자열 계약을 유지한다. |
|
||||
| 2026-08-04 | Community 생성 form은 최종 적용 이미지 preview를 표시하고 이미지 → 조건부 오디오 → 내용 → 가격 순서로 배치한다. 새 이미지 선택 시작 시 아직 저장되지 않은 기존 로컬 이미지와 오디오를 제거하며 crop 취소 후 로컬 이미지를 복원하지 않는다. 가격은 Audio와 같은 기본 0·blank/음수/소수/상한 거부 계약으로 항상 request에 포함한다. 내부 관리자 화면이므로 P6-R5 browser QA에서 별도 zoom 검증은 제외하고 375/768/1280 표준 viewport를 확인한다. |
|
||||
|
||||
Reference in New Issue
Block a user