897 lines
82 KiB
Markdown
897 lines
82 KiB
Markdown
# AI 캐릭터 관리자 웹 PRD
|
||
|
||
## 문서 정보
|
||
|
||
| 항목 | 내용 |
|
||
|---|---|
|
||
| 문서 상태 | OpenAPI 반영 구현 기준 |
|
||
| 작성일 | 2026-07-25 |
|
||
| 최종 수정일 | 2026-07-28 |
|
||
| 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 |
|
||
| 구현 대상 | React + TypeScript + Vite SPA |
|
||
| UI 기반 | Tailwind CSS + shadcn/ui |
|
||
| 관련 계획 | [plan-task.md](./plan-task.md) |
|
||
| 정규화 계약 | [api-contract.openapi.json](./api-contract.openapi.json) |
|
||
|
||
### 상태 표기
|
||
|
||
- **확정**: 이번 인터뷰에서 합의되어 구현 기준으로 사용할 사항
|
||
- **미결**: 프론트엔드 제품·UI 또는 운영 정책이 결정되지 않아 후속 인터뷰나 UI 검토가 필요한 사항
|
||
- **외부 의존**: 프론트엔드가 결정할 사항이 아니며, 해당 기능의 network integration 전에 백엔드가 제공해야 하는 계약
|
||
- **권고**: 미결 사항에 대한 현재 추천안이며, 확정 전에는 계약으로 간주하지 않음
|
||
- **제외**: 현재 OpenAPI 또는 릴리스 범위에 포함하지 않으며, 다시 포함할 조건을 별도로 기록한 사항
|
||
|
||
### 문서 유지보수 원칙
|
||
|
||
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, OpenAPI 소비 주의사항, 미결·외부 의존 사항을 함께 갱신한다.
|
||
2. 구현 범위나 순서가 바뀌면 같은 디렉터리의 `plan-task.md`도 같은 변경에서 갱신한다.
|
||
3. endpoint, query, multipart part, request/response field, required 여부, status와 오류 응답은 OpenAPI 계약을 단일 진실 원천으로 사용한다. OpenAPI에 표현되지 않는 제품·UI·운영 정책은 이 문서가 소유한다.
|
||
4. 미결 사항과 외부 의존 계약은 추측으로 구현하지 않는다. **미결**에는 추천안을, **외부 의존**에는 제공 주체와 영향을 함께 기록한다.
|
||
5. 완료된 미결 사항과 제공 완료된 외부 의존 계약은 결정일과 결정 내용을 “결정 기록”에 추가한 뒤 관련 수용 기준까지 갱신한다.
|
||
6. goal 실행의 objective·순서·완료 증거·범위는 `plan-task.md`의 `Phase Goal`과 `Goal 실행`을 기준으로 한다. goal 수행 중 제품 결정이 바뀌면 이 문서의 결정 기록과 요구사항을 먼저 갱신한다.
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘텐츠·시리즈·커뮤니티·FanTalk·댓글 활동을 수행하도록 관리하는 독립 관리자 웹을 만든다.
|
||
|
||
관리자는 ADMIN 권한으로 로그인한 뒤 AI 캐릭터를 선택한다. 이후의 모든 생성·수정·비활성화 작업은 선택한 `characterId`와 연결된 AI 캐릭터 크리에이터의 활동으로 저장된다. 관리자가 AI 캐릭터 계정으로 직접 로그인하거나 토큰을 교환하는 방식은 사용하지 않는다.
|
||
|
||
이번 문서는 요구사항과 구현 계획만 정의한다. 애플리케이션 코드는 이번 단계에서 수정하지 않는다.
|
||
|
||
## 2. Problem Statement
|
||
|
||
현재 AI 캐릭터는 연결된 `creator(memberKind = AI_CHARACTER)`를 가지지만, 운영자가 캐릭터의 전체 활동을 한곳에서 관리할 독립 UI가 없다.
|
||
|
||
운영자는 다음 문제를 해결해야 한다.
|
||
|
||
- 캐릭터와 연결된 creator의 관계를 이해하지 않아도 안전하게 캐릭터를 관리해야 한다.
|
||
- 여러 캐릭터의 리소스가 섞이지 않도록 선택한 캐릭터 문맥 안에서만 작업해야 한다.
|
||
- 오디오 콘텐츠를 관리자 화면에서 즉시 재생해 검수해야 한다.
|
||
- 예약 공개, 시리즈 연결과 순서, 게시글 고정, FanTalk 단일 답변 같은 도메인 규칙을 UI에서 명확히 안내해야 한다.
|
||
- 데스크톱에서는 전체 운영을 수행하고 모바일에서는 조회와 긴급 응대가 가능해야 한다.
|
||
- 정식 OpenAPI 계약과 기존 PRD·구현 계획의 잘못되었거나 누락된 API 전제를 구현 전에 바로잡아야 한다.
|
||
|
||
## 3. Goals
|
||
|
||
### 3.1 제품 목표
|
||
|
||
- AI 캐릭터 목록 검색, 상세 조회, 생성, 수정, 비활성화를 제공한다.
|
||
- 선택한 캐릭터 문맥에서 오디오 콘텐츠, 시리즈, 커뮤니티 게시글을 관리한다.
|
||
- 선택한 캐릭터로 FanTalk에 한 번 답변할 수 있게 한다. 기존 답변 수정은 OpenAPI에 수정 endpoint가 추가된 뒤 활성화한다.
|
||
- 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다.
|
||
- 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다.
|
||
- 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다.
|
||
|
||
### 3.2 UX 목표
|
||
|
||
- 캐릭터 선택 이후 모든 화면에서 현재 대상 캐릭터를 분명히 표시한다.
|
||
- 데이터가 많은 운영 화면을 조밀하지만 빠르게 탐색할 수 있게 한다.
|
||
- 업로드, 저장, 비활성화, 연결 해제 등 비동기 작업의 상태와 결과를 즉시 피드백한다.
|
||
- 데스크톱·태블릿에서는 전체 기능을, 모바일에서는 합의된 조회·응대 기능을 제공한다.
|
||
- 기본적인 키보드 탐색, 포커스 표시, 입력 레이블, 오류 연결을 보장한다.
|
||
|
||
## 4. Non-Goals
|
||
|
||
- 일반 사용자 또는 사람 크리에이터를 관리하는 기능
|
||
- 관리자 계정을 AI 캐릭터 계정으로 전환하거나 AI 캐릭터 JWT를 발급하는 기능
|
||
- refresh token 또는 자동 access token 갱신
|
||
- 비활성 리소스 복원
|
||
- hard delete
|
||
- FanTalk 답변 삭제 또는 두 번째 답변 추가
|
||
- WAV 업로드
|
||
- 오디오 최대 재생 길이 제한
|
||
- 모바일에서 캐릭터·오디오·시리즈·커뮤니티 리소스 생성/수정/비활성화, 파일 업로드, 시리즈 연결/순서 변경
|
||
- 다국어 UI
|
||
- 초기 릴리스의 다크 모드, 테마 전환 버튼과 시스템 색상 테마 연동
|
||
- 정식 WCAG 2.2 AA 인증 또는 외부 접근성 감사
|
||
- 백엔드가 담당할 creator 생성·프로필 동기화의 조건부 정책 변경
|
||
- 비활성 ID에 대한 상세 GET 반환 여부와 오류 status 등 백엔드 조회 정책 결정
|
||
- 실제 API의 404를 감지해 mock 응답으로 자동 전환하는 production fallback
|
||
|
||
## 5. Target Users
|
||
|
||
### 5.1 주 사용자
|
||
|
||
- AI 캐릭터와 해당 캐릭터의 콘텐츠를 운영하는 ADMIN
|
||
- 콘텐츠 공개 상태와 오디오 품질을 확인하는 운영 담당자
|
||
- 모바일에서 댓글 또는 FanTalk에 긴급 응대하는 운영 담당자
|
||
|
||
### 5.2 권한
|
||
|
||
- JWT claim의 role과 현재 DB role이 모두 ADMIN이어야 한다.
|
||
- 비ADMIN JWT는 403으로 차단한다.
|
||
- JWT claim은 ADMIN이지만 현재 DB role이 비ADMIN인 stale claim도 403으로 차단한다.
|
||
- 관리자는 선택한 캐릭터의 creator 권한으로 리소스를 작성하지만 인증 주체는 계속 ADMIN이다.
|
||
|
||
## 6. 핵심 사용자 흐름
|
||
|
||
1. 관리자가 이메일과 비밀번호로 로그인한다.
|
||
2. AI 캐릭터 목록에서 이름으로 검색하고 캐릭터를 선택한다.
|
||
3. 캐릭터 워크스페이스의 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk 탭을 이동한다.
|
||
4. 활성 캐릭터라면 데스크톱·태블릿에서 리소스를 생성·수정·비활성화한다.
|
||
5. 오디오 플레이어로 캐릭터가 올린 오디오를 검수한다.
|
||
6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변한다. 기존 답변 수정은 수정 계약이 제공된 뒤 추가한다.
|
||
7. 오디오 또는 커뮤니티 댓글에 캐릭터 명의로 댓글·답글을 작성하거나 운영 정책에 따라 삭제한다.
|
||
8. 모바일에서는 리소스를 조회하고 오디오를 재생하며 댓글과 FanTalk 답변을 관리한다.
|
||
9. JWT가 만료되거나 폐기되면 인증 정보를 지우고 로그인 화면으로 이동한다.
|
||
|
||
## 7. 정보 구조와 라우팅
|
||
|
||
### 7.1 화면 구조
|
||
|
||
```text
|
||
/login
|
||
/ai-characters
|
||
/ai-characters/new
|
||
/ai-characters/:characterId
|
||
/profile
|
||
/audio-contents
|
||
/audio-contents/new
|
||
/audio-contents/:contentId
|
||
/audio-contents/:contentId/edit
|
||
/series
|
||
/series/new
|
||
/series/:seriesId
|
||
/series/:seriesId/edit
|
||
/series/orders
|
||
/community-posts
|
||
/community-posts/new
|
||
/fan-talks
|
||
```
|
||
|
||
라우트 문자열은 구현 시 확정하되 다음 원칙은 고정한다.
|
||
|
||
- 캐릭터를 선택하지 않은 전역 화면은 로그인과 캐릭터 목록·생성뿐이다.
|
||
- 캐릭터 리소스 화면은 모두 URL에 `characterId`를 포함한다.
|
||
- 계약이 제공하는 `searchTerm`, `search_word`, `page`, `size`와 제품 filter 상태는 URL query에 보존한다. 계약에 없는 server filter를 client 전체 결과 filter처럼 가장하지 않는다.
|
||
- 상세 GET이 제공되는 주요 리소스의 목록과 상세 화면은 새로고침과 직접 링크 진입이 가능해야 한다.
|
||
- 커뮤니티 게시글은 별도 상세·수정 route 없이 목록 행/카드에서 여는 Sheet를 사용한다. 페이지 새로고침은 목록을 다시 조회한다.
|
||
- FanTalk도 별도 상세 GET이 없으므로 목록 item을 source로 Sheet 또는 panel을 열고 답변을 작성한다.
|
||
- 존재하지 않거나 다른 캐릭터 소유인 하위 리소스는 서버 결과에 따라 오류 화면으로 처리한다.
|
||
|
||
### 7.2 캐릭터 워크스페이스
|
||
|
||
- 상단에 캐릭터 이미지, 이름, 활성 상태, `characterId`를 항상 표시한다.
|
||
- 1차 탭은 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk로 구성한다.
|
||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터 데이터를 받은 경우에는 읽기 전용 배너를 표시하고 모든 변경 진입점을 숨기거나 비활성화한다. soft delete mutation 성공 직후에는 이 규칙보다 `CHAR-014`의 목록 이동을 우선한다. 비활성 ID의 상세 GET 반환 여부는 프론트엔드가 규정하지 않는다.
|
||
- 브레드크럼으로 캐릭터 목록과 현재 리소스 위치를 표시한다.
|
||
|
||
## 8. 기능 요구사항
|
||
|
||
### 8.1 인증과 세션
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| AUTH-001 | 확정 | 독립 관리자 웹에 자체 로그인 화면을 제공한다. |
|
||
| AUTH-002 | 확정 | 로그인 입력은 이메일과 비밀번호다. |
|
||
| AUTH-003 | 확정 | ADMIN만 보호 라우트에 접근할 수 있다. |
|
||
| AUTH-004 | 확정 | refresh token과 자동 갱신을 사용하지 않는다. |
|
||
| AUTH-005 | 확정 | 401 수신 시 보관 중인 인증 상태를 제거하고 로그인으로 이동한다. |
|
||
| AUTH-006 | 확정 | 403 수신 시 접근 거부 화면을 표시하며 권한이 필요한 작업을 실행하지 않는다. |
|
||
| AUTH-007 | 확정 | 모든 API 요청에 `Accept-Language: ko`를 보낸다. |
|
||
| AUTH-008 | 확정 | 로그인은 `POST /admin/member/login`에 email/password JSON body를 보낸다. |
|
||
| AUTH-009 | 확정 | 로그인 성공 시 응답 `data.token`과 `data.role`을 받고 role은 `ADMIN`이어야 한다. |
|
||
| AUTH-010 | 확정 | 보호 API와 로그아웃에는 `Authorization: Bearer {jwt-token}` header를 사용한다. |
|
||
| AUTH-011 | 확정 | 관리자 전용 로그아웃 endpoint는 없으며 공통 `POST /member/logout`을 body 없이 호출한다. |
|
||
| AUTH-012 | 확정 | 로그인 성공 시 JWT와 ADMIN role을 `sessionStorage`에만 저장한다. 같은 탭의 새로고침에서는 session을 복원하고 탭 종료 시 브라우저 동작에 따라 제거한다. `localStorage`, IndexedDB, cookie에는 저장하지 않는다. |
|
||
| AUTH-013 | 확정 | `POST /member/logout`이 성공하거나 네트워크·비2xx 오류로 실패해도 프론트엔드는 `sessionStorage` 인증 정보를 제거하고 로그인 화면으로 이동한다. 실패 시 서버 로그아웃 확인 실패 경고를 표시하며 session을 복원하지 않는다. |
|
||
|
||
### 8.2 AI 캐릭터
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| CHAR-001 | 확정 | 이름 검색, 페이지네이션이 있는 캐릭터 목록을 제공한다. |
|
||
| CHAR-002 | 확정 | 캐릭터 상세, 생성, 수정, 비활성화를 제공한다. |
|
||
| CHAR-003 | 확정 | 생성 multipart는 필수 `image`와 필수 `request` part를 사용한다. `request`의 필수 입력은 `name`, `systemPrompt`, `description`이고 나머지 필드는 OpenAPI의 optional/nullable 정의를 따른다. |
|
||
| CHAR-004 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. 최초 활성 상태는 백엔드가 결정한다. |
|
||
| CHAR-005 | 확정 | `externalCharacterId`는 존재하지 않는 값이므로 모든 요청·응답·UI에서 제거한다. |
|
||
| CHAR-006 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| CHAR-007 | 확정 | 복원·hard delete는 제공하지 않는다. 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 해당 workspace는 read-only로 처리한다. soft delete mutation 성공 직후에는 `CHAR-014`를 우선하며, 비활성 ID 상세 조회 정책은 백엔드 범위다. |
|
||
| CHAR-008 | 확정 | 캐릭터 생성 시 연결된 `creator(memberKind = AI_CHARACTER)` 생성은 백엔드가 함께 수행한다. |
|
||
| CHAR-009 | 확정 | 캐릭터 이름·설명·이미지 변경 시 creator의 nickname·introduce·profile image 동기화는 현재 백엔드가 수행한다. |
|
||
| CHAR-010 | 확정 | creator 상황에 따른 조건부 생성·동기화는 다음 백엔드 범위이며 현재 UI 범위가 아니다. |
|
||
| CHAR-011 | 제외 | 현 OpenAPI의 Character 목록·상세 응답에는 `creatorMemberId`, `creatorNickname`이 없다. 계약에 추가되기 전에는 creator 정보 UI와 DTO를 만들지 않는다. |
|
||
| CHAR-012 | 외부 의존 | OpenAPI는 `searchTerm`을 생략하면 활성 목록을 반환한다고 명시하지만, `searchTerm` 지정 시에는 “레거시 검색”만 명시해 active-only 여부가 불명확하다. 검색 결과 보장이 추가되기 전에도 client 활성 filter는 만들지 않고 서버 반환값을 표시한다. |
|
||
| CHAR-013 | 외부 의존 | `originalWorkId`는 생성·수정 request에서 optional nullable이므로 미선택 시 key 생략과 `null`이 모두 계약상 가능하다. 원작 검색 선택기에 필요한 lookup API는 OpenAPI에 없으므로 제공 전에는 원작 선택 network integration을 구현하지 않는다. |
|
||
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||
| CHAR-015 | 확정 | 목록은 `searchTerm`, `page`, `size`를 사용하고 `data.totalCount`, `data.content[]`를 소비한다. 목록 ID field는 `id`다. |
|
||
| CHAR-016 | 확정 | 생성·수정 성공은 `data=null`이므로 생성 후 목록을 무효화해 이동하고, 수정 후 기존 `characterId` 상세와 목록을 다시 조회한다. 생성 응답에서 새 ID나 상세 DTO를 추정하지 않는다. |
|
||
| CHAR-017 | 확정 | 상세의 `characterUUID`는 OpenAPI에 존재하는 읽기 전용 값이며, 제거된 `externalCharacterId`와 다른 field다. |
|
||
| CHAR-018 | 확정 | 생성 request에는 `region`이 있지만 수정 request에는 없다. 수정 화면에서 region을 읽기 전용으로 표시하고 update payload에 보내지 않는다. |
|
||
|
||
#### 캐릭터 생성·수정 폼
|
||
|
||
- 이름, system prompt와 설명은 명시적인 레이블과 필드 오류 영역을 가진다.
|
||
- OpenAPI의 optional scalar(`age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType`)와 배열(`tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`)을 생성 form에서 편집할 수 있게 한다. `originalWorkId`는 lookup 계약 제공 후 선택기로 편집하고, 수정 request에 없는 `region`은 수정 화면에서 읽기 전용이다.
|
||
- 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다.
|
||
- 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다.
|
||
- 원작 lookup 계약이 제공되면 이름 검색형 Combobox로 선택한다. 미선택은 허용하고 serializer는 key 생략 또는 `null` 중 한 가지 canonical form을 contract test로 고정한다.
|
||
- 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다.
|
||
- 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다.
|
||
|
||
### 8.3 오디오 콘텐츠
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·제목 검색·상세·생성·수정·비활성화를 제공한다. |
|
||
| AUDIO-002 | 제외 | 현 OpenAPI 목록·상세에는 `OPEN`, `SCHEDULED` status field가 없고 목록 status query도 없다. 상태 badge와 server status filter는 계약에 추가되기 전에는 제공하지 않는다. |
|
||
| AUDIO-003 | 확정 | 공개 예약은 생성 request의 nullable `releaseDate`와 `timezone`으로 표현한다. 목록·상세에서는 OpenAPI가 반환한 `releaseDate` 문자열을 그대로 표시하고 별도 status enum을 만들지 않는다. |
|
||
| AUDIO-004 | 확정 | 프론트엔드는 `releaseDate`로 `OPEN`·`SCHEDULED` 같은 API status를 재계산하거나 DTO에 추가하지 않는다. |
|
||
| AUDIO-005 | 제외 | 현 OpenAPI에 status filter가 없으므로 status query를 보내거나 현재 page를 client에서 status별로 거르지 않는다. |
|
||
| AUDIO-006 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||
| AUDIO-007 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| AUDIO-008 | 확정 | 공개 방식은 “지금 즉시 공개”와 “예약 공개” 두 선택 버튼으로 제공한다. |
|
||
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null`과 `timezone="Asia/Seoul"`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
|
||
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. |
|
||
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하고 생성 API에는 `yyyy-MM-dd HH:mm` 형식의 `releaseDate`와 `timezone="Asia/Seoul"`을 보낸다. UTC `Z` 값으로 변환하지 않는다. |
|
||
| AUDIO-012 | 확정 | 생성 multipart의 `contentFile`, `coverImage`, `request`는 필수다. 수정은 `coverImage`와 `request`만 허용하므로 오디오 원본 파일 교체 UI를 제공하지 않는다. |
|
||
| AUDIO-013 | 확정 | 오디오 확장자는 `.mp3`, `.aac`, `.m4a`를 허용한다. WAV는 허용하지 않는다. |
|
||
| AUDIO-014 | 확정 | canonical MIME은 `audio/mpeg`, `audio/aac`, `audio/mp4`다. |
|
||
| AUDIO-015 | 확정 | 오디오 파일의 운영 기준 최대 크기는 1,024MB이고 최대 재생 길이는 제한하지 않는다. |
|
||
| AUDIO-016 | 확정 | 확장자와 MIME만 신뢰하지 않고 실제 컨테이너·코덱 검증은 백엔드가 수행해야 한다. |
|
||
| AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. |
|
||
| AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. |
|
||
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 0 이상의 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
|
||
| AUDIO-020 | 확정 | Audio 생성·수정 request에는 `seriesIds`가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다. |
|
||
| AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. |
|
||
| AUDIO-022 | 외부 의존 | 오디오 목록 request에는 활성 상태 query와 status query가 없고 응답에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side 활성 filter는 만들지 않는다. |
|
||
| AUDIO-023 | 확정 | 최대 크기는 decimal 1,024MB인 `1,024,000,000 bytes` 이하이며 `1,024,000,001 bytes`부터 거부한다. 오디오 콘텐츠와 커뮤니티 첨부 audio에 동일하게 적용한다. |
|
||
| AUDIO-024 | 확정 | `audio/x-m4a`는 `.m4a` 파일에 한해 호환 MIME으로 허용한다. 실제 MP4/M4A 컨테이너·코덱 검증을 통과해야 하며 다른 확장자와의 조합은 거부한다. |
|
||
| AUDIO-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||
| AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
|
||
| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 유효한 `themeId`가 필수다. OpenAPI의 기본값 `0`은 binding 기본값일 뿐 domain에서 유효하지 않으므로 프론트엔드는 테마 선택을 강제한다. |
|
||
| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답 `data[]`의 `id`, `theme`, `image`를 사용한다. |
|
||
| AUDIO-029 | 확정 | 목록 검색 query는 `search_word`이며 검색어가 2자 이상일 때만 보낸다. 목록 응답은 `data.totalCount`, `data.items[]`를 사용한다. |
|
||
| AUDIO-030 | 확정 | 상세 GET은 필수 `timezone=Asia/Seoul` query를 보낸다. |
|
||
| AUDIO-031 | 확정 | 생성 성공은 `data.contentId`를 사용해 상세로 이동할 수 있다. 수정·soft delete 성공은 `data=null`이므로 기존 ID 기준 cache를 무효화한다. |
|
||
| AUDIO-032 | 확정 | 생성 `request`는 필수 `title`, `detail`, `tags`, `price`와 OpenAPI의 optional field만 보낸다. 수정은 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable`만 변경할 수 있다. |
|
||
| AUDIO-033 | 확정 | 생성 form은 OpenAPI의 `purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 계약 enum·type과 default에 맞춰 제공한다. 계약에 없는 추가 상관관계 validation은 만들지 않는다. |
|
||
|
||
현 수정 계약에는 `releaseDate`, `timezone`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
|
||
|
||
#### 관리자 오디오 플레이어
|
||
|
||
- 재생/일시정지, 탐색, 현재/전체 시간, 볼륨, 배속을 제공한다.
|
||
- 명시적인 다운로드 버튼은 제공하지 않는다.
|
||
- 여러 행의 플레이어가 동시에 재생되지 않게 현재 재생 항목을 단일화한다.
|
||
- 재생 오류는 원인을 signed URL 만료로 구분하지 않고 “오디오를 재생할 수 없습니다”와 수동 재시도·페이지 새로고침 안내를 표시한다.
|
||
- media error 자체로 상세·목록 API를 자동 재조회하거나 `play()`를 자동 재호출하지 않는다.
|
||
- signed URL은 로그, 분석 이벤트, 영구 저장소에 기록하지 않는다.
|
||
- 커뮤니티 첨부 오디오는 URL 갱신만을 목적으로 요청하지 않는다. 사용자가 페이지를 새로고침하거나 mutation 후 cache 무효화 등 일반 목록 lifecycle로 재조회된 경우 새 응답의 URL을 사용한다. 별도 상세 조회나 URL 재발급 호출을 추가하지 않는다.
|
||
|
||
### 8.4 시리즈
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| SERIES-001 | 확정 | 목록·상세·생성·수정·비활성화, 콘텐츠 연결·해제, 시리즈 순서 변경을 제공한다. |
|
||
| SERIES-002 | 확정 | 상태 enum은 `PROCEEDING`(연재중), `SUSPEND`(휴재중), `COMPLETE`(완결)이다. `OPEN`은 유효하지 않다. |
|
||
| SERIES-003 | 확정 | 생성 시 state를 선택하거나 보내지 않는다. 성공 응답은 `data=null`이므로 생성 후 목록으로 이동해 서버가 결정한 state를 다시 조회한다. |
|
||
| SERIES-004 | 확정 | 수정 시 state를 선택할 수 있으며 선택하지 않으면 필드를 생략해 이전 상태를 유지한다. |
|
||
| SERIES-005 | 확정 | 연재 요일 enum은 `SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `RANDOM`이다. |
|
||
| SERIES-006 | 확정 | `RANDOM`은 다른 요일과 함께 보낼 수 없다. 값은 RANDOM 단독 또는 하나 이상의 실제 요일 목록이어야 한다. |
|
||
| SERIES-007 | 외부 의존 | 생성 시 유효한 `genreId`가 필요하고 OpenAPI 기본값 `0`은 domain에서 유효하지 않다. 장르 이름 검색 API는 OpenAPI에 없으므로 제공 전에는 장르 선택 network integration과 Series 생성을 완료할 수 없다. |
|
||
| SERIES-008 | 확정 | `data.totalCount`, `data.items[]`를 page별로 읽어 서버가 반환한 시리즈 전체를 별도 순서 변경 모드에 표시하고 최종 순서의 모든 ID를 `{ "ids": [...] }`로 전송한다. active-only 여부는 `SERIES-012` 계약 제공 후 검증한다. |
|
||
| SERIES-009 | 확정 | drag-and-drop 외에 키보드와 위/아래 버튼으로 순서를 바꿀 수 있어야 한다. |
|
||
| SERIES-010 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`, 복원과 hard delete는 제공하지 않는다. |
|
||
| SERIES-011 | 외부 의존 | 장르 이름 검색 endpoint·DTO는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
|
||
| SERIES-012 | 외부 의존 | 시리즈 목록 request에는 활성 상태 query가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 list item의 `isActive`를 client에서 숨기는 방식으로 대체하지 않는다. |
|
||
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||
| SERIES-014 | 확정 | 생성 multipart의 `image`와 `request`는 필수다. 생성 request는 `keyword` 단일 문자열을 사용하며 `keywords` 배열을 보내지 않는다. |
|
||
| SERIES-015 | 확정 | 목록은 enum `publishedDaysOfWeek`, `genreId`, enum `state`를 사용하지만 상세는 표시용 문자열 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state`를 사용한다. 상세 표시 문자열을 update enum으로 재사용하지 않는다. |
|
||
| SERIES-016 | 확정 | 연결 후보는 `GET .../contents/search?search_word=...`, 연결은 `{ "contentIdList": [...] }`, 해제는 body 없는 DELETE를 사용한다. |
|
||
| SERIES-017 | 외부 의존 | 상세 응답에는 update에 필요한 `genreId`, enum `publishedDaysOfWeek`, enum `state`가 없다. 직접 링크에서도 안전하게 수정 form을 초기화할 edit DTO 또는 별도 mapping 계약이 제공되기 전에는 상세 표시 문자열을 역변환하지 않고 수정 network integration을 완료하지 않는다. |
|
||
| SERIES-018 | 확정 | 생성 request의 `keyword`는 수정 request에 없다. 수정 화면에서 상세의 `keywords`를 읽기 전용으로 표시하고 update payload에 보내지 않는다. |
|
||
|
||
#### 시리즈 콘텐츠 연결
|
||
|
||
- 현재 연결 콘텐츠는 `page`, `size`로 조회한다. 현 계약에는 연결 목록 검색 query가 없다.
|
||
- 연결 후보는 `.../contents/search?search_word=...`의 반환값을 사용하며 선택 캐릭터·해당 시리즈 문맥을 벗어난 별도 Audio 후보 endpoint를 만들지 않는다.
|
||
- 이미 연결된 콘텐츠를 중복 연결하지 않는다.
|
||
- 연결 해제 전 대상 제목과 영향을 확인한다.
|
||
- 연결/해제 성공 후 시리즈 상세와 콘텐츠 목록을 함께 갱신한다.
|
||
|
||
### 8.5 커뮤니티 게시글
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| COMMUNITY-001 | 확정 | 선택 캐릭터의 게시글 목록 기반 조회·등록·수정·고정·비활성화를 제공한다. |
|
||
| COMMUNITY-002 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
|
||
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
|
||
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false`와 `isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
|
||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 0 이상의 정수 “캔” 단위를 사용한다. |
|
||
| COMMUNITY-008 | 외부 의존 | 커뮤니티 목록 request에는 활성 상태 query가 없고 item에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side filter는 만들지 않는다. |
|
||
| COMMUNITY-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=Asia/Seoul`, `page`, `size`를 사용하고 `data`의 게시글 배열을 소비한다. 응답에 `totalCount`, `page`, `hasNext`가 없으므로 전체 건수·마지막 page를 추정하지 않는다. |
|
||
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용하고 request에 필수 `content`, `isCommentAvailable`, `isAdult`와 optional `price`만 보낸다. |
|
||
| COMMUNITY-014 | 확정 | 수정 multipart는 optional `postImage`와 필수 `request`만 허용한다. 수정에서 가격·첨부 audio 교체는 제공하지 않고, 고정은 `isFixed`, soft delete는 `isActive=false`로 처리한다. |
|
||
| COMMUNITY-015 | 확정 | 생성·수정·고정·soft delete 성공은 `data=null`이므로 목록을 무효화·재조회하고 mutation 응답에 게시글 DTO가 있다고 가정하지 않는다. |
|
||
|
||
### 8.6 FanTalk
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| FANTALK-001 | 확정 | 기본 목록은 backend가 반환한 순서를 유지한다. 현 계약에 sort query가 없으므로 client가 page 사이의 최신순을 재정렬하지 않는다. |
|
||
| FANTALK-002 | 외부 의존 | 전체·미답변·답변 완료 server filter query가 OpenAPI에 없다. 전체 결과 filter 계약이 제공되기 전에는 현재 page만 거르는 filter를 완성 기능으로 제공하지 않는다. |
|
||
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
|
||
| FANTALK-004 | 외부 의존 | 답변이 있으면 추가 작성은 UI에서 차단한다. 기존 답변 수정 endpoint는 OpenAPI에 없으므로 계약 제공 전에는 수정 network integration을 구현하지 않는다. |
|
||
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
|
||
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 목록 조회와 답변 작성을 지원한다. 답변 수정은 수정 계약이 제공된 뒤 같은 viewport 범위에 추가한다. |
|
||
| FANTALK-007 | 외부 의존 | 목록 GET과 답변 POST는 제공됐다. 별도 상세 GET, 답변 수정 endpoint·DTO, 답변 상태 filter와 sort 계약은 백엔드가 제공해야 하며 제공 전에는 해당 network integration을 구현하지 않는다. |
|
||
| FANTALK-008 | 외부 의존 | 답변 1개 불변식의 원자적 강제와 중복 생성의 정확한 비2xx status/message key는 백엔드가 결정·제공해야 한다. |
|
||
| FANTALK-009 | 확정 | 목록은 `page`, `size`를 사용하고 `data.fanTalkCount`, `data.fanTalks`, `data.page`, `data.size`, `data.hasNext`를 소비한다. 각 item의 `creatorReplies[]`로 답변 유무를 판단한다. |
|
||
| FANTALK-010 | 확정 | 답변 작성은 `{ "content": string }`을 보내고 성공 응답의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. |
|
||
| FANTALK-011 | 확정 | 별도 상세 endpoint가 없으므로 목록 item을 source로 collection Sheet 또는 panel을 열며 `/fan-talks/:fanTalkId` 직접 route를 만들지 않는다. |
|
||
|
||
### 8.7 댓글과 답글
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| COMMENT-001 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글에 댓글 영역을 제공한다. |
|
||
| COMMENT-002 | 확정 | 구조는 루트 댓글과 그 댓글의 직접 답글까지 정확히 2단계다. 답글의 답글은 허용하지 않는다. |
|
||
| COMMENT-003 | 확정 | AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정·soft delete할 수 있다. |
|
||
| COMMENT-004 | 확정 | 팬이 작성한 루트 댓글과 답글은 수정할 수 없고 운영 목적의 soft delete만 가능하다. |
|
||
| COMMENT-005 | 확정 | 모바일에서도 조회·작성·수정·soft delete를 모두 지원한다. |
|
||
| COMMENT-006 | 외부 의존 | 댓글 목록·작성·수정·soft delete API와 팬 댓글 삭제 권한 오류 계약은 백엔드가 결정·제공해야 한다. 제공 전에는 댓글 network integration을 구현하지 않는다. |
|
||
|
||
### 8.8 공통 파일 정책
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| FILE-001 | 확정 | 캐릭터·오디오 cover·시리즈·커뮤니티 image의 최대 크기는 `10,485,760 bytes` 이하다. `10,485,761 bytes`부터 거부한다. |
|
||
| FILE-002 | 확정 | 기본 image 형식은 JPEG(`.jpg`/`.jpeg`, `image/jpeg`)와 PNG(`.png`, `image/png`)다. WebP 등 다른 형식은 허용하지 않는다. |
|
||
| FILE-003 | 확정 | GIF(`.gif`, `image/gif`)는 커뮤니티 image에서만 허용한다. 캐릭터·시리즈·오디오 cover에서는 거부한다. |
|
||
| FILE-004 | 확정 | 커뮤니티 JPEG/PNG image는 자유 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px, 세로는 선택한 crop ratio에 따라 결정한다. |
|
||
| FILE-005 | 확정 | 시리즈 image는 `210:297` 세로형 고정 aspect ratio로 크롭하며 결과의 최대 가로 폭은 1,000px다. |
|
||
| FILE-006 | 확정 | 오디오 콘텐츠 cover는 `1:1` 고정 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px다. |
|
||
| FILE-007 | 확정 | 커뮤니티 JPEG/PNG·시리즈·오디오 콘텐츠에서 새 image를 선택하면 업로드 전에 crop UI를 반드시 거친다. 커뮤니티 GIF는 예외다. |
|
||
| FILE-008 | 확정 | crop UI는 이동, 확대/축소, 초기화, 결과 미리보기, 취소, 적용을 제공한다. drag/pinch만 강제하지 않고 키보드와 버튼 대안을 제공한다. |
|
||
| FILE-009 | 확정 | optional 교체 파일 미전송은 기존 media 유지다. crop 취소도 기존 media를 변경하지 않는다. 기존 media 자체 제거는 별도 remove contract가 없어 범위 밖이다. |
|
||
| 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 이내 오차를 허용한다. |
|
||
|
||
### 8.9 개발 전용 Mock Preview
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| MOCK-001 | 확정 | 개발 환경은 명시적인 `server`와 `mock` API mode를 제공한다. 기본 `npm run dev`는 실제 개발 API를 사용하고 `npm run dev:mock`만 browser MSW를 활성화한다. |
|
||
| MOCK-002 | 확정 | mock mode도 production과 같은 Page, Query, API client, endpoint path, request serializer와 response DTO를 사용한다. 별도 화면이나 mock 전용 API adapter를 만들지 않는다. |
|
||
| MOCK-003 | 확정 | 실제 API의 404·network error를 감지해 mock으로 자동 fallback하지 않는다. `server` mode의 오류는 실제 오류 UI로 처리한다. |
|
||
| MOCK-004 | 확정 | production build에서는 mock mode를 거부하고 browser worker·fixture가 활성화되지 않는다. |
|
||
| MOCK-005 | 확정 | 계약이 제공됐지만 backend endpoint가 아직 구현되지 않은 기능은 정규화 API Contract 기반 fixture와 browser handler로 최종 UI의 happy path를 확인할 수 있다. |
|
||
| MOCK-006 | 확정 | endpoint·DTO·오류 계약 자체가 미제공인 기능은 fixture를 추정하지 않는다. 계약과 무관한 shell·상태 inventory만 구현하고 최종 network UI 완료를 주장하지 않는다. |
|
||
| MOCK-007 | 확정 | 각 도메인 mock은 deterministic seed와 새로고침 시 초기화되는 in-memory store를 사용해 목록·상세·생성·수정·soft delete의 연결된 흐름을 재현한다. |
|
||
| MOCK-008 | 확정 | mock mode 화면에는 실제 서버가 아니라는 지속적으로 보이는 안내를 제공하고, JWT·password·signed URL·업로드 파일 본문을 log나 영구 저장소에 기록하지 않는다. |
|
||
| MOCK-009 | 확정 | 도메인 완료 상태는 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)`를 분리한다. mock Gate만 통과한 경우 Phase 전체와 network integration을 완료로 표시하지 않는다. |
|
||
|
||
“가로 800/1,000”은 이 문서에서 등록 결과의 **최대 출력 폭**으로 해석한다. JPEG/PNG crop 결과에는 이 제한을 적용하되 선택한 원본 crop 영역이 더 작으면 확대하지 않는다. 커뮤니티 GIF는 원본 가로가 800px 이하일 때만 등록할 수 있다.
|
||
|
||
#### Image crop UI 흐름
|
||
|
||
1. 새 image 선택 직후 resource별 MIME과 `10,485,760 bytes` 이하 제한을 먼저 검증한다.
|
||
2. 커뮤니티 GIF이면 원본 가로를 검사한다. 800px 초과는 inline 오류로 차단하고, 800px 이하는 crop Dialog 없이 원본 비율과 animation을 유지한다.
|
||
3. 캐릭터 image, 커뮤니티 JPEG/PNG, 시리즈 image, 오디오 콘텐츠 cover이면 crop Dialog를 열고 resource별 자유/고정 aspect ratio를 적용한다.
|
||
4. crop Dialog에는 현재 crop 영역과 예상 결과 크기를 표시한다.
|
||
5. “적용” 시 crop 결과 File과 미리보기를 form에 반영하고, “취소” 시 새 선택과 crop 결과를 버려 기존 image 상태를 유지한다.
|
||
6. form 제출 시 crop 대상은 적용된 결과 File을, 커뮤니티 GIF는 crop하지 않은 File을 기존 multipart image part로 보낸다.
|
||
|
||
커뮤니티 GIF는 canvas crop이나 resize 대상이 아니다. client 검증을 우회한 요청도 server가 원본 가로 800px 제한을 다시 검증하고 거부한다.
|
||
|
||
## 9. 반응형 기능 범위
|
||
|
||
| 기능 | 데스크톱 | 태블릿 | 모바일 |
|
||
|---|---:|---:|---:|
|
||
| 로그인·로그아웃 | 전체 | 전체 | 전체 |
|
||
| 캐릭터 목록·검색·상세 조회 | 전체 | 전체 | 조회 |
|
||
| 캐릭터 생성·수정·비활성화 | 전체 | 전체 | 미지원 |
|
||
| 오디오 목록·상세·재생 | 전체 | 전체 | 전체 |
|
||
| 오디오 생성·수정·비활성화·업로드 | 전체 | 전체 | 미지원 |
|
||
| 시리즈 목록·상세·연결 콘텐츠 조회 | 전체 | 전체 | 조회 |
|
||
| 시리즈 생성·수정·비활성화·연결·순서 | 전체 | 전체 | 미지원 |
|
||
| 커뮤니티 목록·게시글 Sheet·오디오 재생 | 전체 | 전체 | 전체 |
|
||
| 커뮤니티 등록·수정·고정·비활성화 | 전체 | 전체 | 미지원 |
|
||
| 댓글·답글 관리 | 전체 | 전체 | 전체 |
|
||
| FanTalk 조회·답변 작성 | 전체 | 전체 | 전체 |
|
||
| FanTalk 답변 수정 | 계약 제공 후 전체 | 계약 제공 후 전체 | 계약 제공 후 전체 |
|
||
|
||
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
|
||
|
||
## 10. UI/UX Expectations
|
||
|
||
### 10.1 디자인 방향
|
||
|
||
`ui-ux-pro-max` 검색과 shadcn/ui 패턴을 바탕으로 다음을 초기 설계 기준으로 사용한다. `#00BDF7` main color는 고정하고, 보조 치수와 supporting semantic color는 접근성·실화면 검증에서 같은 역할과 대비를 유지하는 범위 안에서 조정할 수 있다.
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| UX-001 | 확정 | 초기 릴리스는 밝은 테마만 제공한다. 다크 모드, 테마 전환 버튼과 `prefers-color-scheme: dark` 연동은 후속 범위다. |
|
||
| UX-002 | 확정 | 브랜드 main color와 primary 기준색은 `#00BDF7`이다. 전체 밝은 테마 palette는 이 cyan 계열과 조화되는 semantic token으로 구성하며, raw hex를 feature component에서 직접 사용하지 않는다. |
|
||
|
||
| 항목 | 초기 권고 기준 |
|
||
|---|---|
|
||
| 스타일 | Data-Dense Dashboard + Trust & Authority |
|
||
| 정보 밀도 | 9/10, 조밀하지만 행·필드 그룹은 명확히 구분 |
|
||
| 시각적 변주 | 2/10, 장식보다 상태와 계층 중심 |
|
||
| 모션 | 2/10, 150–200ms의 짧은 상태 전환만 사용 |
|
||
| 기본 모드 | 밝은 테마만 제공 |
|
||
| 기본 간격 | 8px 배수 |
|
||
| 데스크톱 내비게이션 | 약 240px sidebar + 56px header |
|
||
| 표 행 높이 | 최소 44px, 헤더 고정 가능 |
|
||
| 아이콘 | shadcn/ui와 일관된 Lucide 아이콘, emoji 아이콘 금지 |
|
||
|
||
검색 결과에 포함된 landing page 중심의 과장된 대형 타이포그래피, glassmorphism, 지속 애니메이션은 운영 관리자 화면과 맞지 않아 적용하지 않는다.
|
||
|
||
### 10.2 색상과 토큰
|
||
|
||
기능 코드에서 `cyan-600`이나 raw hex처럼 용도를 알 수 없는 값을 직접 반복하지 않는다. primitive → semantic → component의 3단계 token을 사용하고 shadcn/ui CSS variable과 Tailwind semantic color에 연결한다.
|
||
|
||
#### Primary primitive scale
|
||
|
||
`brand-500`은 사용자가 지정한 값을 변경하지 않는다. 옅은 단계는 배경·선택 상태, 짙은 단계는 링크·focus·정보 상태에 사용한다.
|
||
|
||
| Token | 값 | 대표 용도 |
|
||
|---|---|---|
|
||
| `brand-50` | `#F0FBFF` | 아주 옅은 강조 배경 |
|
||
| `brand-100` | `#D9F6FF` | 선택 행, accent 배경 |
|
||
| `brand-200` | `#B5EEFF` | 강조 border |
|
||
| `brand-300` | `#7CE2FF` | 비활성 장식 강조 |
|
||
| `brand-400` | `#36D1FF` | 보조 시각 강조 |
|
||
| `brand-500` | `#00BDF7` | main color, primary 기본 배경 |
|
||
| `brand-600` | `#00A9DE` | primary hover |
|
||
| `brand-700` | `#009DCE` | primary active |
|
||
| `brand-800` | `#007EA8` | 링크, focus ring, info |
|
||
| `brand-900` | `#086789` | 링크 hover |
|
||
| `brand-950` | `#063747` | 가장 짙은 브랜드 강조 |
|
||
|
||
#### 밝은 테마 semantic palette
|
||
|
||
| Semantic token | 기준색 | 사용 |
|
||
|---|---|---|
|
||
| `background` | `#F6FBFD` | 페이지 배경 |
|
||
| `card` / `popover` | `#FFFFFF` | 카드, 표, 폼, overlay surface |
|
||
| `foreground` | `#102A33` | 본문과 제목 |
|
||
| `muted` | `#E9F4F7` | 비강조 surface |
|
||
| `muted-foreground` | `#425F69` | 보조 정보 |
|
||
| `secondary` | `#E1F5FA` | 보조 버튼과 선택 전 control |
|
||
| `secondary-foreground` | `#123E4B` | secondary 위 텍스트 |
|
||
| `accent` | `#D9F6FF` | 선택 행, hover surface |
|
||
| `accent-foreground` | `#0C566F` | accent 위 텍스트 |
|
||
| `border` | `#D5E8EE` | 표 구분선 등 장식 경계 |
|
||
| `input` | `#577581` | 입력과 필수 조작 경계 |
|
||
| `primary` | `#00BDF7` | 주요 CTA와 브랜드 강조 |
|
||
| `primary-hover` | `#00A9DE` | 주요 CTA hover |
|
||
| `primary-active` | `#009DCE` | 주요 CTA pressed/active |
|
||
| `primary-foreground` | `#062B36` | primary 위 텍스트·아이콘 |
|
||
| `link` / `ring` / `info` | `#007EA8` | 흰 배경 링크, focus indicator, 정보 상태 |
|
||
| `link-hover` | `#086789` | 링크 hover |
|
||
| `success` / `OPEN` | `#167347` | 현재 공개, 성공 |
|
||
| `success-surface` | `#EAF8F0` | 성공 Badge·Alert 배경 |
|
||
| `warning` / `SCHEDULED` | `#9A5B00` | 예약 공개, 주의 |
|
||
| `warning-surface` | `#FFF7E6` | 경고 Badge·Alert 배경 |
|
||
| `destructive` | `#B42318` | 비활성화, 실패 |
|
||
| `destructive-surface` | `#FEF0EE` | 오류 Badge·Alert 배경 |
|
||
| `inactive` | `#52636A` | 비활성 상태 |
|
||
| `inactive-surface` | `#EEF3F5` | 비활성 Badge 배경 |
|
||
|
||
- 상태는 색상만으로 전달하지 않고 Badge 텍스트와 아이콘 또는 보조 문구를 함께 사용한다.
|
||
- 일반 텍스트 대비는 4.5:1, 정보를 이해하는 데 필요한 control boundary와 focus indicator는 3:1을 목표로 검증한다. 장식용 divider는 정보 전달 수단으로 사용하지 않는다.
|
||
- `#00BDF7` 위에는 흰색을 사용하지 않고 `primary-foreground=#062B36`을 사용한다. 이 조합은 약 6.84:1이며, `#00BDF7`과 흰색의 약 2.18:1 조합은 텍스트용으로 금지한다.
|
||
- 흰 배경 위 텍스트 링크와 focus ring에는 `brand-500`이 아니라 `brand-800=#007EA8`을 사용한다. `#007EA8`과 흰색은 약 4.62:1이다.
|
||
- 반복되는 상태색은 `success`, `warning`, `destructive`, `inactive` token으로 정의한다.
|
||
- 초기 릴리스에서는 밝은 테마용 semantic token만 구현한다. 다크 token override, ThemeProvider와 테마 전환 control을 만들지 않으며 시스템이 dark mode여도 밝은 테마를 유지한다.
|
||
|
||
### 10.3 타이포그래피
|
||
|
||
- 한국어 가독성을 위해 `Pretendard`, `Noto Sans KR`, `Apple SD Gothic Neo`, `system-ui` 순의 sans-serif stack을 사용한다.
|
||
- 원격 font가 초기 렌더링을 막지 않게 system fallback을 항상 둔다.
|
||
- 페이지 제목 24px, 섹션 제목 18–20px, 본문·표 14px, 보조 정보 12–13px를 기본으로 한다.
|
||
- iOS 입력 확대를 막기 위해 모바일 입력 요소의 실제 font-size는 16px 이상으로 한다.
|
||
- heading에 mono font나 landing page용 48px 이상 크기를 사용하지 않는다.
|
||
|
||
### 10.4 shadcn/ui 구성요소 매핑
|
||
|
||
| UI 목적 | 기본 구성요소 |
|
||
|---|---|
|
||
| 앱 구조 | Sidebar, Sheet, Breadcrumb, Tabs, Separator, ScrollArea |
|
||
| 목록 | Table/Data Table, Card, Badge, Pagination, DropdownMenu |
|
||
| 검색·선택 | Input, Select, Command + Popover Combobox |
|
||
| 폼 | Form, Label, Input, Textarea, Checkbox, RadioGroup, Calendar, Popover |
|
||
| 상태·피드백 | Alert, Skeleton, Progress, Sonner/Toast |
|
||
| 확인·편집 | Dialog, AlertDialog, Drawer |
|
||
| 파일 | Input 기반 공통 FileField + Dialog 기반 ImageCropDialog |
|
||
| 정렬 | 접근 가능한 SortableList와 위/아래 Button |
|
||
| 미디어 | native audio를 감싼 공통 AdminAudioPlayer |
|
||
|
||
- 비활성화처럼 영향이 큰 동작은 Switch가 아니라 AlertDialog를 사용한다.
|
||
- icon-only 버튼에는 `aria-label`과 Tooltip을 제공한다.
|
||
- 모바일의 보조 작업은 DropdownMenu 또는 Drawer에 배치하되 핵심 답변·댓글 동작은 한 번에 찾을 수 있어야 한다.
|
||
- crop Dialog는 pointer drag와 pinch/zoom을 지원하되 이동·확대·축소·초기화를 실행하는 명시적 버튼과 keyboard 조작도 제공한다.
|
||
- crop frame, preview, 적용/취소 control은 tablet touch target 44×44px 이상과 보이는 label 또는 accessible name을 가진다.
|
||
|
||
### 10.5 화면 상태
|
||
|
||
모든 목록과 상세 화면은 다음 상태를 명시적으로 가진다.
|
||
|
||
- 첫 로딩: 레이아웃과 유사한 Skeleton
|
||
- 백그라운드 갱신: 기존 데이터를 유지하고 작은 진행 표시
|
||
- 빈 결과: 현재 검색·필터를 설명하고 초기화 동작 제공
|
||
- 오류: 서버의 한국어 message, 재시도, 필요한 경우 목록으로 이동
|
||
- 저장 중: 제출 버튼 비활성화와 진행 표시
|
||
- 저장 성공: toast와 최신 서버 응답 반영
|
||
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 비활성 항목 제외는 active-only 계약 제공 후 검증
|
||
- 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 `aria-invalid`, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 `errorProperty`가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
|
||
- 업로드: 파일별 진행률, 취소, 재시도
|
||
|
||
300ms 이상 걸릴 수 있는 작업에는 시각적 피드백을 제공하며, 연속 장식 애니메이션은 사용하지 않는다.
|
||
|
||
### 10.6 반응형 원칙
|
||
|
||
- Tailwind의 mobile-first breakpoints를 사용하고 한 요소에 2–3개를 넘는 불필요한 breakpoint를 피한다.
|
||
- 320, 640, 768, 1024, 1280px와 가로 방향을 검증한다.
|
||
- 데스크톱 표는 모바일에서 핵심 필드 중심 Card 목록으로 전환한다. 단순 horizontal scroll만으로 핵심 동작을 숨기지 않는다.
|
||
- 모바일 터치 target은 최소 44×44px이다.
|
||
- 좁은 화면에서 가로 넘침, 잘린 dialog, keyboard에 가려진 답변 입력이 없어야 한다.
|
||
- 데스크톱 sidebar는 모바일에서 Sheet 내비게이션으로 바뀐다.
|
||
|
||
### 10.7 접근성 최소 기준
|
||
|
||
- semantic HTML과 올바른 button/link를 사용한다.
|
||
- “본문으로 건너뛰기” 링크와 `main` landmark를 제공한다.
|
||
- 모든 입력에 보이는 Label을 제공하고 placeholder를 Label 대신 사용하지 않는다.
|
||
- 키보드만으로 메뉴, 탭, 표 행 동작, dialog, 정렬, 답변 작성이 가능해야 한다.
|
||
- focus indicator를 제거하지 않는다.
|
||
- dialog focus trap, 닫힌 후 trigger로 focus 복귀를 검증한다.
|
||
- 비동기 결과와 오류는 적절한 live region 또는 shadcn toast로 전달한다.
|
||
- `prefers-reduced-motion`을 존중한다.
|
||
- 200% browser zoom에서도 핵심 기능을 사용할 수 있어야 한다.
|
||
|
||
### 10.8 z-index와 overlay
|
||
|
||
임의의 `z-[9999]`를 사용하지 않고 semantic scale을 둔다.
|
||
|
||
| 단계 | 값 | 예 |
|
||
|---|---:|---|
|
||
| sticky | 10 | table header |
|
||
| navigation | 20 | header, sidebar |
|
||
| popover | 30 | select, dropdown, date picker |
|
||
| overlay | 40 | sheet/dialog backdrop |
|
||
| modal/notification | 50 | dialog content, toast |
|
||
|
||
Radix Portal과 stacking context를 함께 검증해 Popover가 Dialog 뒤로 숨지 않게 한다.
|
||
|
||
### 10.9 ui-ux-pro-max 사용 의무
|
||
|
||
UI 구현자는 화면 생성 전에 저장소의 `ui-ux-pro-max` design-system 검색을 실행하고 결과를 이 문서의 디자인 방향과 대조한다.
|
||
|
||
```bash
|
||
python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||
"enterprise internal admin console data tables forms file upload operational dashboard neutral compact" \
|
||
--design-system --variance 2 --motion 2 --density 9 \
|
||
-p "AI Character Admin" -f markdown
|
||
```
|
||
|
||
구현 중에는 필요한 domain 검색을 수행하고, 화면 검증 전에 다음 UX 검색을 다시 실행한다.
|
||
|
||
```bash
|
||
python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||
"animation accessibility z-index loading" --domain ux -n 12
|
||
```
|
||
|
||
검색 결과가 관리자 제품의 목적과 충돌하면 그대로 적용하지 않고, 채택·제외 이유를 PR 또는 작업 기록에 남긴다.
|
||
|
||
## 11. API 계약
|
||
|
||
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.0.0`인
|
||
`api-contract.openapi.json`의 15개 path·23개 operation이다.
|
||
OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 `EXT-006 현재 구현
|
||
기준 계약`에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀
|
||
검증의 임시 기준으로 사용한다.
|
||
|
||
### 11.1 공통 응답
|
||
|
||
`/api/v2/admin/ai-characters` 아래 OpenAPI operation의 성공 응답은
|
||
`success`, `message`, `data`, `errorProperty`를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": null,
|
||
"data": {},
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
mutation에 따라 `data`는 상세 DTO, ID DTO 또는 `null`이다. 각 operation의
|
||
response schema를 따르며 공통 client가 임의의 상세 응답으로 정규화하지
|
||
않는다.
|
||
|
||
오류는 의미에 맞는 비2xx status와 다음 envelope를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "잘못된 요청입니다.",
|
||
"data": null,
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
- 오류를 2xx로 normalize하지 않는다.
|
||
- UI는 서버가 반환한 현지화된 한국어 `message`를 우선 사용한다.
|
||
- OpenAPI의 `Accept-Language`는 optional이고 기본값은 `ko`다. `ko|en|ja` 이외 값과 header 누락은 KO로 fallback하며, 프론트엔드는 일관되게 `ko`를 보낸다.
|
||
- OpenAPI operation은 전역 `bearerAuth`를 사용한다. 로그인 이외의 관리자 API에는 `Authorization: Bearer {jwt-token}`을 보낸다.
|
||
- 공통 `page`는 기본 `0`, 최소 `0`이고 공통 `size`는 기본 `20`, 최소 `1`이다. 전역 최대 `50`은 없다. FanTalk `size`만 설명에 따라 `20..50`으로 보정된다.
|
||
- Audio 상세와 Community 목록은 필수 `timezone=Asia/Seoul` query를 보낸다.
|
||
- `characterId`는 선택된 대상 캐릭터가 필요한 하위 resource endpoint에만 사용한다.
|
||
- 캐릭터 목록·검색과 캐릭터 생성에는 path `characterId`가 없다.
|
||
- `/admin/member/login`, `/member/logout`은 현 OpenAPI 범위 밖의 기존 인증 계약이다. 두 endpoint의 현재 구현 기준은 `11.5 EXT-006`에 기록하고 Phase 1 구현과 회귀 검증을 유지하되, 정식 OpenAPI operation으로 표기하지 않는다.
|
||
|
||
### 11.2 오류 처리 매핑
|
||
|
||
| HTTP | 의미 | UI 처리 |
|
||
|---:|---|---|
|
||
| 400 | binding, target 미존재, creator 불변식 등 invalid request | 로컬 검증은 inline, 서버 `errorProperty`가 필드명을 제공할 때만 inline, 그 외 화면 Alert |
|
||
| 401 | JWT 없음·잘못됨·만료·폐기 | 인증 제거 후 로그인 이동 |
|
||
| 403 | 비ADMIN 또는 stale ADMIN claim | 접근 거부 화면 |
|
||
| 404 | path 또는 target 미존재 | 찾을 수 없음과 가능한 목록 이동 |
|
||
| 405 | 지원하지 않는 method | 서버 message와 재시도 불가 안내 |
|
||
| 406 | 허용되지 않는 표현 또는 응답 조건 | 서버 message와 요청 조건 확인 안내 |
|
||
| 415 | 지원하지 않는 media type | 파일 필드 오류와 허용 형식 안내 |
|
||
| 500 | 예상하지 못한 오류 | 일반 오류, request ID가 있으면 함께 표시, 재시도 |
|
||
|
||
OpenAPI는 공통 status와 `ApiErrorResponse` shape만 제공하고 도메인별 정확한
|
||
message key를 열거하지 않는다. 특정 message key 분기가 필요한 기능은
|
||
구현 전에 backend 계약을 추가로 받아야 하며, 그전에는 status와
|
||
`errorProperty`가 제공된 경우만 공통 처리한다.
|
||
|
||
### 11.3 제공된 endpoint 목록
|
||
|
||
AI 캐릭터 관리자 domain의 query, multipart part, request/response field와
|
||
오류 응답은 [api-contract.openapi.json](./api-contract.openapi.json)을
|
||
기준으로 한다. 인증 두 건은 현재 구현·test 기준을 백엔드 정식화 입력으로
|
||
함께 표시한 것이며 현 OpenAPI operation에는 포함되지 않는다.
|
||
|
||
| 영역 | Method | Path | 계약 상태 |
|
||
|---|---|---|---|
|
||
| 인증 | POST | `/admin/member/login` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||
| 인증 | POST | `/member/logout` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨 |
|
||
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 |
|
||
| 오디오 테마 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | 제공됨, query/body 없음 |
|
||
| 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨 |
|
||
| 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 |
|
||
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨 |
|
||
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
|
||
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨 |
|
||
| 시리즈 콘텐츠 | GET, POST | `.../series/{seriesId}/contents` | 제공됨 |
|
||
| 시리즈 연결 후보 | GET | `.../series/{seriesId}/contents/search` | 제공됨 |
|
||
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
|
||
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨 |
|
||
| 커뮤니티 | PUT | `.../community-posts/{postId}` | 제공됨 |
|
||
| FanTalk 목록 | GET | `.../{characterId}/fan-talks` | 제공됨 |
|
||
| FanTalk 답변 | POST | `.../{characterId}/fan-talks/{fanTalkId}/replies` | 제공됨 |
|
||
|
||
### 11.4 OpenAPI 소비 시 주의사항
|
||
|
||
아래 표는 삭제된 Markdown 계약을 기준으로 작성된 기존 문서·구현 계획을
|
||
OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI payload에
|
||
없는 field를 추가하는 근거로 사용돼서는 안 된다.
|
||
|
||
| 영역 | OpenAPI 계약 | 프론트엔드 처리 |
|
||
|---|---|---|
|
||
| Character 목록 | query `searchTerm`; `data={totalCount,content}`; item ID `id` | `search`·`items`·`hasNext`로 바꾸지 않는다. |
|
||
| Character 생성 | 필수 multipart `image`, `request`; request의 필수 `name`, `systemPrompt`, `description`; 성공 `data=null` | 생성 후 목록을 재조회하고 새 ID를 응답에서 추정하지 않는다. |
|
||
| Character 상세 | `characterUUID`, `originalWork`를 포함하고 creator field는 없음 | `characterUUID`를 `externalCharacterId`로 취급하지 않고 creator UI는 만들지 않는다. |
|
||
| Audio 목록 | query `search_word`; `data={totalCount,items}`; status query/field 없음 | 2자 이상 제목 검색만 보내고 status filter·badge를 만들지 않는다. |
|
||
| Audio 테마 | `data=[{id,theme,image}]` | `themeId`·`themeName`·`imageUrl`로 역직렬화하지 않는다. |
|
||
| Audio 생성 | multipart `contentFile`, `coverImage`, `request`; `releaseDate`는 `yyyy-MM-dd HH:mm`; 성공 `data.contentId` | `audioFile`, `releaseDateUtc`, `seriesIds`를 보내지 않는다. |
|
||
| Audio 수정 | optional `coverImage`와 제한된 request field만 제공 | content file, 공개 예약, theme, series 연결 수정 UI를 제공하지 않는다. |
|
||
| Series 생성 | 필수 `image`; request의 `keyword`는 문자열; 성공 `data=null` | `keywords` 배열을 보내지 않고 목록으로 이동해 재조회한다. |
|
||
| Series 상세 | 요일·장르·keywords·state가 표시용 문자열 | 목록 enum 또는 update payload 값으로 재사용하지 않는다. |
|
||
| Series 연결·순서 | 후보 `contents/search`; 연결 `{contentIdList}`; 순서 `{ids}` | `{contentIds}`, `{seriesIds}`를 보내지 않는다. |
|
||
| Community 목록 | 필수 `timezone`; `data`는 배열이고 pagination metadata 없음 | `audioUrl`을 사용하고 total/hasNext를 추정하지 않는다. |
|
||
| Community 생성·수정 | 생성 part `postImage`/`audioFile`; 수정은 `postImage`만 교체 가능; mutation `data=null` | `image`, `audioSignedUrl`, 수정 audio/price field를 만들지 않는다. |
|
||
| FanTalk | 목록 GET과 답변 POST만 제공 | 목록 item 기반 UI와 답변 생성만 구현하고 상세·수정·filter/sort는 외부 의존으로 둔다. |
|
||
|
||
### 11.5 백엔드 제공 대기 계약
|
||
|
||
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부
|
||
의존**이다. P0 계약을 제공받기 전에는 영향을 받는 network integration을
|
||
구현하지 않는다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수
|
||
있다. 단, `EXT-006`은 이미 구현·검증된 기존 인증 endpoint의 정식 문서화
|
||
의존이므로 Phase 3~9 진행을 차단하지 않는다.
|
||
|
||
| ID | 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|
||
|---|---:|---|---|
|
||
| EXT-001 | P0 | 원작 검색 lookup | Character 원작 선택기 network integration 대기 |
|
||
| EXT-002 | P0 | 장르 검색 lookup | 유효한 `genreId` 선택이 필요한 Series 생성 integration 대기 |
|
||
| EXT-003 | P0 | Series 수정 form 초기화용 edit DTO 또는 표시 문자열 mapping | 직접 링크에서 genreId·요일 enum·state enum을 안전하게 복원하는 수정 integration 대기 |
|
||
| EXT-004 | P0 | FanTalk 상세·답변 수정·답변 상태 filter·sort·유일성 오류 | 목록 item 밖의 상세·수정, 전체 결과 filter와 동시 중복 답변 처리 대기 |
|
||
| EXT-005 | P0 | 오디오·커뮤니티 댓글 CRUD와 팬 댓글 삭제 권한 오류 | 댓글·답글 연동과 권한별 오류 처리 대기 |
|
||
| EXT-006 | P0 비차단 | 현재 구현된 `POST /admin/member/login`, `POST /member/logout`의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 | 현재 구현과 Phase 1 회귀는 유지하며 Phase 3~9를 차단하지 않는다. 신규 인증 변경과 전체 계약의 단일 추적성만 정식 계약 제공 대기다. |
|
||
| EXT-007 | P0 | Character 검색 결과와 Audio·Series·Community 목록의 active-only 반환 보장 | soft delete 뒤 비활성 항목이 서버 목록에서 제외된다는 수용 기준 검증 대기 |
|
||
| EXT-008 | P1 | Community pagination의 total/hasNext 또는 종료 규칙 | 신뢰할 수 있는 전체 건수와 마지막 page UI 대기 |
|
||
| EXT-009 | P1 | price 최대값 | 현재 0 이상 정수 규칙만 적용하며 상한 계약 제공 시 Audio·Community schema와 경계값 test 갱신 |
|
||
| EXT-010 | P1 | 파일 크기·MIME·crop·container/codec의 backend 검증 계약 | PRD의 client 사전 검증은 유지하되 server와 동일 경계라는 완료 주장은 계약 제공 후 검증 |
|
||
| EXT-011 | P0 | 도메인별 오류 | 기능별 정확한 비2xx status와 message key 분기가 필요한 흐름 대기 |
|
||
|
||
#### EXT-006 현재 구현 기준 계약
|
||
|
||
다음 내용은 현재 프론트엔드 adapter·contract test·E2E가 사용하는 기존
|
||
인증 계약이다. 백엔드는 이를 정식 OpenAPI operation으로 옮기거나 별도
|
||
버전 고정 계약으로 제공할 수 있다. 정식 계약이 제공되기 전에도 현재
|
||
로그인·로그아웃 구현은 유지하며 Phase 3~9를 진행한다.
|
||
|
||
| 항목 | Admin Login | Member Logout |
|
||
|---|---|---|
|
||
| Method·Path | `POST /admin/member/login` | `POST /member/logout` |
|
||
| `Accept-Language` | `ko` | `ko` |
|
||
| `Content-Type` | `application/json` | body 없음 |
|
||
| Authorization | 보내지 않음 | `Bearer {jwt-token}` 필수 |
|
||
| Request body | `{ "email": string, "password": string }` | 없음 |
|
||
| 성공 `data` | `{ "token": string, "role": "ADMIN" }` | `{}` |
|
||
| 현재 client 처리 | 유효한 token·ADMIN role만 session으로 저장 | 성공·비2xx·network 오류 모두 local session 제거 후 `/login` 이동 |
|
||
|
||
성공 응답은 현재 다음 envelope를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": null,
|
||
"data": {},
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
- 로그인 성공에서는 `data`가 `{ "token": "jwt-token", "role": "ADMIN" }`이다.
|
||
- 로그아웃 성공에서는 `data`가 `{}`다.
|
||
- 현재 contract fixture는 로그인 JSON binding 실패 `400`, 지원하지 않는 media type `415`, 로그아웃의 Bearer 없음·잘못됨·폐기 `401`, 비ADMIN `403`, body 전송 `400`을 검증한다.
|
||
- 정확한 backend 오류 message key와 추가 status는 정식 계약 제공 시 확정한다. 제공 전에는 기존 공통 envelope와 서버 `message` 우선 표시를 유지한다.
|
||
- 구현 근거는 `src/features/auth/api/auth-api.ts`, `src/features/auth/model/auth-session.tsx`, `src/features/auth/tests/auth-api.test.ts`, `src/features/auth/tests/auth-session.test.tsx`, `src/shared/mocks/__tests__/auth-handlers.test.ts`, `tests/e2e/auth.spec.ts`다.
|
||
|
||
## 12. 보안과 데이터 취급
|
||
|
||
- JWT, 비밀번호, signed URL, 업로드 파일 본문을 console·분석 이벤트·오류 리포트에 남기지 않는다.
|
||
- 로그인 요청은 인증 header 없이 `POST /admin/member/login`을 호출한다.
|
||
- 로그인 응답의 `token`은 보호 API와 `POST /member/logout`에 `Authorization: Bearer {jwt-token}` 형식으로 적용한다.
|
||
- 로그인 응답의 JWT와 ADMIN role은 `sessionStorage`에만 저장하고 `localStorage`, IndexedDB 또는 cookie로 복제하지 않는다. 같은 탭의 새로고침에서는 복원하며 로그아웃과 401 처리 시 제거한다.
|
||
- 로그아웃 요청은 현재 Bearer token으로 한 번 호출한다. 성공 여부와 관계없이 로컬 인증 정보를 제거하고 로그인 화면으로 이동하며, 네트워크·비2xx 실패 시 서버 로그아웃 확인 실패 경고를 표시한다.
|
||
- API client는 Bearer header와 `Accept-Language: ko`를 중앙에서 일관되게 적용한다.
|
||
- 401 처리 중 여러 요청이 동시에 실패해도 로그인 이동과 알림을 한 번만 수행한다.
|
||
- 파일 이름은 화면 표시에만 사용하고 경로나 MIME을 신뢰하지 않는다.
|
||
- 캐릭터 하위 mutation은 URL의 `characterId`와 서버 ownership 검증을 모두 통과해야 한다.
|
||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 해당 character workspace의 mutation 진입점을 차단한다. soft delete mutation 성공 직후에는 목록 이동을 우선한다. 비활성 ID 조회 허용 여부와 서버의 mutation 검증 정책은 백엔드 책임이다.
|
||
|
||
### 감사 로그 미결 사항과 권고
|
||
|
||
현재 감사 로그 정책은 **미결**이다.
|
||
|
||
**권고:** 이번 범위에서는 백엔드가 관리자 ID, 대상 캐릭터 ID, resource 종류와 ID, action, 성공/실패, 서버 시각, request ID, 민감정보를 제거한 변경 요약을 기록한다. 관리자 UI의 감사 로그 조회 화면은 후속 범위로 둔다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.
|
||
|
||
## 13. 성능과 품질 요구사항
|
||
|
||
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 종료 metadata 계약이 제공되기 전까지 전체 건수·마지막 page를 표시하지 않는다.
|
||
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
|
||
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
|
||
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
|
||
- 목록 검색 debounce 시간은 구현 시 300ms 전후로 일관되게 적용한다.
|
||
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
|
||
- 브라우저 지원 범위는 데스크톱 Chrome/Edge/Safari 최신 2개 주요 버전과 모바일 Chrome/Safari 최신 2개 주요 버전이다.
|
||
- mock/server mode는 build-time 환경 설정으로 명시적으로 선택하며 runtime 404 fallback을 사용하지 않는다.
|
||
- domain fixture와 browser handler는 `api-contract.openapi.json`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
|
||
|
||
## 14. 성공 기준
|
||
|
||
### 14.1 기능 수용 기준
|
||
|
||
- ADMIN이 로그인해 캐릭터를 검색하고 선택할 수 있다.
|
||
- 로그인 요청이 `POST /admin/member/login`에 email/password만 보내고 ADMIN role과 token을 처리한다.
|
||
- 로그인 성공 후 JWT와 ADMIN role이 `sessionStorage`에만 저장되어 같은 탭의 새로고침에서 복원되고, `localStorage`, IndexedDB, cookie에는 기록되지 않으며 로그아웃과 401에서 제거된다.
|
||
- 로그아웃 요청이 관리자 전용 경로가 아닌 `POST /member/logout`에 Bearer header와 body 없이 전송된다.
|
||
- 로그아웃 API가 성공하거나 네트워크·비2xx 오류로 실패해도 로컬 session이 제거되고 로그인 화면으로 이동하며, 실패한 경우 경고가 표시되고 session이 복원되지 않는다.
|
||
- 캐릭터 생성 multipart가 필수 `image`와 `request`를 보내고 request에 `name`, `systemPrompt`, `description`이 포함되며 `isActive`와 `externalCharacterId`는 포함되지 않는다.
|
||
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
|
||
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 서버 목록에서 비활성 항목이 제외되는지는 active-only 계약 제공 후 검증한다.
|
||
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
|
||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 목록 이동·재조회를 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
|
||
- 오디오 생성에서 즉시 공개는 `releaseDate=null`, 예약 공개는 미래 Asia/Seoul 시각을 `yyyy-MM-dd HH:mm`로 보내고 `timezone="Asia/Seoul"`을 포함한다.
|
||
- MP3, AAC, M4A 업로드의 진행률·취소·재시도와 `1,024,000,000 bytes` 허용·`1,024,000,001 bytes` 거부 경계 검증이 동작한다.
|
||
- 오디오 콘텐츠와 커뮤니티 첨부 audio가 동일한 확장자·MIME·최대 크기·재생 길이 정책을 사용한다.
|
||
- `.m4a`는 `audio/mp4`와 `audio/x-m4a`를 허용하되 호환 MIME도 실제 MP4/M4A container·codec 검증을 통과해야 한다.
|
||
- 모든 image upload가 `10,485,760 bytes` 이하 제한과 resource별 JPEG/PNG/GIF 허용 범위를 적용한다.
|
||
- 캐릭터 image는 `1:1`로 크롭하고 최대 800×800px 결과를 업로드한다.
|
||
- 커뮤니티 JPEG/PNG는 자유 비율·최대 800px, Series는 `210:297`·최대 1,000px, Audio cover는 `1:1`·최대 800px crop 결과를 업로드한다.
|
||
- JPEG/PNG crop 영역이 resource별 최대 출력 폭보다 작으면 확대하지 않고 가능한 원본 pixel 크기로 업로드한다.
|
||
- Series crop 결과는 `height = round(width × 297 ÷ 210)`을 사용하며 최대 출력은 1,000×1,414px이고 검증 시 1px 이내 오차를 허용한다.
|
||
- 커뮤니티 GIF는 crop Dialog를 열지 않고 원본 비율과 animation을 유지해 등록하며, 원본 가로가 800px을 초과하면 제출 전에 거부한다.
|
||
- crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다.
|
||
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
|
||
- 오디오 콘텐츠 생성 시 테마 목록 `data[]`의 `id`, `theme`, `image`를 query/body 없이 조회하고 선택한 `id`를 request의 `themeId`에 포함한다.
|
||
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
|
||
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
|
||
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
|
||
- Series 생성에는 state를 보내지 않고 `keyword` 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
|
||
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
|
||
- FanTalk 목록 item의 `creatorReplies`에 답변이 있으면 두 번째 POST를 UI에서 차단한다. 답변 수정·전체 결과 filter·직접 또는 동시 중복 요청 거부는 외부 계약이 제공된 뒤 수용 기준을 활성화한다.
|
||
- 댓글 계약이 제공된 뒤 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
|
||
- 모바일에서 조회·오디오 재생과 FanTalk 답변 작성이 가능하다. 댓글 관리와 FanTalk 답변 수정은 각 외부 계약 제공 후 같은 capability로 활성화한다.
|
||
- `npm run dev:mock`에서 실제 backend 요청 없이 제공 계약 범위의 최종 UI happy path를 확인할 수 있고 mock mode 안내가 표시된다.
|
||
- 기본 `npm run dev`에서는 실제 개발 API를 사용하며 404·network error가 mock 응답으로 바뀌지 않는다.
|
||
- production build에는 browser mock이 활성화되지 않고 mock mode 설정을 허용하지 않는다.
|
||
- 각 도메인의 Progress와 Phase Gate는 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)` 증거를 별도로 기록한다.
|
||
|
||
### 14.2 UI/UX 수용 기준
|
||
|
||
- 모든 화면에 로딩, 빈 결과, 오류, 성공 상태가 있다.
|
||
- 모든 폼 입력은 보이는 label과 연결된 오류를 가진다.
|
||
- keyboard-only로 로그인, 캐릭터 선택, 탭 이동, FanTalk 답변, 댓글 관리가 가능하다.
|
||
- 320px에서 가로 넘침 없이 합의된 모바일 기능을 사용할 수 있다.
|
||
- 주요 touch target이 44×44px 이상이다.
|
||
- 상태 정보가 색상에만 의존하지 않는다.
|
||
- primary 기준색은 `#00BDF7`이고 그 위 텍스트·아이콘은 `#062B36`을 사용한다. 흰 배경의 링크와 focus ring은 `#007EA8`을 사용하며 핵심 foreground/background 조합이 WCAG 대비 기준을 충족한다.
|
||
- 초기 릴리스에는 다크 모드와 테마 전환 control이 없고, 시스템 색상 테마와 관계없이 접근성 검증을 통과한 밝은 token을 사용한다.
|
||
- `prefers-reduced-motion`에서 불필요한 transition이 제거된다.
|
||
- 모든 핵심 route의 axe 기반 자동 검사에서 critical·serious 접근성 위반이 0건이다.
|
||
- `ui-ux-pro-max`의 loading, reduced motion, z-index, touch 검증 항목을 확인한다.
|
||
|
||
## 15. Open Questions와 결정 절차
|
||
|
||
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|
||
|---|---|---|---|
|
||
| OQ-009 | 확정 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다. |
|
||
| OQ-010 | 미결 | 감사 로그 UI 제공 여부 | backend 기록 우선, 조회 UI는 후속 범위 |
|
||
|
||
## 16. 결정 기록
|
||
|
||
| 날짜 | 결정 |
|
||
|---|---|
|
||
| 2026-07-25 | 독립 관리자 웹, 자체 이메일/비밀번호 인증, ADMIN JWT만 사용한다. |
|
||
| 2026-07-25 | refresh token과 자동 갱신 없이 401에서 로그아웃 처리한다. |
|
||
| 2026-07-25 | 로그인은 `POST /admin/member/login`, 로그아웃은 공통 `POST /member/logout`을 사용하고 JWT는 Bearer header로 전달한다. |
|
||
| 2026-07-25 | JWT와 ADMIN role은 `sessionStorage`에만 저장해 같은 탭의 새로고침에서 복원하고 탭 종료 시 제거한다. `localStorage`, IndexedDB, cookie에는 저장하지 않는다. |
|
||
| 2026-07-25 | 로그아웃 API가 실패해도 로컬 session을 제거하고 로그인 화면으로 이동하며 서버 로그아웃 확인 실패 경고를 표시한다. |
|
||
| 2026-07-25 | 관리자는 선택한 캐릭터 문맥에서 작업하며 캐릭터 계정으로 전환하지 않는다. |
|
||
| 2026-07-25 | 생성과 일반 수정 요청에는 `isActive`를 보내지 않고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| 2026-07-25 | Character, Audio, Series, Community 목록은 활성 resource만 반환하고 활성 상태 filter/query를 제공하지 않는다. |
|
||
| 2026-07-25 | Character, Audio, Series soft delete 성공 후 해당 active-only 목록으로 이동하고, Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신하며 모두 성공 알림을 표시한다. |
|
||
| 2026-07-25 | 비활성 ID 상세 GET의 반환·거부 정책은 백엔드 책임이므로 프론트엔드 요구사항·P0 Gate에서 제외한다. 프론트엔드는 실제 성공 또는 비2xx 응답을 공통 규칙대로 처리한다. |
|
||
| 2026-07-25 | `externalCharacterId`를 모든 계약에서 제거한다. |
|
||
| 2026-07-25 | 오디오 상태는 `OPEN`과 `SCHEDULED`이며 서버가 계산한다. |
|
||
| 2026-07-25 | 즉시 공개는 null, 예약 공개만 날짜를 입력하고 UTC로 전송한다. |
|
||
| 2026-07-25 | MP3, AAC, M4A, 최대 decimal 1,024MB(`1,024,000,000 bytes`), 재생 길이 무제한을 사용한다. |
|
||
| 2026-07-25 | `audio/x-m4a`는 `.m4a` 파일에만 허용하고 실제 MP4/M4A container·codec을 검증한다. |
|
||
| 2026-07-25 | 커뮤니티 첨부 audio도 오디오 콘텐츠와 같은 MP3/AAC/M4A, 최대 1,024MB, 재생 길이 무제한 정책을 사용한다. |
|
||
| 2026-07-25 | 공통 image upload 최대 크기는 10MB로 한다. exact byte 값은 백엔드 확인 후 PRD·API 계약·구현 상수·경계 test를 같은 값으로 정렬한다. |
|
||
| 2026-07-27 | 공통 image upload 10MB의 exact byte를 `10,485,760 bytes`로 확정하고 `10,485,761 bytes`부터 거부한다. |
|
||
| 2026-07-25 | image는 JPEG/PNG를 허용하고 GIF는 커뮤니티에만 허용한다. Character는 1:1·800px, Community JPEG/PNG는 자유 비율·800px, Series는 210:297·1,000px, Audio cover는 1:1·800px crop UI를 제공한다. Community GIF는 crop·resize하지 않고 원본 가로 800px 초과 시 등록을 거부한다. |
|
||
| 2026-07-25 | JPEG/PNG crop 결과는 선택한 원본 crop 영역보다 확대하지 않고 resource별 800px/1,000px을 최대 출력 폭으로만 사용한다. |
|
||
| 2026-07-25 | Series crop 세로는 `round(width × 297 ÷ 210)`으로 계산하고 최대 1,000×1,414px, 검증 오차 1px을 적용한다. |
|
||
| 2026-07-25 | Series state와 요일 enum을 실제 backend enum에 맞게 정정한다. |
|
||
| 2026-07-25 | FanTalk는 한 번만 답변할 수 있고 기존 답변 수정은 허용한다. |
|
||
| 2026-07-25 | 모바일은 조회, 오디오 재생, 댓글 전체 관리, FanTalk 답변 작성·수정을 지원한다. |
|
||
| 2026-07-25 | Tailwind CSS와 shadcn/ui를 사용하고 `ui-ux-pro-max`로 UI를 생성·검증한다. |
|
||
| 2026-07-26 | 초기 릴리스는 밝은 테마만 제공하고 다크 모드, 테마 전환 버튼과 시스템 색상 테마 연동은 후속 범위로 둔다. |
|
||
| 2026-07-26 | main color와 primary 기준색을 `#00BDF7`로 정하고 이에 맞춘 cyan 계열 primitive·semantic palette를 사용한다. primary 위에는 접근 가능한 `#062B36`을, 흰 배경의 링크·focus에는 `#007EA8`을 사용한다. |
|
||
| 2026-07-26 | Series 생성 request에는 state를 보내지 않으며 프론트엔드는 서버의 정확한 초기 기본값을 알 필요 없이 생성 응답의 state를 표시한다. |
|
||
| 2026-07-26 | 재생 오류를 signed URL 만료로 구분하지 않고, media error만으로 API 자동 재조회나 자동 재생을 수행하지 않는다. 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
|
||
| 2026-07-26 | 커뮤니티 전용 상세 GET과 상세·수정 route를 추가하지 않는다. 목록 응답 기반 Sheet를 사용하고 첨부 audio URL 갱신 전용 요청은 만들지 않는다. 사용자 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 있으면 새 응답 값을 사용한다. |
|
||
| 2026-07-26 | 문자열 최대 길이와 배열 최대 개수는 초기 UI 작성 후 각 페이지에서 권고값을 정하고 백엔드 호환 확인 후 확정한다. 그 전에는 제공 계약에 없는 최대값을 추정하지 않는다. |
|
||
| 2026-07-26 | 댓글 API·팬 댓글 삭제 권한 오류와 FanTalk 목록·상세·답변 수정·중복 답변 계약은 백엔드 제공 대기 사항으로 분류하고 프론트엔드 Open Questions에서 제외한다. |
|
||
| 2026-07-27 | 오디오 콘텐츠 생성에는 `themeId`가 필수이며, 테마 선택지는 `GET /api/v2/admin/ai-characters/audio-content-themes`에서 query/body 없이 조회한다. |
|
||
| 2026-07-27 | backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 확인할 수 있도록 명시적 개발 전용 browser MSW mode를 제공한다. 실제 404 자동 fallback은 금지하고 mock UI 완료와 실제 server 연동 완료를 분리한다. |
|
||
| 2026-07-28 | 삭제된 `api-contract.md`를 `api-contract.openapi.json`으로 대체하고, endpoint·query·multipart·request/response·오류는 OpenAPI를 단일 진실 원천으로 사용한다. OpenAPI에 없는 제품·UI 정책만 PRD가 소유한다. |
|
||
| 2026-07-28 | 새 OpenAPI에 맞춰 Character 생성 image/systemPrompt 필수, Audio의 `search_word`·`contentFile`·`releaseDate`·테마 field, Series의 `keyword`·`contentIdList`·`ids`, Community의 `timezone`·`postImage`·`audioUrl`, FanTalk 목록 GET을 구현 기준으로 정정한다. |
|
||
| 2026-07-28 | OpenAPI에 없는 인증 정식 명세, 원작·장르 lookup, Series 수정 form의 edit DTO, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. |
|
||
| 2026-07-28 | OQ-009는 초기 UI를 먼저 구현하고 실제 페이지를 보며 문자열 최대 길이와 배열 최대 개수를 제안한 뒤 backend 호환을 확인해 확정하는 절차로 종결한다. 실제 최대값 확정 전에는 임의 상한을 추가하지 않는다. |
|
||
| 2026-07-28 | `EXT-006`에 현재 구현된 `POST /admin/member/login`과 `POST /member/logout`의 request·response·인증·client 처리 기준을 기록한다. 정식 OpenAPI 포함은 비차단 외부 의존이며 기존 인증 구현과 Phase 3~9 진행을 유지한다. |
|
||
| 2026-07-28 | Phase 3부터는 OpenAPI 제공 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행한다. 미제공 계약은 영향을 받는 기능만 후속 범위로 남기고, 계약 도착 후 별도 vertical slice와 관련 Phase Gate·Phase 9를 다시 실행한다. |
|