30 KiB
AI 캐릭터 관리자 웹 정규화 API Contract
문서 정보
| 항목 | 내용 |
|---|---|
| 상태 | 인터뷰 보정 반영, 백엔드 제공 대기 계약 분리 |
| 작성일 | 2026-07-25 |
| 기준 | 사용자 제공 API Contract + 인터뷰 확정사항 |
| 관련 요구사항 | prd.md |
| 관련 구현 계획 | plan-task.md |
이 문서는 대화로 제공된 API Contract를 저장소에 영속적으로 보존하고 인터뷰에서 확정된 보정사항을 적용한 프론트엔드 기준 계약이다. 실제 백엔드 구현을 다른 저장소에서 추정하지 않는다. “백엔드 제공 대기”로 표시한 endpoint와 세부 규칙은 프론트엔드 Open Question이 아니며, 백엔드 계약이 제공되기 전에는 해당 network integration을 구현하지 않는다.
goal 실행 시 계약 입력은 해당 Task의 시작 조건이 가리키는 이 문서 section을 사용한다. 제공 대기 계약을 추정해 goal을 완료하지 않으며, 계약이 새로 제공되거나 제외 결정이 나면 PRD 결정 기록 → 이 문서 → plan-task.md 순서로 갱신한다.
1. 공통 규칙
1.1 성공 응답
{
"success": true,
"message": null,
"data": {}
}
- 모든 성공 응답은
ApiResponse.ok(...)wrapper를 사용한다. - 인증 endpoint의 실제 성공 응답은 최상위
errorProperty=null도 포함한다. 프론트엔드 공통 type은 성공 응답의errorProperty가 없거나null인 두 형태를 수용한다.
1.2 오류 응답
{
"success": false,
"message": "잘못된 요청입니다.",
"data": null,
"errorProperty": null
}
- 모든 오류는 의미에 맞는 비2xx HTTP status와
ApiResponse.error(...)wrapper를 사용한다. - 오류를 2xx로 normalize하지 않는다.
Accept-Language: ko|en|ja에 따라 KO/EN/JA message를 반환한다.- header가 없거나 지원하지 않는 언어면 KO로 fallback한다.
- security filter 단계도 MVC interceptor에 의존하지 않고
Accept-Languageheader를 직접 해석한다. - 이 관리자 웹은 모든 요청에
Accept-Language: ko를 보낸다.
| 오류 | HTTP status | message key |
|---|---|---|
| JWT 없음·잘못됨·만료·폐기 | 401 | common.error.bad_credentials |
| JWT role 비ADMIN | 403 | common.error.access_denied |
| JWT ADMIN + 현재 DB role 비ADMIN stale claim | 403 | common.error.access_denied |
| request binding·target 미존재·creatorMember 누락·role/memberKind 불변식 위반 | 400 | common.error.invalid_request |
| 신규 prefix 미매핑 경로 | 404 | common.error.invalid_request |
| 지원하지 않는 HTTP method | 405 | common.error.invalid_request |
| 지원하지 않는 media type | 415 | common.error.invalid_request |
| 예상하지 못한 서버 오류 | 500 | common.error.unknown |
Phase 2~6에서 추가되는 domain/client/server 오류는 구현 전에 정확한 비2xx status와 KO/EN/JA message key를 확정한다. 신규 prefix 전용 오류 처리는 legacy/public endpoint에 적용하지 않는다.
1.3 페이지 응답
모든 목록·검색 endpoint는 page 기본 0, size 기본 20, 최소 20, 최대 50 보정을 적용한다.
type PageData<T> = {
totalCount: number
page: number
size: number
hasNext: boolean
items: T[]
}
1.4 인터뷰 공통 보정
characterId는 선택된 target resource endpoint의 외부 대상 식별자다.- 캐릭터 목록·검색과 생성에는 path
characterId가 없다. externalCharacterId는 존재하지 않으므로 모든 request, response, UI에서 제거한다.- 생성 request에는
isActive를 보내지 않는다. 최초 상태는 백엔드가 결정한다. - 일반 수정 request에는
isActivekey를 보내지 않는다. soft delete request에만isActive=false를 보내며isActive=true는 전송하지 않는다. - 비활성 resource의 복원과 hard delete는 현재 범위가 아니다.
- multipart의 JSON part 이름은
request다. - optional 교체 파일을 보내지 않으면 기존 파일을 유지한다. 기존 media 자체를 제거하는 contract는 없다.
- Character, Audio, Series, Community 목록 endpoint는 활성 resource만 반환하며
isActive,activeStatus등 활성 상태 query를 받지 않는다. - Character, Audio, Series의 soft delete 성공 응답을 받으면 프론트엔드는 관련 active-only 목록 query를 무효화·재조회해 비활성화한 항목을 표시하지 않고 목록으로 이동해 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신해 해당 항목을 제거한다.
- 비활성 ID 상세 GET의 반환·거부 정책은 백엔드 책임이며 이 프론트엔드 계약에서 규정하거나 구현 Gate로 관리하지 않는다. 클라이언트는 성공 envelope가 오면 반환 데이터를 사용하고 비2xx이면 공통 오류 처리를 적용한다.
- 오디오 media error는 signed URL 만료로 구분하거나 추정하지 않는다. 재생 오류만으로 목록·상세 API를 자동 재조회하거나
play()를 자동 재호출하지 않으며 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. - 커뮤니티 첨부 audio URL만을 갱신하기 위한 요청은 하지 않고 media error를 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회에서는 새 응답의 URL을 사용한다. 전용 상세 조회나 URL 재발급 호출을 추가하지 않는다.
1.5 개발 전용 Mock Preview 계약 경계
- browser MSW handler와 fixture는 이 문서에 제공됨으로 기록된 endpoint, request/response DTO와 오류 규칙에서만 파생한다.
- mock mode도 production과 같은 URL, method, header, serializer, envelope와 API client를 사용하며 별도 mock 전용 DTO·adapter를 만들지 않는다.
- 계약은 제공됐지만 backend endpoint가 아직 404인 경우 mock mode에서 최종 UI를 확인할 수 있다. server mode의 404·network error를 감지해 mock으로 자동 fallback하지 않는다.
- “백엔드 제공 대기” 항목은 fixture로 추정하지 않는다. 계약이 제공되기 전에는 관련 최종 network UI와 integration 완료를 주장하지 않는다.
- mock mutation은 deterministic in-memory store를 갱신하고 새로고침 때 seed로 초기화한다. domain fixture를 browser 영구 저장소에 기록하지 않는다.
- mock mode는 개발 환경에서만 명시적으로 활성화하고 production build에서는 거부한다.
- mock UI Gate 통과는 실제 backend 연동 완료 증거가 아니다. 각 도메인은
UI 확인 완료(mock)와실제 서버 연동 완료(server)결과를 별도로 기록한다.
2. 공통 enum과 형식
type AudioContentStatus = "OPEN" | "SCHEDULED"
type SeriesState = "PROCEEDING" | "SUSPEND" | "COMPLETE"
type SeriesPublishedDay =
| "SUN"
| "MON"
| "TUE"
| "WED"
| "THU"
| "FRI"
| "SAT"
| "RANDOM"
OPEN은 현재 출시된 오디오 콘텐츠다.SCHEDULED는 미래 공개가 예약된 오디오 콘텐츠다.- Audio status는 백엔드가 선택해 반환하며 프론트엔드가 계산하지 않는다.
RANDOM은 다른 요일과 함께 보낼 수 없다.- published days는 RANDOM 단독 또는 하나 이상의 실제 요일 목록이다.
- 모든
*AtUtc,releaseDateUtc는 ISO-8601 UTCZ문자열이다. - 가격은 0 이상의 정수이며 단위는 “캔”이다.
2.1 이미지 업로드와 crop
모든 image part의 원본 파일 크기는 최대 10,485,760 bytes다. 10,485,760 bytes는 허용하고 10,485,761 bytes부터 거부한다. 확장자 문자열만 신뢰하지 않고 실제 MIME을 함께 검증한다.
| resource | 허용 확장자 | 허용 MIME | crop aspect ratio | 등록 결과 최대 가로 폭 | 결과 세로 |
|---|---|---|---|---|---|
| 캐릭터 | .jpg, .jpeg, .png |
image/jpeg, image/png |
1:1 |
800px | 가로와 동일 |
| 커뮤니티 JPEG/PNG | .jpg, .jpeg, .png |
image/jpeg, image/png |
자유 | 800px | 선택한 crop ratio에 따라 결정 |
| 커뮤니티 GIF | .gif |
image/gif |
crop 없음 | 800px | 원본 비율 유지 |
| 시리즈 | .jpg, .jpeg, .png |
image/jpeg, image/png |
210:297 |
1,000px | 비율에 따라 결정 |
| 오디오 콘텐츠 cover | .jpg, .jpeg, .png |
image/jpeg, image/png |
1:1 |
800px | 가로와 동일 |
- WebP와 표에 없는 image 형식은 허용하지 않는다.
- GIF는 커뮤니티 image에서만 허용하며 캐릭터, 시리즈, 오디오 콘텐츠 cover에서는 거부한다.
- 캐릭터, 커뮤니티 JPEG/PNG, 시리즈, 오디오 콘텐츠에서 새 image를 선택하면 resource별 비율로 crop한 결과 File을 기존 multipart file part에 보낸다. crop 좌표나 비율을 별도 request field로 추가하지 않는다.
- 커뮤니티 GIF는 crop Dialog를 열거나 canvas로 재처리하지 않는다. 원본 가로가 800px 이하인 경우에만 원본 비율·animation 유지 File을 기존 multipart
imagepart에 보낸다. - 수정 화면에서 새 image를 선택하지 않거나 crop을 취소하면 해당 file part를 보내지 않으며 기존 image를 유지한다.
- 서버는 파일 크기, 실제 MIME, 결과 pixel 크기와 고정 비율을 다시 검증해야 한다.
- “가로 800/1,000”은 등록 결과의 최대 출력 폭이다. JPEG/PNG crop 결과에는 이 제한을 적용하되 선택된 원본 crop 영역보다 확대하지 않는다.
- Series crop 결과의 세로 pixel은
round(width × 297 ÷ 210)으로 계산한다. 최대 폭에서는 1,000×1,414px이며 server 비율 검증은 계산된 세로값 기준 1px 이내 오차를 허용한다. - 커뮤니티 GIF의 원본 가로가 800px을 초과하면 client가 제출을 차단하고 server도
400과common.error.invalid_requestenvelope로 거부한다. GIF를 축소·crop·재인코딩하지 않는다.
3. 인증
인증 endpoint는 AI 캐릭터 관리자 신규 prefix가 아니라 기존 admin/common member 경로를 사용한다.
3.1 Admin Login
POST /admin/member/login
Content-Type: application/json
Authorization header: 없음
Request body:
{
"email": "admin@test.com",
"password": "password"
}
Response:
{
"success": true,
"message": null,
"data": {
"token": "jwt-token",
"role": "ADMIN"
},
"errorProperty": null
}
- 보호 route에 진입하려면
data.role이ADMIN이어야 한다. data.token은 보호 API의 Bearer token으로 사용한다.
3.2 Admin Logout
관리자 전용 로그아웃 endpoint는 없다. 공통 로그아웃 endpoint를 사용한다.
POST /member/logout
Headers:
Authorization: Bearer {jwt-token}
- Request body 없음
Response:
{
"success": true,
"message": null,
"data": {},
"errorProperty": null
}
3.3 클라이언트 인증 규칙
- 로그인 요청에는 Authorization header를 보내지 않는다.
- 로그인 이후 보호 API와 로그아웃에는
Authorization: Bearer {jwt-token}을 보낸다. - refresh token과 자동 갱신은 사용하지 않는다.
- 401이면 인증 상태를 제거하고 로그인으로 이동한다.
- 로그인 성공 시
data.token과data.role="ADMIN"을sessionStorage에 저장한다. - 앱 시작과 같은 탭의 새로고침에서는
sessionStorage의 token과 role을 읽어 session을 복원한다. 값이 없거나 role이ADMIN이 아니면 보호 route에 진입시키지 않는다. - 로그아웃 요청은 현재 Bearer token으로 한 번 호출한다. 성공 여부와 관계없이
sessionStorage의 인증 정보를 제거하고 로그인 화면으로 이동한다. - 로그아웃이 네트워크 오류 또는 비2xx로 실패하면 서버 로그아웃 확인 실패 경고를 표시하고 제거한 session을 복원하지 않는다.
- 401 처리 시에도
sessionStorage의 인증 정보를 제거하고 로그인 화면으로 이동한다. - 인증 정보를
localStorage, IndexedDB 또는 cookie로 복제하지 않는다. 탭 종료 시에는sessionStorage의 브라우저 lifecycle을 따른다.
4. 캐릭터
4.1 목록·검색
GET /api/v2/admin/ai-characters?search=루나&page=0&size=20
- 활성 캐릭터만 반환한다. 활성 상태 filter/query는 없다.
Query:
{
search?: string
page?: number
size?: number
}
Response data:
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"characterId": 101,
"name": "루나",
"description": "달빛을 좋아하는 AI 캐릭터",
"imageUrl": "https://cdn.example.com/characters/luna.png",
"creatorMemberId": 9001,
"creatorNickname": "루나",
"originalWorkId": 31,
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z"
}
]
}
4.2 상세
GET /api/v2/admin/ai-characters/{characterId}
Response data:
{
"characterId": 101,
"name": "루나",
"description": "달빛을 좋아하는 AI 캐릭터",
"imageUrl": "https://cdn.example.com/characters/luna.png",
"creatorMemberId": 9001,
"creatorNickname": "루나",
"creatorProfileImageUrl": "https://cdn.example.com/characters/luna.png",
"creatorIntroduce": "달빛을 좋아하는 AI 캐릭터",
"originalWorkId": 31,
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
4.3 생성
POST /api/v2/admin/ai-characters
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
File | 아니요 |
request |
application/json |
예 |
request:
{
name: string
description: string
originalWorkId?: number | null
}
isActive와externalCharacterId를 보내지 않는다.originalWorkId는 UI에서 선택 사항이다. 미선택 값을null로 보낼지 key를 생략할지는 백엔드 canonical form 확정이 필요하다.- 백엔드는 새
ChatCharacter와 연결된creator(memberKind = AI_CHARACTER)를 함께 생성한다. - Response
data는 캐릭터 상세와 같다.
4.4 수정·비활성화
PUT /api/v2/admin/ai-characters/{characterId}
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
교체 File | 아니요 |
request |
application/json |
예 |
request:
{
name: string
description: string
originalWorkId?: number | null
isActive?: false
}
image미전송은 기존 이미지 유지다.- 기존 이미지 자체를 제거하는 flag는 제공되지 않았다.
- 일반 수정에서는
isActive를 생략한다.isActive=false는 soft delete 전용이며true는 보내지 않는다. - 현재 백엔드는 name/description/image 변경을 creator nickname/introduce/profile image에 동기화한다.
- creator 상황에 따른 조건부 동기화는 다음 백엔드 범위이며 현재 프론트엔드가 분기하지 않는다.
- Response
data는 캐릭터 상세와 같다. - soft delete 성공 후 캐릭터 active-only 목록으로 이동하고 성공 알림을 표시한다.
5. 오디오 콘텐츠
5.0 오디오 콘텐츠 테마 목록
GET /api/v2/admin/ai-characters/audio-content-themes
- Query 없음
- Request body 없음
Response data:
[
{
"themeId": 11,
"themeName": "ASMR",
"imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png"
}
]
- 오디오 콘텐츠 생성 UI는 이 목록에서 테마를 선택하게 하고, 선택한
themeId를 생성 request JSON에 포함한다. - 테마 목록은 선택 캐릭터 path를 포함하지 않는 공통 관리자 조회다.
5.1 목록·검색
GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20
- 활성 오디오만 반환한다. 활성 상태 filter/query는 없으며
status는 공개 상태 필터다.
Query:
{
search?: string
status?: "OPEN" | "SCHEDULED"
page?: number
size?: number
}
Response data:
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"contentId": 501,
"title": "밤 산책",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
"price": 1000,
"isAdult": false,
"isActive": true,
"releaseDateUtc": "2026-07-25T00:00:00Z",
"status": "OPEN"
}
]
}
- status 미전송 시 어떤 집합을 반환할지는 백엔드가 결정한다.
5.2 상세
GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}
Response data:
{
"contentId": 501,
"title": "밤 산책",
"description": "조용한 밤 산책 오디오",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
"price": 1000,
"isAdult": false,
"isActive": true,
"releaseDateUtc": "2026-07-25T00:00:00Z",
"status": "OPEN",
"seriesIds": [701],
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
5.3 생성
POST /api/v2/admin/ai-characters/{characterId}/audio-contents
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
coverImage |
File | 예 |
audioFile |
File | 예 |
request |
application/json |
예 |
request:
{
title: string
description: string
themeId: number
price: number
isAdult: boolean
releaseDateUtc: string | null
seriesIds: number[]
}
isActive와status를 보내지 않는다.themeId는 필수이며GET /api/v2/admin/ai-characters/audio-content-themes응답에서 선택한 값이다.- 즉시 공개는
releaseDateUtc=null이다. - 예약 공개는 Asia/Seoul 미래 시각을 UTC
Z문자열로 변환해 보낸다. - Response
data는 오디오 상세와 같다.
5.4 수정·비활성화
PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
coverImage |
교체 File | 아니요 |
audioFile |
교체 File | 아니요 |
request |
application/json |
예 |
request:
{
title: string
description: string
price: number
isAdult: boolean
isActive?: false
releaseDateUtc: string | null
seriesIds: number[]
}
- 교체 File 미전송은 기존 media 유지다.
- 일반 수정에서는
isActive를 생략한다.isActive=false는 soft delete 전용이며true는 보내지 않는다. - Response
data는 오디오 상세와 같다. - soft delete 성공 후 선택 캐릭터의 오디오 active-only 목록으로 이동하고 성공 알림을 표시한다.
5.5 업로드 규칙
- 오디오 콘텐츠 확장자:
.mp3,.aac,.m4a - canonical MIME:
audio/mpeg,audio/aac,audio/mp4 - compatibility MIME:
audio/x-m4a는.m4a파일에 한해 허용 - 최대 파일 크기: decimal 1,024MB,
1,024,000,000 bytes이하 - byte 경계:
1,024,000,000허용,1,024,000,001거부 - 최대 재생 길이: 제한 없음
- WAV: 미지원
- 서버는 확장자와 MIME 외에 실제 container/codec을 검증해야 한다.
audio/x-m4a가.m4a가 아닌 확장자와 조합되거나 실제 MP4/M4A container·codec 검증에 실패하면415와common.error.invalid_requestenvelope로 거부한다.- cover image의 형식,
1:1crop과 최대 800px 출력 규칙은2.1 이미지 업로드와 crop을 따른다.
6. 시리즈
6.1 목록
GET /api/v2/admin/ai-characters/{characterId}/series?page=0&size=20
- 활성 시리즈만 반환한다. 활성 상태 filter/query는 없다.
Query:
{
page?: number
size?: number
}
Response data:
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"seriesId": 701,
"title": "루나의 밤",
"introduction": "밤을 주제로 한 시리즈",
"coverImageUrl": "https://cdn.example.com/series/701.png",
"genreId": 3,
"isAdult": false,
"state": "PROCEEDING",
"isActive": true,
"orders": 1
}
]
}
6.2 상세
GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}
Response data:
{
"seriesId": 701,
"title": "루나의 밤",
"introduction": "밤을 주제로 한 시리즈",
"coverImageUrl": "https://cdn.example.com/series/701.png",
"publishedDaysOfWeek": ["MON", "WED"],
"genreId": 3,
"keywords": ["밤", "산책"],
"isAdult": false,
"state": "PROCEEDING",
"isActive": true,
"writer": "루나",
"studio": "소다라이브",
"orders": 1
}
6.3 생성
POST /api/v2/admin/ai-characters/{characterId}/series
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
File | 아니요 |
request |
application/json |
예 |
request:
{
title: string
introduction: string
publishedDaysOfWeek: SeriesPublishedDay[]
genreId: number
keywords: string[]
isAdult: boolean
writer: string
studio: string
}
state와isActive를 보내지 않는다.- 초기 state는 백엔드가 결정한다. 프론트엔드는 정확한 기본값에 의존하지 않고 생성 응답의
state를 그대로 표시한다. - Response
data는 시리즈 상세와 같다.
6.4 수정·비활성화
PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
교체 File | 아니요 |
request |
application/json |
예 |
request:
{
title: string
introduction: string
publishedDaysOfWeek: SeriesPublishedDay[]
genreId: number
keywords: string[]
isAdult: boolean
state?: "PROCEEDING" | "SUSPEND" | "COMPLETE"
isActive?: false
writer: string
studio: string
}
- state를 변경하지 않을 때는 key를 생략한다.
null을 보내지 않는다. - image 미전송은 기존 이미지 유지다.
- 일반 수정에서는
isActive를 생략한다.isActive=false는 soft delete 전용이며true는 보내지 않는다. - Response
data는 시리즈 상세와 같다. - soft delete 성공 후 선택 캐릭터의 시리즈 active-only 목록으로 이동하고 성공 알림을 표시한다.
6.5 시리즈 콘텐츠 조회
GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents?search=밤&page=0&size=20
Query:
{
search?: string
page?: number
size?: number
}
Response data:
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"contentId": 501,
"title": "밤 산책",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"isAdult": false,
"orders": 1
}
]
}
6.6 시리즈 콘텐츠 연결
POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents
Request body:
{
"contentIds": [501, 502]
}
Response data는 시리즈 상세와 같다.
6.7 시리즈 콘텐츠 연결 해제
DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}
- Request body 없음
- Response
data는 시리즈 상세와 같다.
6.8 시리즈 순서 변경
PUT /api/v2/admin/ai-characters/{characterId}/series/orders
Request body:
{
"seriesIds": [701, 702, 703]
}
- 활성 시리즈 전체의 ID를 최종 순서대로 보낸다.
- 50개를 초과할 때 전체를 읽는 방식, 누락 ID 오류, 동시 변경 충돌 처리는 백엔드 제공 대기 계약이다.
- Response
data는 시리즈 목록과 같은 page envelope다.
7. 커뮤니티 게시글
7.1 목록
GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20
- 활성 게시글만 반환한다. 활성 상태 filter/query는 없다.
Query:
{
page?: number
size?: number
}
Response data:
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"postId": 801,
"content": "오늘의 소식입니다.",
"imageUrl": "https://cdn.example.com/community/801.png",
"audioSignedUrl": null,
"price": 0,
"isAdult": false,
"isFixed": true,
"fixedAtUtc": "2026-07-24T00:00:00Z",
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z"
}
]
}
7.2 등록
POST /api/v2/admin/ai-characters/{characterId}/community-posts
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
File | 아니요 |
audioFile |
File | 아니요 |
request |
application/json |
예 |
request:
{
content: string
price: number
isAdult: boolean
isFixed: boolean
}
isActive를 보내지 않는다.
Response data:
{
"postId": 801,
"content": "오늘의 소식입니다.",
"imageUrl": "https://cdn.example.com/community/801.png",
"audioSignedUrl": null,
"price": 0,
"isAdult": false,
"isFixed": false,
"fixedAtUtc": null,
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
7.3 수정·고정·비활성화
PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}
Content-Type: multipart/form-data
| Part | Content | 필수 |
|---|---|---|
image |
교체 File | 아니요 |
audioFile |
교체 File | 아니요 |
request |
application/json |
예 |
request:
{
content: string
price: number
isAdult: boolean
isFixed: boolean
isActive?: false
}
- 교체 File 미전송은 기존 media 유지다.
- 일반 수정에서는
isActive를 생략한다.isActive=false는 soft delete 전용이며 이때 서버 응답은isFixed=false,fixedAtUtc=null이어야 한다.true는 보내지 않는다. - Response
data는 등록 응답과 같다. - soft delete 성공 후 열린 게시글 Sheet를 닫고 선택 캐릭터의 커뮤니티 게시글 active-only 목록을 무효화·재조회해 해당 항목을 제거하며 성공 알림을 표시한다.
7.4 프론트엔드 조회·갱신 규칙
GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}는 추가하거나 소비하지 않는다.- 커뮤니티 상세·수정 직접 route를 제공하지 않고 목록 응답의 item으로 목록 행/카드 Sheet를 초기화한다.
- 조회·audio 재생·수정·고정·비활성화·댓글 진입에 목록 item의
postId와 필드를 사용한다. - 생성·수정 응답은 목록 cache를 갱신하는 데 사용한다.
- 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회에서는 새
audioSignedUrl을 사용한다. media error는 목록·상세 조회, URL 재발급 또는 자동 재생의 trigger가 아니다.
7.5 첨부 audio upload 규칙
커뮤니티의 optional audioFile은 오디오 콘텐츠의 5.5 업로드 규칙을 그대로 사용한다.
- 확장자:
.mp3,.aac,.m4a - canonical MIME:
audio/mpeg,audio/aac,audio/mp4 - compatibility MIME:
audio/x-m4a는.m4a파일에 한해 허용 - 최대 파일 크기: decimal 1,024MB,
1,024,000,000 bytes이하 - byte 경계:
1,024,000,000허용,1,024,000,001거부 - 최대 재생 길이: 제한 없음
- WAV: 미지원
- 확장자와 MIME 외에 실제 container/codec을 server가 검증한다.
audio/x-m4a도 실제 MP4/M4A container·codec 검증을 통과해야 하며 불일치는415와common.error.invalid_requestenvelope로 거부한다.
8. FanTalk 답변
8.1 답변 작성
POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies
Request body:
{
"content": "응원해줘서 고마워요!"
}
Response data:
{
"fanTalkId": 901,
"replyId": 902,
"creatorMemberId": 9001,
"content": "응원해줘서 고마워요!",
"createdAtUtc": "2026-07-24T00:00:00Z"
}
- 하나의 FanTalk에는 답변을 한 번만 생성할 수 있다.
- 답변이 이미 있으면 수정만 가능하고 삭제는 범위 밖이다.
- 백엔드가 유일성을 원자적으로 강제하는 방식과 중복 생성의 비2xx status/message key는 백엔드 제공 대기 계약이다. 프론트엔드에서 status를 추정하지 않는다.
9. 백엔드 제공 대기 계약
아래 항목은 프론트엔드 인터뷰의 Open Question이 아니라 백엔드 소유 계약이다. P0 계약을 제공받기 전에는 관련 network integration을 구현하지 않는다. P1은 제공 계약 없이 값을 추정하지 않고 출시 전 검증을 맞춘다.
| 우선순위 | 영역 | 필요한 계약 |
|---|---|---|
| P0 | FanTalk | 목록·상세·답변 수정, 전체/미답변/답변 완료 filter, 최신순, reply uniqueness 오류 |
| P0 | 댓글 | 오디오·커뮤니티의 2단계 목록·작성·수정·soft delete, 팬 댓글 삭제를 포함한 작성자별 권한 오류와 status/message key |
| P0 | lookup | original work 이름 검색, genre 이름 검색, originalWork 미선택 null/omit |
| P0 | Series 연결 후보 | 선택 캐릭터의 연결 가능한 활성 오디오 조회 |
| P0 | Series | 전체 순서의 50개 초과 로딩·누락 ID·동시 변경 충돌 |
| P1 | validation | price 최대값 |
| P0 | 오류 | 신규 domain 오류별 비2xx status와 KO/EN/JA message key |
| P2 | 감사 로그 | backend event schema. 관리자 조회 UI 여부는 PRD OQ-010에서 별도로 결정 |
10. UI 작성 후 확정할 validation
문자열 최대 길이와 배열 최대 개수는 초기 UI를 만든 뒤 페이지별로 검토해 결정한다. 제공된 계약에 명시되지 않은 최대값을 프론트엔드가 먼저 추정해 schema에 추가하지 않는다.
- 문자열 대상: Character
name·description, Audiotitle·description, Seriestitle·introduction·keywords[]·writer·studio, Communitycontent, FanTalk replycontent, 댓글content - 배열 대상: Audio
seriesIds, SeriespublishedDaysOfWeek·keywords·연결contentIds·순서seriesIds - UI 검토에서 권고값을 작성하고 백엔드 validation과 호환되는지 확인한 뒤 확정한다. 확정 시 이 문서, PRD, form schema와 최소/최대/초과 경계값 test를 같은 변경에서 갱신한다.