docs(ai-character): OpenAPI 계약 반영

This commit is contained in:
Yu Sung
2026-07-28 03:33:16 +09:00
parent 2a5efb0f3e
commit dd30e36323
13 changed files with 1896 additions and 1164 deletions

View File

@@ -4,13 +4,14 @@
| 항목 | 내용 |
|---|---|
| 문서 상태 | 인터뷰 반영 초안 |
| 문서 상태 | 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.md](./api-contract.md) |
| 정규화 계약 | [api-contract.openapi.json](./api-contract.openapi.json) |
### 상태 표기
@@ -18,12 +19,13 @@
- **미결**: 프론트엔드 제품·UI 또는 운영 정책이 결정되지 않아 후속 인터뷰나 UI 검토가 필요한 사항
- **외부 의존**: 프론트엔드가 결정할 사항이 아니며, 해당 기능의 network integration 전에 백엔드가 제공해야 하는 계약
- **권고**: 미결 사항에 대한 현재 추천안이며, 확정 전에는 계약으로 간주하지 않음
- **제외**: 현재 OpenAPI 또는 릴리스 범위에 포함하지 않으며, 다시 포함할 조건을 별도로 기록한 사항
### 문서 유지보수 원칙
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, API 계약 보정표, 미결 사항을 함께 갱신한다.
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, OpenAPI 소비 주의사항, 미결·외부 의존 사항을 함께 갱신한다.
2. 구현 범위나 순서가 바뀌면 같은 디렉터리의 `plan-task.md`도 같은 변경에서 갱신한다.
3. 사용자 인터뷰 결정과 최초 API Contract가 충돌하면 이 문서의 “API 계약 보정사항”을 우선한다.
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 수행 중 제품 결정이 바뀌면 이 문서의 결정 기록과 요구사항을 먼저 갱신한다.
@@ -49,7 +51,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- 오디오 콘텐츠를 관리자 화면에서 즉시 재생해 검수해야 한다.
- 예약 공개, 시리즈 연결과 순서, 게시글 고정, FanTalk 단일 답변 같은 도메인 규칙을 UI에서 명확히 안내해야 한다.
- 데스크톱에서는 전체 운영을 수행하고 모바일에서는 조회와 긴급 응대가 가능해야 한다.
- 최초 API Contract의 잘못되었거나 누락된 필드를 구현 전에 바로잡아야 한다.
- 정식 OpenAPI 계약과 기존 PRD·구현 계획의 잘못되었거나 누락된 API 전제를 구현 전에 바로잡아야 한다.
## 3. Goals
@@ -57,7 +59,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- AI 캐릭터 목록 검색, 상세 조회, 생성, 수정, 비활성화를 제공한다.
- 선택한 캐릭터 문맥에서 오디오 콘텐츠, 시리즈, 커뮤니티 게시글을 관리한다.
- 선택한 캐릭터로 FanTalk에 한 번 답변하고 기존 답변 수정할 수 있게 한다.
- 선택한 캐릭터로 FanTalk에 한 번 답변할 수 있게 한다. 기존 답변 수정은 OpenAPI에 수정 endpoint가 추가된 뒤 활성화한다.
- 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다.
- 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다.
- 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다.
@@ -110,7 +112,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
3. 캐릭터 워크스페이스의 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk 탭을 이동한다.
4. 활성 캐릭터라면 데스크톱·태블릿에서 리소스를 생성·수정·비활성화한다.
5. 오디오 플레이어로 캐릭터가 올린 오디오를 검수한다.
6. FanTalk에는 한 번 답변하고 필요하면 기존 답변 수정한다.
6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변한다. 기존 답변 수정은 수정 계약이 제공된 뒤 추가한다.
7. 오디오 또는 커뮤니티 댓글에 캐릭터 명의로 댓글·답글을 작성하거나 운영 정책에 따라 삭제한다.
8. 모바일에서는 리소스를 조회하고 오디오를 재생하며 댓글과 FanTalk 답변을 관리한다.
9. JWT가 만료되거나 폐기되면 인증 정보를 지우고 로그인 화면으로 이동한다.
@@ -137,16 +139,16 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
/community-posts
/community-posts/new
/fan-talks
/fan-talks/:fanTalkId
```
라우트 문자열은 구현 시 확정하되 다음 원칙은 고정한다.
- 캐릭터를 선택하지 않은 전역 화면은 로그인과 캐릭터 목록·생성뿐이다.
- 캐릭터 리소스 화면은 모두 URL에 `characterId`를 포함한다.
- 목록의 `search`, `status`, 답변 상태, `page`, `size`는 가능한 범위에서 URL query에 보존한다.
- 계약이 제공하는 `searchTerm`, `search_word`, `page`, `size`와 제품 filter 상태는 URL query에 보존한다. 계약에 없는 server filter를 client 전체 결과 filter처럼 가장하지 않는다.
- 상세 GET이 제공되는 주요 리소스의 목록과 상세 화면은 새로고침과 직접 링크 진입이 가능해야 한다.
- 커뮤니티 게시글은 별도 상세·수정 route 없이 목록 행/카드에서 여는 Sheet를 사용한다. 페이지 새로고침은 목록을 다시 조회한다.
- FanTalk도 별도 상세 GET이 없으므로 목록 item을 source로 Sheet 또는 panel을 열고 답변을 작성한다.
- 존재하지 않거나 다른 캐릭터 소유인 하위 리소스는 서버 결과에 따라 오류 화면으로 처리한다.
### 7.2 캐릭터 워크스페이스
@@ -182,7 +184,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|---|---|---|
| CHAR-001 | 확정 | 이름 검색, 페이지네이션이 있는 캐릭터 목록을 제공한다. |
| CHAR-002 | 확정 | 캐릭터 상세, 생성, 수정, 비활성화를 제공한다. |
| CHAR-003 | 확정 | 생성 입력은 `name`, `description`, 선택 이미지, 선택 `originalWorkId`다. |
| 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`는 전송하지 않는다. |
@@ -190,16 +192,22 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| CHAR-008 | 확정 | 캐릭터 생성 시 연결된 `creator(memberKind = AI_CHARACTER)` 생성은 백엔드가 함께 수행한다. |
| CHAR-009 | 확정 | 캐릭터 이름·설명·이미지 변경 시 creator의 nickname·introduce·profile image 동기화는 현재 백엔드가 수행한다. |
| CHAR-010 | 확정 | creator 상황에 따른 조건부 생성·동기화는 다음 백엔드 범위이며 현재 UI 범위가 아니다. |
| CHAR-011 | 확정 | `creatorMemberId`, `creatorNickname` 등 응답으로 제공되는 creator 정보는 읽기 전용으로 표시한다. |
| CHAR-012 | 확정 | 캐릭터 목록은 활성 캐릭터만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
| CHAR-013 | 외부 의존 | 원작 검색 선택기에 필요한 lookup API와 원작 미선택 직렬화 계약은 백엔드가 제공해야 한다. 제공 전에는 원작 선택 network integration을 구현하지 않는다. |
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
| 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가 없어 현재 범위가 아니다.
- 원작은 이름 검색형 Combobox로 선택한다. 미선택은 허용하되 multipart JSON에서 `null`을 보낼지 key를 생략할지는 API 계약으로 확정한다.
- 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다.
- 원작 lookup 계약이 제공되면 이름 검색형 Combobox로 선택한다. 미선택은 허용하고 serializer는 key 생략 또는 `null` 중 한 가지 canonical form을 contract test로 고정한다.
- 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다.
- 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다.
@@ -207,18 +215,18 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| ID | 상태 | 요구사항 |
|---|---|---|
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·검색·상태 필터·상세·생성·수정·비활성화를 제공한다. |
| AUDIO-002 | 확정 | 상태 값은 `OPEN` `SCHEDULED` 두 개뿐이다. |
| AUDIO-003 | 확정 | `OPEN`은 현재 출시된 콘텐츠, `SCHEDULED`는 미래 출시 예약 콘텐츠다. |
| AUDIO-004 | 확정 | 상태는 백엔드가 공개 시각을 기준으로 계산해 반환하고 프론트엔드는 재계산하지 않는다. |
| AUDIO-005 | 확정 | 상태 필터를 보내지 않은 경우의 결과 집합도 백엔드가 결정해 반환한다. |
| 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 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDateUtc=null`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null``timezone="Asia/Seoul"`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. |
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하고 API에는 UTC ISO-8601 `Z` 값으로 변환해 보낸다. |
| AUDIO-012 | 확정 | 생성 cover image와 audio file은 필수이며 수정 시 교체 파일은 선택이다. |
| 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이고 최대 재생 길이는 제한하지 않는다. |
@@ -226,17 +234,22 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. |
| AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. |
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 0 이상의 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
| AUDIO-020 | 확정 | 오디오를 여러 시리즈에 연결할 수 있도록 `seriesIds` 다중 선택을 제공한다. |
| AUDIO-020 | 확정 | Audio 생성·수정 request에는 `seriesIds`가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다. |
| AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. |
| AUDIO-022 | 확정 | 오디오 목록은 활성 오디오만 반환하며 활성 상태 filter/query를 제공하지 않는다. 기존 `status=OPEN|SCHEDULED` 공개 상태 필터는 유지한다. |
| 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-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
| AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 `themeId` 필수이며, 프론트엔드는 `GET /api/v2/admin/ai-characters/audio-content-themes`로 테마 목록을 불러와 선택 UI를 제공한다. |
| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답 `themeId`, `themeName`, `imageUrl` 사용한다. |
| 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은 만들지 않는다. |
수정 화면은 즉시 공개로 재초기화하지 않는다. 서버의 기존 `releaseDateUtc` `status`로 공개 방식과 날짜를 초기화하고, 관리자가 바꾸지 않으면 기존 값을 유지한다.
수정 계약에는 `releaseDate`, `timezone`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
#### 관리자 오디오 플레이어
@@ -254,22 +267,27 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|---|---|---|
| SERIES-001 | 확정 | 목록·상세·생성·수정·비활성화, 콘텐츠 연결·해제, 시리즈 순서 변경을 제공한다. |
| SERIES-002 | 확정 | 상태 enum은 `PROCEEDING`(연재중), `SUSPEND`(휴재중), `COMPLETE`(완결)이다. `OPEN`은 유효하지 않다. |
| SERIES-003 | 확정 | 생성 시 state를 선택하거나 보내지 않는다. 초기 state는 백엔드가 결정하며 프론트엔드는 정확한 기본값을 알 필요 없이 생성 응답의 state를 그대로 표시한다. |
| 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 | 확정 | 장르 이름 검색형 선택기로 고르고 API에는 `genreId`를 보낸다. |
| SERIES-008 | 확정 | 활성 시리즈 전체를 별도 순서 변경 모드에서 불러와 최종 순서의 모든 `seriesIds` 전송한다. |
| 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 | 외부 의존 | 장르 이름 검색 API는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
| SERIES-012 | 확정 | 시리즈 목록은 활성 시리즈만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
| 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를 만들지 않는다.
- 이미 연결된 콘텐츠를 중복 연결하지 않는다.
- 연결 해제 전 대상 제목과 영향을 확인한다.
- 연결/해제 성공 후 시리즈 상세와 콘텐츠 목록을 함께 갱신한다.
@@ -283,25 +301,32 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
| COMMUNITY-006 | 확정 | 비활성 게시글은 반드시 `isFixed=false`, `fixedAtUtc=null` 상태여야 한다. |
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false``isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 0 이상의 정수 “캔” 단위를 사용한다. |
| COMMUNITY-008 | 확정 | 커뮤니티 게시글 목록은 활성 게시글만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
| 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 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioSignedUrl`을 사용한다. |
| 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 | 확정 | 기본 목록은 전체 FanTalk를 최신순으로 표시한다. |
| FANTALK-002 | 확정 | 필터는 전체, 미답변, 답변 완료 세 가지다. |
| FANTALK-001 | 확정 | 기본 목록은 backend가 반환한 순서를 유지한다. 현 계약에 sort query가 없으므로 client가 page 사이의 최신순을 재정렬하지 않는다. |
| FANTALK-002 | 외부 의존 | 전체·미답변·답변 완료 server filter query가 OpenAPI에 없다. 전체 결과 filter 계약이 제공되기 전에는 현재 page만 거르는 filter를 완성 기능으로 제공하지 않는다. |
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
| FANTALK-004 | 확정 | 답변이 있으면 추가 작성은 차단하고 기존 답변 수정만 허용한다. |
| FANTALK-004 | 외부 의존 | 답변이 있으면 추가 작성은 UI에서 차단한다. 기존 답변 수정 endpoint는 OpenAPI에 없으므로 계약 제공 전에는 수정 network integration을 구현하지 않는다. |
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 답변 작성과 수정을 지원한다. |
| FANTALK-007 | 외부 의존 | 제공된 계약에는 답변 POST만 있다. 목록·상세·답변 수정 endpointDTO 백엔드가 제공해야 하며, 제공 전에는 해당 network integration을 구현하지 않는다. |
| 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 댓글과 답글
@@ -375,7 +400,8 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| 커뮤니티 목록·게시글 Sheet·오디오 재생 | 전체 | 전체 | 전체 |
| 커뮤니티 등록·수정·고정·비활성화 | 전체 | 전체 | 미지원 |
| 댓글·답글 관리 | 전체 | 전체 | 전체 |
| FanTalk 조회·답변 작성·답변 수정 | 전체 | 전체 | 전체 |
| FanTalk 조회·답변 작성 | 전체 | 전체 | 전체 |
| FanTalk 답변 수정 | 계약 제공 후 전체 | 계약 제공 후 전체 | 계약 제공 후 전체 |
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
@@ -501,7 +527,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
- 오류: 서버의 한국어 message, 재시도, 필요한 경우 목록으로 이동
- 저장 중: 제출 버튼 비활성화와 진행 표시
- 저장 성공: toast와 최신 서버 응답 반영
- soft delete 성공: 해당 resource의 active-only 목록으로 이동하고 성공 toast 표시
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 비활성 항목 제외는 active-only 계약 제공 후 검증
- 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 `aria-invalid`, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 `errorProperty`가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
- 업로드: 파일별 진행률, 취소, 재시도
@@ -564,21 +590,31 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
## 11. API 계약
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.0.0`
`api-contract.openapi.json`의 15개 path·23개 operation이다.
OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 `EXT-006 현재 구현
기준 계약`에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀
검증의 임시 기준으로 사용한다.
### 11.1 공통 응답
모든 성공 응답은 `ApiResponse.ok(...)` wrapper를 사용한다.
`/api/v2/admin/ai-characters` 아래 OpenAPI operation의 성공 응답은
`success`, `message`, `data`, `errorProperty`를 사용한다.
```json
{
"success": true,
"message": null,
"data": {}
"data": {},
"errorProperty": null
}
```
`/admin/member/login``/member/logout` 성공 예시는 최상위 `errorProperty=null`도 포함한다. 공통 API client는 성공 응답에서 `errorProperty`가 없거나 `null`인 두 형태를 모두 수용한다.
mutation에 따라 `data`는 상세 DTO, ID DTO 또는 `null`이다. 각 operation의
response schema를 따르며 공통 client가 임의의 상세 응답으로 정규화하지
않는다.
모든 오류는 의미에 맞는 비2xx status와 `ApiResponse.error(...)` wrapper를 사용한다.
오류는 의미에 맞는 비2xx status와 다음 envelope를 사용한다.
```json
{
@@ -591,10 +627,13 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
- 오류를 2xx로 normalize하지 않는다.
- UI는 서버가 반환한 현지화된 한국어 `message`를 우선 사용한다.
- 백엔드는 `Accept-Language: ko|en|ja`에 따라 번역하고 누락·미지원 언어는 KO로 fallback한다. security filter도 header를 직접 해석한다.
- 모든 목록·검색 endpoint는 `page=0`, `size=20` 기본값과 size 최소 20, 최대 50 보정을 적용한다.
- 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 오류 처리 매핑
@@ -603,67 +642,121 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
| 400 | binding, target 미존재, creator 불변식 등 invalid request | 로컬 검증은 inline, 서버 `errorProperty`가 필드명을 제공할 때만 inline, 그 외 화면 Alert |
| 401 | JWT 없음·잘못됨·만료·폐기 | 인증 제거 후 로그인 이동 |
| 403 | 비ADMIN 또는 stale ADMIN claim | 접근 거부 화면 |
| 404 | 신규 prefix 미매핑 경로 | 찾을 수 없음과 목록 이동 |
| 404 | path 또는 target 미존재 | 찾을 수 없음과 가능한 목록 이동 |
| 405 | 지원하지 않는 method | 서버 message와 재시도 불가 안내 |
| 406 | 허용되지 않는 표현 또는 응답 조건 | 서버 message와 요청 조건 확인 안내 |
| 415 | 지원하지 않는 media type | 파일 필드 오류와 허용 형식 안내 |
| 500 | 예상하지 못한 오류 | 일반 오류, request ID가 있으면 함께 표시, 재시도 |
Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 KO/EN/JA message key를 확정해야 한다. 신규 prefix 전용 처리를 legacy/public endpoint로 확장하지 않는다.
OpenAPI는 공통 status와 `ApiErrorResponse` shape만 제공하고 도메인별 정확한
message key를 열거하지 않는다. 특정 message key 분기가 필요한 기능은
구현 전에 backend 계약을 추가로 받아야 하며, 그전에는 status와
`errorProperty`가 제공된 경우만 공통 처리한다.
### 11.3 제공된 endpoint 목록
모든 query, multipart part, request/response field를 보존한 보정 후 계약은 [api-contract.md](./api-contract.md)를 기준으로 한다.
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` | 제공됨, body는 email/password, 성공 시 token/ADMIN role |
| 인증 | POST | `/member/logout` | 제공됨, 공통 endpoint, Bearer header, body 없음 |
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨, 필드 보정 필요 |
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨, 필드 보정 필요 |
| 인증 | 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, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨 |
| 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 |
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨, enum 보정 필요 |
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨, enum 보정 필요 |
| 시리즈 콘텐츠 | GET, POST | `.../series/{seriesId}/contents` | 제공됨 |
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨 |
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨, 생성 필드 보정 필요 |
| 시리즈 | 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 API 계약 보정사항
### 11.4 OpenAPI 소비 시 주의사항
표는 최초 API Contract보다 우선한다.
아래 표는 삭제된 Markdown 계약을 기준으로 작성된 기존 문서·구현 계획을
OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI payload에
없는 field를 추가하는 근거로 사용돼서는 안 된다.
| 항목 | 최초 계약 | 확정 보정 |
| 영역 | OpenAPI 계약 | 프론트엔드 처리 |
|---|---|---|
| Character `externalCharacterId` | 요청·응답에 존재 | 존재하지 않는 필드이므로 전부 제거 |
| 생성 `isActive` | Character, Audio, Community 예시에 존재 | 모든 생성 요청에서 제거, 서버가 초기값 결정 |
| 수정 `isActive` | 수정 예시에 존재 | 일반 수정에서는 key를 생략하고 soft delete에만 `false`를 보낸다. `true`는 전송하지 않는다. |
| Audio status | 예시에 `OPEN` | 허용값은 `OPEN`, `SCHEDULED`이며 서버 계산 |
| Audio create `themeId` | 누락 | 생성 시 필수이며 오디오 테마 목록 endpoint에서 선택한 `themeId`를 보낸다. |
| Series state | 예시에 `OPEN` | `PROCEEDING`, `SUSPEND`, `COMPLETE`만 허용 |
| Series 생성 state | 요청 예시에 `OPEN` | 생성 요청에서 state 제거 |
| Series 수정 state | 필수처럼 표현 | 선택 필드, 미선택 시 생략하여 기존 값 유지 |
| Series 요일 | `MONDAY` 등 장문 값 | `SUN`~`SAT``RANDOM` 사용 |
| RANDOM | 규칙 없음 | 단독만 허용, 다른 요일과 조합 금지 |
| Character creator | 응답 연결만 표현 | 생성 시 AI_CHARACTER creator 동시 생성, 프로필 동기화는 백엔드 담당 |
| FanTalk 답변 | POST만 표현 | 한 번만 생성, 기존 답변 수정 가능, 삭제 불가 |
| 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의 request/response/error 계약을 제공받기 전에는 해당 기능의 network integration을 구현하지 않는다. P1은 추정하지 않고 출시 전 계약과 검증을 맞춘다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수 있다.
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부
의존**이다. P0 계약을 제공받기 전에는 영향을 받는 network integration을
구현하지 않는다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수
있다. 단, `EXT-006`은 이미 구현·검증된 기존 인증 endpoint의 정식 문서화
의존이므로 Phase 3~9 진행을 차단하지 않는다.
| 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|---:|---|---|
| P0 | FanTalk 목록·상세·답변 수정·유일성 | 목록·상세·수정 연동과 동시 중복 답변 처리 대기 |
| P0 | 오디오·커뮤니티 댓글 CRUD와 팬 댓글 삭제 권한 오류 | 댓글·답글 연동과 권한별 오류 처리 대기 |
| P0 | 원작·장르 검색과 originalWork 미선택 직렬화 | Character·Series 선택기 연동 대기 |
| P0 | 시리즈 연결 후보 | 연결 가능한 활성 오디오 선택기 연동 대기 |
| P0 | 시리즈 전체 순서의 50개 초과 로딩·누락 ID·동시 충돌 | 전체 순서 저장 연동 대기 |
| P1 | price 최대값 | 현재 0 이상 정수 규칙만 적용하며 상한 계약 제공 시 Audio·Community schema와 경계값 test 갱신 |
| P0 | 신규 도메인 오류 | 기능별 정확한 비2xx status와 KO/EN/JA message key 제공 대기 |
| 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. 보안과 데이터 취급
@@ -686,7 +779,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
## 13. 성능과 품질 요구사항
- 목록은 서버 페이지네이션을 사용하고 무제한 전체 로드를 피한다. 단, 시리즈 순서 변경 모드는 계약상 활성 시리즈 전체를 명시적으로 로드한다.
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 종료 metadata 계약이 제공되기 전까지 전체 건수·마지막 page를 표시하지 않는다.
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
@@ -694,7 +787,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
- 브라우저 지원 범위는 데스크톱 Chrome/Edge/Safari 최신 2개 주요 버전과 모바일 Chrome/Safari 최신 2개 주요 버전이다.
- mock/server mode는 build-time 환경 설정으로 명시적으로 선택하며 runtime 404 fallback을 사용하지 않는다.
- domain fixture와 browser handler는 `api-contract.md`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
- domain fixture와 browser handler는 `api-contract.openapi.json`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
## 14. 성공 기준
@@ -705,12 +798,12 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
- 로그인 성공 후 JWT와 ADMIN role이 `sessionStorage`에만 저장되어 같은 탭의 새로고침에서 복원되고, `localStorage`, IndexedDB, cookie에는 기록되지 않으며 로그아웃과 401에서 제거된다.
- 로그아웃 요청이 관리자 전용 경로가 아닌 `POST /member/logout`에 Bearer header와 body 없이 전송된다.
- 로그아웃 API가 성공하거나 네트워크·비2xx 오류로 실패해도 로컬 session이 제거되고 로그인 화면으로 이동하며, 실패한 경우 경고가 표시되고 session이 복원되지 않는다.
- 캐릭터 생성 요청에 `isActive``externalCharacterId` 포함되지 않는다.
- 캐릭터 생성 multipart가 필수 `image``request`를 보내고 request에 `name`, `systemPrompt`, `description`이 포함되며 `isActive``externalCharacterId` 포함되지 않는다.
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
- Character, Audio, Series의 soft delete가 성공하면 해당 active-only 목록 cache를 갱신해 비활성화한 항목을 표시하지 않고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신해 해당 항목을 제거한다.
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 서버 목록에서 비활성 항목이 제외되는지는 active-only 계약 제공 후 검증한다.
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 active-only 목록 이동을 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
- 오디오 생성에서 즉시 공개는 `releaseDateUtc=null`, 예약 공개는 미래 UTC 시각을 보낸다.
- 워크스페이스 진입·복원용 상세 성공 응답으로 `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 검증을 통과해야 한다.
@@ -722,15 +815,15 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
- 커뮤니티 GIF는 crop Dialog를 열지 않고 원본 비율과 animation을 유지해 등록하며, 원본 가로가 800px을 초과하면 제출 전에 거부한다.
- crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다.
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
- 오디오 콘텐츠 생성 시 테마 목록 query/body 없이 조회하고 선택한 `themeId`를 request JSON에 포함한다.
- 오디오 콘텐츠 생성 시 테마 목록 `data[]``id`, `theme`, `image` query/body 없이 조회하고 선택한 `id`를 request`themeId`에 포함한다.
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
- Series 생성에는 state를 보내지 않고 수정 미선택 시 state를 생략한다.
- Series 생성에는 state를 보내지 않고 `keyword` 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
- FanTalk 답변이 있으면 두 번째 POST UI에서 차단되고 수정 동작만 제공되며, 직접·동시 요청도 백엔드가 원자적으로 거부한다.
- 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
- 모바일에서 조회·오디오 재생·댓글 관리·FanTalk 답변 작성/수정이 가능하다.
- 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 설정을 허용하지 않는다.
@@ -750,11 +843,11 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
- 모든 핵심 route의 axe 기반 자동 검사에서 critical·serious 접근성 위반이 0건이다.
- `ui-ux-pro-max`의 loading, reduced motion, z-index, touch 검증 항목을 확인한다.
## 15. Open Questions
## 15. Open Questions와 결정 절차
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|---|---|---|---|
| OQ-009 | 미결 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 만든 뒤 Character의 이름·설명, Audio의 제목·설명·`seriesIds`, Series의 제목·소개·요일·keywords·writer·studio·연결 `contentIds`·순서 `seriesIds`, Community 본문, FanTalk 답변과 댓글을 페이지별로 검토해 권고값을 작성한다. 백엔드 호환 확인 후 확정하며, 그 전에는 제공 계약에 없는 임의의 최대값을 추가하지 않는다. 확정 시 PRD·API Contract·schema·경계값 test를 함께 갱신한다. |
| OQ-009 | 확정 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다. |
| OQ-010 | 미결 | 감사 로그 UI 제공 여부 | backend 기록 우선, 조회 UI는 후속 범위 |
## 16. 결정 기록
@@ -795,3 +888,9 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
| 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를 다시 실행한다. |