Files
voiceon-character-admin/docs/20260725_AI캐릭터관리자웹/prd.md

97 KiB
Raw Blame History

AI 캐릭터 관리자 웹 PRD

문서 정보

항목 내용
문서 상태 OpenAPI 반영 구현 기준
작성일 2026-07-25
최종 수정일 2026-07-30
대상 제품 AI 캐릭터 전용 독립 관리자 웹
구현 대상 React + TypeScript + Vite SPA
UI 기반 Tailwind CSS + shadcn/ui
관련 계획 plan-task.md
정규화 계약 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.mdPhase GoalGoal 실행을 기준으로 한다. 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에 한 번 답변하고 기존 답변을 수정할 수 있게 한다.
  • 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다.
  • 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다.
  • 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다.

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 등 백엔드 조회 정책 결정
  • 감사 로그 조회 UI
  • 실제 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 화면 구조

/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.tokendata.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 확정 이름 검색 여부와 관계없이 캐릭터 목록 API는 isActive=true인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않고 서버 반환값을 사용한다.
CHAR-013 확정 원작 검색은 필수 searchTerm query를 사용하는 GET /api/v2/admin/ai-characters/original-works/search를 호출하고 data[]id, title, contentType, category, isAdult, description, originalWork, originalLink, writer, studio, originalLinks, tags, imageUrl을 사용한다. originalWorkId 미선택은 serializer의 canonical 생략으로 고정한다.
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는 원작 검색 선택기로 편집하고, 수정 request에 없는 region은 수정 화면에서 읽기 전용이다.
  • 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다.
  • 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다.
  • 원작은 제목·콘텐츠 타입·카테고리 부분 검색을 지원하는 Combobox로 선택한다. 미선택은 허용하고 serializer는 originalWorkId key 생략을 canonical form으로 사용한다.
  • 비활성화는 폼 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로 표현한다. 예약 값은 클라이언트가 UTC로 변환한 ISO-8601 Z 문자열이며 timezone field는 보내지 않는다. 목록·상세의 releaseDate도 UTC Z 문자열 또는 null로 소비하고 별도 status enum을 만들지 않는다.
AUDIO-004 확정 프론트엔드는 releaseDateOPEN·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은 보내지 않는다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다.
AUDIO-010 확정 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다.
AUDIO-011 확정 예약 시각은 Asia/Seoul로 입력·표시하되, 생성 API에는 해당 시각을 클라이언트에서 UTC로 변환한 ISO-8601 Z 형식의 releaseDate를 보낸다. 예를 들어 2026-07-29 18:00 Asia/Seoul은 2026-07-29T09:00:00Z로 전송한다.
AUDIO-012 확정 생성 multipart의 contentFile, coverImage, request는 필수다. 수정은 coverImagerequest만 허용하므로 오디오 원본 파일 교체 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..99999 정수다. 0은 무료이며 UI는 예: 1,000캔으로 표시한다.
AUDIO-020 확정 Audio 생성·수정 request에는 seriesIds가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다.
AUDIO-021 확정 목록과 상세에서 오디오를 재생할 수 있다.
AUDIO-022 확정 오디오 목록 API는 isActive=true인 항목만 반환한다. request에는 활성 상태 query를 추가하지 않고 응답에도 client-side 활성 filter를 적용하지 않는다. status query는 여전히 제공하지 않는다.
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 query를 보내지 않는다. 응답의 nullable releaseDate는 ISO-8601 UTC Z 값으로 소비하고 화면 표시 시 Asia/Seoul로 변환한다.
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은 만들지 않는다.
AUDIO-034 확정 공통 관리자 오디오 플레이어는 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일한 compact audio-only control bar를 사용한다. Plyr audio player를 1차 시각 기준, Media Chrome audio player를 control anatomy 기준으로 삼아 표준 viewport에서는 재생·진행·현재/전체 시간·배속·음량을 상시 텍스트 label 없이 한 줄에 표시한다. 별도 image·video 영역을 만들지 않고 기존 화면의 cover·게시물 media는 그대로 유지한다.

현 수정 계약에는 releaseDate, themeId, contentFile이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.

관리자 오디오 플레이어

  • 재생/일시정지, 탐색, 현재/전체 시간, 볼륨, 배속을 제공한다.
  • native <audio>와 현재 재생 상태·오류 처리 로직은 유지하고, Plyr audio의 밝은 compact bar를 1차 시각 기준, Media Chrome audio의 명시적 시간·배속 anatomy를 보조 기준으로 사용한다.
  • 표준 viewport의 기본 배치는 원형 재생 버튼, 유동형 재생 위치 slider, 현재/전체 시간, compact 배속 select, 음량 icon과 slider 순서의 단일 control bar다. 볼륨, 재생 속도 같은 설명 label은 화면에 상시 노출하지 않고 accessible name으로 제공한다.
  • 200% zoom처럼 실제 player 폭이 control 최소 폭보다 작을 때만 secondary control을 다음 줄로 보내며, control이 잘리거나 가로 overflow가 생기지 않아야 한다.
  • 플레이어 내부에는 cover image, poster, video viewport를 표시하지 않는다. 오디오 콘텐츠 cover와 게시물 media는 기존 화면 영역에서만 표시한다.
  • 새 audio player library, waveform, playlist와 download control은 추가하지 않는다.
  • 명시적인 다운로드 버튼은 제공하지 않는다.
  • 여러 행의 플레이어가 동시에 재생되지 않게 현재 재생 항목을 단일화한다.
  • 재생 오류는 원인을 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에서 유효하지 않다. 장르 선택지는 query/body 없는 GET /api/v2/admin/ai-characters/series-genresdata[]에서 id, genre, isAdult를 사용한다.
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는 검색·페이지 query 없이 활성 장르 전체를 반환한다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다.
SERIES-012 확정 시리즈 목록 API는 isActive=true인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않는다.
SERIES-013 확정 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다.
SERIES-014 확정 생성 multipart의 imagerequest는 필수다. 생성 request는 keyword 단일 문자열을 사용하며 keywords 배열을 보내지 않는다.
SERIES-015 확정 목록과 상세는 동일한 SeriesListItem schema를 사용하고 genreId, enum 배열 publishedDaysOfWeek, enum state를 반환한다. 화면 label은 클라이언트에서 표시용으로 변환하되 원본 enum과 ID를 수정 payload에 사용한다.
SERIES-016 확정 연결 후보는 GET .../contents/search?search_word=..., 연결은 { "contentIdList": [...] }, 해제는 body 없는 DELETE를 사용한다.
SERIES-017 확정 시리즈 상세 응답은 목록 item과 동일한 수정용 원본값 genreId, enum publishedDaysOfWeek, enum state를 제공하므로 별도 edit DTO가 필요 없다. 수정 화면은 상세 응답으로 기존 선택값을 초기화하고, 장르 API는 option 목록 표시용으로 호출한다.
SERIES-018 확정 생성 request의 keyword는 수정 request와 상세 응답에 없다. 수정 화면에서 keyword 편집·표시를 추가하거나 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=falseisFixed=false를 함께 보낸다. 현 목록 응답에는 fixedAtUtc가 없으므로 해당 field를 DTO·UI에 만들지 않는다.
COMMUNITY-007 확정 가격은 오디오와 동일하게 0..99999 정수 “캔” 단위를 사용한다.
COMMUNITY-008 확정 커뮤니티 목록 API는 isActive=true인 게시글만 반환한다. request에는 활성 상태 query를 추가하지 않고 item에도 client-side 활성 filter를 적용하지 않는다.
COMMUNITY-009 확정 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다.
COMMUNITY-010 확정 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 서버의 active-only 목록에서 해당 게시글이 제외돼야 한다.
COMMUNITY-011 확정 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 audioUrl을 사용한다.
COMMUNITY-012 확정 목록 GET은 timezone 없이 page, size를 사용하고 data.totalCount, data.page, data.size, data.hasNext, data.items[]를 소비한다. 전체 건수와 다음 page 여부는 서버 metadata를 그대로 사용한다.
COMMUNITY-013 확정 생성 multipart는 optional audioFile, optional postImage, 필수 request를 사용하고 request에 필수 content, isCommentAvailable, isAdult와 optional price만 보낸다.
COMMUNITY-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 제외 현재 UI에는 전체·미답변·답변 완료 filter control을 제공하지 않는다. 현재 page만 client에서 거르는 불완전한 filter도 만들지 않는다. 후속 제품 범위에서 전체 결과 filter가 필요해지면 server query 계약과 함께 별도 요구사항으로 다시 포함한다.
FANTALK-003 확정 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다.
FANTALK-004 확정 creatorReplies가 비어 있으면 답변 작성 UI를, 비어 있지 않으면 기존 답변 수정 UI를 제공한다. 수정은 PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}{ "content": string }만 보내며 path의 replyId에는 creatorReplies[].fanTalkId를 사용한다. 백엔드 구현 완료 확인에 따라 OpenAPI operation도 implemented로 정정했으므로 mock/client와 실제 server integration을 모두 구현·검증한다.
FANTALK-005 확정 답변 삭제는 현재 범위가 아니다.
FANTALK-006 확정 데스크톱·태블릿·모바일 모두 목록 조회, 답변 작성과 기존 답변 수정을 지원한다.
FANTALK-007 확정 별도 상세 GET·직접 route, 답변 상태 filter, sort control은 현재 UI 범위가 아니다. 목록 item을 source로 Sheet/panel을 열고 backend 반환 순서를 유지하므로 추가 network 계약 없이 목록·작성·수정·팬 원글 삭제를 구현한다.
FANTALK-008 제외 중복 생성의 정확한 비2xx status/message key를 사용하는 도메인별 오류 분기는 만들지 않는다. 답변 1개 불변식 자체는 FANTALK-003의 backend 수용 기준이며, client는 한 화면의 중복 submit을 막고 일반 오류 후 현재 목록을 재조회한다. 동시 POST 후에도 답변이 하나인지 실제 server integration에서 검증하며 위반 시 backend 결함으로 기록한다.
FANTALK-009 확정 목록은 page, size를 사용하고 data.fanTalkCount, data.fanTalks, data.page, data.size, data.hasNext를 소비한다. creatorReplies.length === 0이면 미답변, 하나 이상이면 답변 완료로 판단하며 기존 답변 수정 시 첫 답변의 fanTalkIdreplyId로 사용한다.
FANTALK-010 확정 답변 작성은 { "content": string }을 보내고 성공 응답의 fanTalkId, replyId, creatorMemberId, content, createdAtUtc를 사용한다.
FANTALK-011 확정 별도 상세 endpoint가 없으므로 목록 item을 source로 collection Sheet 또는 panel을 열며 /fan-talks/:fanTalkId 직접 route를 만들지 않는다.
FANTALK-012 확정 관리자는 팬 작성 FanTalk 원글을 DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}로 soft delete할 수 있다. 성공 후 현재 목록을 재조회하고 해당 원글을 닫으며, 연결된 creator reply를 별도 삭제했다고 가정하지 않는다.

8.7 댓글과 답글

ID 상태 요구사항
COMMENT-001 확정 오디오 콘텐츠와 커뮤니티 게시글에 댓글 영역을 제공한다.
COMMENT-002 확정 구조는 루트 댓글과 그 댓글의 직접 답글까지 정확히 2단계다. 답글의 답글은 허용하지 않는다.
COMMENT-003 확정 AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정할 수 있다. 댓글의 writerId와 대상 오디오·커뮤니티 게시글의 creatorId가 같으면 AI 캐릭터 작성으로 판단한다.
COMMENT-004 확정 writerId !== creatorId인 팬 작성 루트 댓글과 답글에는 수정 UI를 제공하지 않는다. 관리자는 작성자와 관계없이 운영 목적으로 해당 row만 soft delete할 수 있으며 하위 답글을 함께 삭제했다고 가정하지 않는다.
COMMENT-005 확정 모바일에서도 조회·작성·수정·soft delete를 모두 지원한다.
COMMENT-006 확정 오디오 콘텐츠와 커뮤니티 게시글은 각각 루트 댓글 목록 GET, 댓글·답글 POST, AI 캐릭터 작성 댓글 PUT, 작성자 무관 DELETE, 루트별 답글 목록 GET을 제공한다. 모든 mutation 성공 data=null을 처리하고 목록·답글 cache를 재조회한다.
COMMENT-007 확정 루트 댓글은 .../comments, 직접 답글은 .../comments/{commentId}/replies에서 조회한다. 작성 POST의 parentId를 생략하거나 null로 보내면 루트, 같은 target의 활성 루트 ID를 보내면 직접 답글이며 답글의 답글은 UI에서 허용하지 않는다.
COMMENT-008 확정 댓글 날짜는 ISO-8601 UTC Z 값으로 소비해 Asia/Seoul로 표시한다. 오디오 댓글 작성 request는 comment, optional parentId, isSecret, languageCode를 사용하고 커뮤니티 댓글 작성 request는 comment, optional parentId, isSecret을 사용한다. 수정 request는 공통 { "comment": string }이다.

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 확정 개발 환경은 명시적인 servermock 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 답변 수정·팬 원글 soft delete 전체 전체 전체

모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.

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, 150200ms의 짧은 상태 전환만 사용
기본 모드 밝은 테마만 제공
기본 간격 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, 섹션 제목 1820px, 본문·표 14px, 보조 정보 1213px를 기본으로 한다.
  • 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 목록에서 비활성 항목 제외를 확인하며 client filter로 보정하지 않음
  • 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 aria-invalid, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 errorProperty가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
  • 업로드: 파일별 진행률, 취소, 재시도

300ms 이상 걸릴 수 있는 작업에는 시각적 피드백을 제공하며, 연속 장식 애니메이션은 사용하지 않는다.

10.6 반응형 원칙

  • Tailwind의 mobile-first breakpoints를 사용하고 한 요소에 23개를 넘는 불필요한 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 검색을 실행하고 결과를 이 문서의 디자인 방향과 대조한다.

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 검색을 다시 실행한다.

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.3.0api-contract.openapi.json의 25개 path·37개 operation이다. OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 EXT-006 현재 구현 기준 계약에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀 검증의 임시 기준으로 사용한다.

11.1 공통 응답

/api/v2/admin/ai-characters 아래 OpenAPI operation의 성공 응답은 success, message, data, errorProperty를 사용한다.

{
  "success": true,
  "message": null,
  "data": {},
  "errorProperty": null
}

mutation에 따라 data는 상세 DTO, ID DTO 또는 null이다. 각 operation의 response schema를 따르며 공통 client가 임의의 상세 응답으로 정규화하지 않는다.

오류는 의미에 맞는 비2xx status와 다음 envelope를 사용한다.

{
  "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으로 보정된다.
  • AI 캐릭터 관리자 API의 날짜·시간 request/response는 OpenAPI가 별도로 명시한 nullable 조건을 제외하고 ISO-8601 UTC Z를 사용하며 timezone query/body field를 보내지 않는다.
  • 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을 기준으로 한다. 인증 두 건은 현재 구현·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 /api/v2/admin/ai-characters/original-works/search 제공됨, 필수 searchTerm
캐릭터 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 .../audio-contents/{contentId}/comments 제공됨
오디오 댓글 PUT, DELETE .../audio-contents/{contentId}/comments/{commentId} 제공됨
오디오 댓글 답글 GET .../audio-contents/{contentId}/comments/{commentId}/replies 제공됨
시리즈 장르 GET /api/v2/admin/ai-characters/series-genres 제공됨, query/body 없음
시리즈 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} 제공됨
커뮤니티 댓글 GET, POST .../community-posts/{postId}/comments 제공됨
커뮤니티 댓글 PUT, DELETE .../community-posts/{postId}/comments/{commentId} 제공됨
커뮤니티 댓글 답글 GET .../community-posts/{postId}/comments/{commentId}/replies 제공됨
FanTalk 목록 GET .../{characterId}/fan-talks 제공됨
FanTalk 팬 원글 DELETE .../{characterId}/fan-talks/{fanTalkId} 제공됨
FanTalk 답변 POST .../{characterId}/fan-talks/{fanTalkId}/replies 제공됨
FanTalk 답변 PUT .../{characterId}/fan-talks/{fanTalkId}/replies/{replyId} 제공됨, backend 구현 완료

11.4 OpenAPI 소비 시 주의사항

아래 표는 삭제된 Markdown 계약을 기준으로 작성된 기존 문서·구현 계획을 OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI payload에 없는 field를 추가하는 근거로 사용돼서는 안 된다.

영역 OpenAPI 계약 프론트엔드 처리
Character 목록 query searchTerm; data={totalCount,content}; item ID id search·items·hasNext로 바꾸지 않는다.
Character 원작 검색 필수 searchTerm; dataOriginalWorkSearchItem[] legacy lookup을 호출하지 않고 반환 idoriginalWorkId로 사용한다.
Character 생성 필수 multipart image, request; request의 필수 name, systemPrompt, description; 성공 data=null 생성 후 목록을 재조회하고 새 ID를 응답에서 추정하지 않는다.
Character 상세 characterUUID, originalWork를 포함하고 creator field는 없음 characterUUIDexternalCharacterId로 취급하지 않고 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는 nullable ISO-8601 UTC Z; timezone field 없음; 성공 data.contentId 예약 입력을 client에서 UTC로 변환하고 audioFile, releaseDateUtc, timezone, seriesIds를 보내지 않는다.
Audio 상세 timezone query 없음; nullable releaseDate는 ISO-8601 UTC Z UTC 값을 Asia/Seoul 표시로 변환하되 API status를 재계산하지 않는다.
Audio 수정 optional coverImage와 제한된 request field만 제공 content file, 공개 예약, theme, series 연결 수정 UI를 제공하지 않는다.
가격 Audio 생성·수정과 Community 생성 request의 price0..99999 정수 -1, 100000, 소수는 제출 전에 거부하고 0, 99999는 허용한다.
Series 생성 필수 image; request의 keyword는 문자열; 성공 data=null keywords 배열을 보내지 않고 목록으로 이동해 재조회한다.
Series 장르 query/body 없는 활성 장르 목록; data=[{id,genre,isAdult}] idgenreId 선택값으로 사용하고 0을 유효값으로 허용하지 않는다.
Series 상세 목록과 같은 SeriesListItem; genreId, enum publishedDaysOfWeek, enum state 직접 수정 form을 원본값으로 초기화하고 create-only keyword를 상세·수정 field로 만들지 않는다.
Series 연결·순서 후보 contents/search; 연결 {contentIdList}; 순서 {ids} {contentIds}, {seriesIds}를 보내지 않는다.
Community 목록 timezone query 없음; data={totalCount,page,size,hasNext,items} audioUrl과 서버 pagination metadata를 그대로 사용한다.
Community 생성·수정 생성 part postImage/audioFile; 수정은 postImage만 교체 가능; mutation data=null image, audioSignedUrl, 수정 audio/price field를 만들지 않는다.
FanTalk 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT이 모두 구현됨 creatorReplies의 빈 배열 여부로 작성/수정을 나누고 creatorReplies[].fanTalkId를 PUT의 replyId로 사용한다. 별도 상세·filter/sort와 중복 오류 key 분기는 현재 UI 범위에서 제외한다.
댓글 Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 제공 writerId === creatorId일 때만 수정 UI를 노출하고 삭제는 작성자와 관계없이 제공한다.

11.5 백엔드 계약 제공·대기 상태

아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 backend 계약의 제공·해결 상태를 추적한다. 해결된 항목은 완료된 Phase를 묵시적으로 다시 열지 않고 plan-task.md의 완료된 Phase 10 후속 vertical slice와 남은 수동 QA에서 검증한다. EXT-006은 현재 확정 기능 구현을 차단하지 않으며 정식 OpenAPI 추적성만 후속으로 관리한다.

ID 우선순위 백엔드 제공 필요 계약 사용하는 화면 관련 API 흐름 현재 프론트엔드 영향
EXT-001 해결 GET /api/v2/admin/ai-characters/original-works/search?searchTerm=...OriginalWorkSearchItem[]가 OpenAPI에 implemented로 추가됐다. 캐릭터 생성 /ai-characters/new, 캐릭터 수정 /ai-characters/:characterId/profile 원작 검색 GET과 Character create/update의 originalWorkId legacy 후보 대신 v2 lookup을 사용하는 선택기·contract test를 Phase 10에서 구현 완료했다.
EXT-002 해결 query/body 없는 GET /api/v2/admin/ai-characters/series-genres{id,genre,isAdult}[]가 OpenAPI에 implemented로 추가됐다. 시리즈 생성 /ai-characters/:characterId/series/new, 수정 /ai-characters/:characterId/series/:seriesId/edit 장르 목록 GET과 Series create/update의 genreId 장르 선택과 Series 생성·수정 후속 구현을 Phase 10에서 완료했다.
EXT-003 필요 없음 별도 Series 수정 DTO 또는 표시 문자열 mapping 계약 시리즈 수정 /ai-characters/:characterId/series/:seriesId/edit, 시리즈 상세 직접 진입 GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}SeriesListItem과 같은 genreId, enum publishedDaysOfWeek, enum state를 내려주면 충분하다. 별도 외부 의존으로 추적하지 않는다. 수정 화면은 상세 응답으로 기존 값을 초기화하고 장르 API는 option 목록 표시용으로만 사용한다.
EXT-004 해결 답변 유무는 creatorReplies의 빈 배열 여부로 판정한다. 답변 PUT과 팬 원글 DELETE가 implemented이며 PUT path의 replyIdcreatorReplies[].fanTalkId를 사용한다. 별도 상세·filter·sort와 중복 오류 key는 현재 UI에 필요하지 않아 FANTALK-002, FANTALK-008에서 제외했다. FanTalk 목록 /ai-characters/:characterId/fan-talks, FanTalk item Sheet/panel 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT을 사용한다. 목록 item이 Sheet source이고 backend 반환 순서를 유지하므로 별도 상세 GET·filter·sort query를 요청하지 않는다. Phase 10에서 답변 수정·팬 원글 삭제의 mock/client integration을 구현 완료했다. 실제 개발 API 수동 QA에서 동시 POST 후 활성 답변 1개 유지 등 server 수용 기준을 분리 확인한다.
EXT-005 해결 Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 operation과 DTO가 OpenAPI에 implemented로 추가됐다. writerId === creatorId이면 AI 작성 댓글로 판정한다. 오디오 상세 /ai-characters/:characterId/audio-contents/:contentId, 커뮤니티 Sheet /ai-characters/:characterId/community-posts 두 target의 comments, comments/{commentId}, comments/{commentId}/replies Phase 10에서 2단계 thread, 작성·수정·삭제, Comments mock/client integration을 구현 완료했다. 팬 작성 댓글은 수정하지 않고 관리자 DELETE만 제공한다.
EXT-006 P0 비차단 현재 구현된 POST /admin/member/login, POST /member/logout의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 로그인 /login, 보호 shell 로그아웃 POST /admin/member/login, POST /member/logout 현재 구현과 Phase 1 회귀는 유지한다. 정식 OpenAPI 추적성만 대기하며 Phase 3~9를 차단하지 않는다.
EXT-007 해결 Character 검색 결과와 Audio·Series·Community 목록 API는 isActive=true인 항목만 반환한다. 캐릭터 목록, 오디오 목록, 시리즈 목록, 커뮤니티 목록 각 목록 GET: /ai-characters, /audio-contents, /series, /community-posts 프론트엔드 변경 없음. 활성 query·client-side filter를 만들지 않고 soft delete 후 목록 재조회 결과를 server integration에서 확인한다.
EXT-008 해결 Community 목록 응답이 data={totalCount,page,size,hasNext,items}로 보완됐다. 커뮤니티 목록 /ai-characters/:characterId/community-posts GET /api/v2/admin/ai-characters/{characterId}/community-posts Phase 10에서 기존 배열 parser와 timezone query 제거, 서버 pagination metadata 기반 UI·test 갱신을 완료했다.
EXT-009 해결 price 최대값은 99999 캔이며 관련 OpenAPI request schema에도 minimum=0, maximum=99999를 명시했다. 오디오 생성·수정, 커뮤니티 생성 Audio POST/PUT .../audio-contents, Community POST .../community-posts Phase 10에서 schema·form과 경계값 test를 0..99999 정수로 갱신 완료했다.
EXT-010 해결 파일 정책은 FILE-001~015로 확정됐다. 이미지 비율·crop 결과는 client가 보장하고 backend는 검증하지 않는다. 파일 용량, MIME 등 서버에서 확인 가능한 항목은 backend가 동일하게 검증한다. 캐릭터 생성·수정, 오디오 생성·수정, 시리즈 생성, 커뮤니티 생성·수정 각 multipart 생성·수정 API의 image/audio file part 외부 의존에서 제외한다. client는 기존 사전 검증을 유지하고, server integration에서는 용량·MIME reject만 확인한다.
EXT-011 해결 OpenAPI에 명시된 공통 오류 envelope/status만 도메인 화면에서 사용한다. OpenAPI 밖의 도메인별 오류 key는 추정하지 않고, 미정의 오류는 알 수 없는 오류가 발생했습니다.로 표시한다. Character, Audio, Series, Community, FanTalk의 form·list·detail·mutation 화면 각 도메인 API의 비2xx 응답 status, message, errorProperty 기능별 정확한 message key 분기가 필요한 inline 오류·충돌·중복·missing-id 처리는 후속 계약 전까지 만들지 않는다.

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를 사용한다.

{
  "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/logoutAuthorization: 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 검증 정책은 백엔드 책임이다.

감사 로그 미결 사항과 권고

감사 로그 조회 UI는 현재 릴리스에서 제외한다.

처리: 이번 범위에서는 백엔드 감사 기록을 우선한다. 조회 화면이 필요해지면 조회 endpoint, 권한, 필터 계약을 포함한 별도 Phase로 다시 계획한다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.

13. 성능과 품질 요구사항

  • 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 totalCount를 기준으로 모든 page를 읽는다. Community는 서버가 제공하는 totalCount, page, size, hasNext metadata로 전체 건수와 다음 page 여부를 표시한다.
  • 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
  • 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
  • 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
  • 목록 검색 debounce 시간은 구현 시 300ms 전후로 일관되게 적용한다.
  • 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
  • 브라우저 지원 범위는 데스크톱 Chrome과 모바일 Chrome이다. 로컬 자동 Gate는 Chromium/mobile Chrome project로 검증한다.
  • 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가 필수 imagerequest를 보내고 request에 name, systemPrompt, description이 포함되며 isActiveexternalCharacterId는 포함되지 않는다.
  • Character, Audio, Series, Community의 일반 수정은 isActive를 생략하고 soft delete에만 isActive=false를 보내며 true는 전송하지 않는다.
  • Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 재조회된 각 서버 목록에는 isActive=false인 항목이 없어야 하며 클라이언트 필터로 이 결과를 만들지 않는다.
  • 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
  • 워크스페이스 진입·복원용 상세 성공 응답으로 isActive=false인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 목록 이동·재조회를 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
  • 오디오 생성에서 즉시 공개는 releaseDate=null을 보내고, 예약 공개는 미래 Asia/Seoul 입력을 클라이언트에서 ISO-8601 UTC Z로 변환해 보낸다. 생성·상세·커뮤니티 목록 요청에는 timezone field/query가 없어야 한다.
  • MP3, AAC, M4A 업로드의 진행률·취소·재시도와 1,024,000,000 bytes 허용·1,024,000,001 bytes 거부 경계 검증이 동작한다.
  • 오디오 콘텐츠와 커뮤니티 첨부 audio가 동일한 확장자·MIME·최대 크기·재생 길이 정책을 사용한다.
  • .m4aaudio/mp4audio/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에 포함한다.
  • 캐릭터 원작 선택기는 필수 searchTerm으로 v2 원작 검색 API를 호출하고 선택한 OriginalWorkSearchItem.id를 create/update의 originalWorkId로 보낸다.
  • media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
  • 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
  • 오디오 생성·수정과 커뮤니티 생성의 가격은 0, 99999를 허용하고 -1, 100000, 소수를 제출 전에 거부한다.
  • Series에 OPEN이나 MONDAY 같은 잘못된 값을 보내지 않는다.
  • Series 생성에는 state를 보내지 않고 keyword 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
  • RANDOM과 실제 요일을 동시에 선택할 수 없다.
  • 시리즈 장르 목록을 query/body 없이 조회하고 선택한 SeriesGenreItem.id를 create/update의 genreId로 보내며, 상세의 genreId·요일 enum·state enum으로 수정 form을 초기화한다.
  • 커뮤니티 목록은 timezone 없이 page, size를 보내고 서버의 totalCount, page, size, hasNext, items를 사용한다.
  • FanTalk 목록 item의 creatorReplies가 비어 있으면 답변 작성, 비어 있지 않으면 답변 수정 UI를 제공한다. 수정 path의 replyId에는 첫 답변의 fanTalkId를 사용하고 client의 두 번째 POST를 차단한다. 실제 server에서는 동일 root에 대한 동시 POST 후 재조회해도 활성 creator reply가 하나만 존재해야 하며, 정확한 충돌 status/message key를 client에서 분기하지 않는다.
  • 팬 작성 FanTalk 원글 soft delete가 성공하면 목록을 재조회하고 열린 Sheet를 닫는다. 연결된 creator reply가 함께 삭제됐다고 가정하지 않는다.
  • 댓글은 루트와 직접 답글의 2단계를 넘지 않는다. writerId === creatorId인 댓글에만 수정 UI를 제공하고, soft delete는 작성자와 관계없이 해당 row에 제공한다.
  • 모바일에서 조회·오디오 재생, 댓글 전체 관리, FanTalk 답변 작성·수정과 팬 원글 soft delete가 가능하다.
  • 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 검증 항목을 확인한다.
  • 공통 오디오 플레이어가 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에서 동일한 compact audio-only UI로 표시되고, 320px와 200% zoom에서도 핵심 control이 가려지거나 가로 overflow를 만들지 않는다.

15. Open Questions와 결정 절차

ID 상태 결정 필요 사항 현재 권고
OQ-009 확정 각 페이지의 문자열 최대 길이와 배열 최대 개수 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다.
OQ-010 제외 감사 로그 UI 제공 여부 현재 릴리스에서는 조회 UI를 만들지 않고 backend 기록을 우선한다. 포함 시 backend 조회 계약을 포함한 별도 Phase로 계획한다.

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 오디오 상태는 OPENSCHEDULED이며 서버가 계산한다.
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.mdapi-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, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. Series 수정 form은 상세 응답이 list item과 같은 genreId, enum 요일 배열, enum state를 제공하면 별도 edit DTO 없이 초기화한다.
2026-07-28 EXT-003은 별도 backend 계약 항목에서 제외한다. 시리즈 상세 API가 SeriesListItem과 같은 수정용 원본값을 내려주고, 장르 API는 선택 option 목록 표시용으로 호출하는 방식으로 처리한다.
2026-07-28 OQ-009는 초기 UI를 먼저 구현하고 실제 페이지를 보며 문자열 최대 길이와 배열 최대 개수를 제안한 뒤 backend 호환을 확인해 확정하는 절차로 종결한다. 실제 최대값 확정 전에는 임의 상한을 추가하지 않는다.
2026-07-28 EXT-006에 현재 구현된 POST /admin/member/loginPOST /member/logout의 request·response·인증·client 처리 기준을 기록한다. 정식 OpenAPI 포함은 비차단 외부 의존이며 기존 인증 구현과 Phase 3~9 진행을 유지한다.
2026-07-28 Phase 3부터는 OpenAPI 제공 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행한다. 미제공 계약은 영향을 받는 기능만 후속 범위로 남기고, 계약 도착 후 별도 vertical slice와 관련 Phase Gate·Phase 9를 다시 실행한다.
2026-07-28 감사 로그 조회 UI는 현재 릴리스에서 제외한다. backend 감사 기록을 우선하고, 조회 UI가 필요해지면 조회 endpoint·권한·필터 계약을 포함한 후속 Phase로 다시 계획한다.
2026-07-29 EXT-010은 해결로 정정한다. 이미지 비율·crop 결과는 backend가 검증하지 않고 client가 보장하며, 파일 용량·MIME 등 서버에서 확인 가능한 항목은 backend가 FILE-001~015와 동일하게 검증한다.
2026-07-29 OpenAPI 2.3.0에 추가된 원작 검색, 활성 장르 목록, Audio·Community 댓글, Community pagination, FanTalk 팬 원글 DELETE와 답변 PUT을 후속 구현 계약으로 채택한다. 목록 API는 서버가 isActive=true 항목만 반환하므로 client 활성 filter를 추가하지 않는다.
2026-07-29 오디오 예약 공개는 Asia/Seoul 입력을 client에서 ISO-8601 UTC Z로 변환해 releaseDate에 보내고 timezone field/query를 제거한다. 즉시 공개는 releaseDate=null을 유지한다.
2026-07-29 FanTalk 답변 여부는 creatorReplies의 빈 배열 여부로 판단하고 수정 path의 replyId에는 creatorReplies[].fanTalkId를 사용한다. 댓글은 writerId === creatorId일 때 AI 캐릭터 작성으로 판단하며 팬 작성 FanTalk 원글 soft delete를 이번 후속 구현 범위에 포함한다.
2026-07-29 백엔드에서 FanTalk 답변 수정 API가 이미 구현됐고 OpenAPI status만 누락됐음을 확인했다. 해당 operation을 implemented로 정정하고 Phase 10에서 mock/client뿐 아니라 실제 server integration까지 구현·검증한다.
2026-07-29 EXT-009의 가격 범위 0..99999를 Audio 생성·수정과 Community 생성 OpenAPI request schema, 요구사항, Phase 10 경계 test에 동일하게 적용한다.
2026-07-29 현재 FanTalk UI는 목록 item 기반 Sheet와 backend 반환 순서만 사용하므로 별도 상세 GET·답변 상태 filter·sort query가 필요하지 않다고 확정했다. 중복 생성 전용 오류 key 분기도 제외하고 일반 오류 후 목록을 재조회한다. 답변 1개 불변식은 FANTALK-003의 backend 수용 기준으로 server integration에서 검증하므로 EXT-004를 해결로 종결한다.
2026-07-31 사용자 직접 지시에 따라 로컬 자동 Gate와 지원 browser 범위는 데스크톱 Chrome·모바일 Chrome의 Chrome 2종으로 한정한다. Playwright는 Chromium/mobile Chrome project만 유지하고, 현재 제품 지원 대상이 아닌 WebKit·Mobile Safari 자동 실행과 수동 QA는 테스트 시간을 크게 늘리므로 현재 릴리스 범위에서 제외한다.
2026-08-03 공통 AdminAudioPlayer의 native media 동작과 단일 재생·오류 계약은 유지하고, Plyr·Media Chrome의 audio-only control 배치를 참고한 compact 가로형 UI로 개선한다. 플레이어 내부 image·video 영역과 새 외부 라이브러리·waveform은 추가하지 않으며 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일하게 적용한다.