Files

40 KiB
Raw Permalink Blame History

PRD: AI 캐릭터 관리자 API

1. Overview

운영자가 AI 캐릭터용 Member로 직접 로그인하지 않고, ADMIN 권한으로 선택한 AI 캐릭터의 크리에이터 채널 자산과 팬 상호작용을 대리 관리하는 신규 v2 관리자 API를 제공한다.


2. Problem

  • AI 캐릭터용 Member(memberKind = AI_CHARACTER)는 직접 로그인할 수 없어야 하지만, 운영자는 캐릭터의 콘텐츠, 시리즈, 커뮤니티, 각 자산의 댓글과 FanTalk를 관리해야 한다.
  • 기존 기능은 creatorMember.id 기반으로 흩어져 있으며, 관리자 frontend가 레거시 endpoint를 조합하면 권한, 소유권, soft delete 의미가 일관되지 않을 수 있다.
  • 기존 creator/admin service 일부에는 소유권 검증이 약한 경로가 있어, 단순 위임만으로는 다른 캐릭터나 HUMAN creator 자원을 변경할 위험이 있다.
  • 캐릭터 등록에 필요한 원작 검색과 시리즈 등록에 필요한 장르 목록은 범용 관리자 API에만 있어 캐릭터 관리자 배포 Origin에서 호출할 수 없다.
  • 기존 legacy/public API 계약은 유지해야 하므로 신규 관리자 표면은 별도 v2 경계로 제공되어야 한다.

3. Goals

  • 신규 prefix /api/v2/admin/ai-characters/**는 JWT auth claim의 ROLE_ADMIN과 JWT subject로 조회한 현재 DB Member.role == ADMIN을 모두 만족하는 요청만 허용한다.
  • 모든 신규 target endpoint는 외부 대상 식별자로 characterId를 받고, 서버가 ChatCharacter.creatorMember를 내부 행위자로 해석한다. 단, 캐릭터 목록/검색·생성, 원작 검색과 시리즈 장르 목록은 선택된 target이 필요하지 않아 characterId를 받지 않는다.
  • target 해석 시 ChatCharacter 존재, creatorMember 존재, creatorMember.role == CREATOR, creatorMember.memberKind == AI_CHARACTER를 모두 검증한다.
  • 검증 실패 시 4xx로 거부하고 DB, S3, 외부 캐릭터 API, 이벤트 발행 등 후속 부작용을 만들지 않는다.
  • 캐릭터, 오디오 콘텐츠와 댓글, 시리즈, 커뮤니티 게시글과 댓글, FanTalk 목록·답변 작성·수정·팬 원글 삭제, 오디오 signed URL을 신규 관리자 API에서 관리한다.
  • 기존 legacy/public endpoint의 URI, 성공·오류 HTTP status, response body, message/i18n을 포함한 외부 계약은 변경하지 않는다. 단, 캐릭터 관리자 frontend가 기존 관리자 인증을 재사용할 수 있도록 /admin/member/login, /member/logout의 CORS 허용 Origin만 path-specific으로 확장한다.
  • 내부 구현은 신규 v2 controller/facade/application 경계를 두고, 기존 entity/repository/S3/CloudFront/event 컴포넌트는 테스트로 고정한 뒤 선택적으로 재사용한다.

4. Non-Goals

  • 이번 PRD는 관리자 API backend 요구사항과 구현 계획만 포함하며, 관리자 UI/frontend 구현은 포함하지 않는다.
  • AI 캐릭터용 Member의 access token, refresh token, 임시 세션, impersonation 로그인은 만들지 않는다.
  • creatorMemberId를 관리자 frontend의 필수 입력으로 노출하지 않는다.
  • HUMAN creator를 이 API로 대리 관리하지 않는다.
  • 위 두 공유 인증 경로의 CORS 허용 Origin 확장 외 기존 legacy/public endpoint 변경, 폐기, deprecation, schema 변경은 포함하지 않는다.
  • 기존 external character API business contract 변경은 포함하지 않으며, 변경이 필요하면 재확인한다.
  • 기존 soft delete 의미 변경은 포함하지 않으며, 변경이 필요하면 재확인한다.
  • 물리 삭제와 연관 데이터 cascade 삭제는 포함하지 않는다.
  • 신규 DB schema/DDL 또는 ChatCharacter-Member 관계 모델 변경은 포함하지 않는다.
  • 라이브, DM, 후원, 정산, 알림 설정, 랭킹 관리, 콘텐츠 구매·좋아요와 커뮤니티 구매·좋아요 관리는 포함하지 않는다.
  • 사용하지 않는 레거시 캐릭터 직접 댓글 /api/chat/character/{characterId}/comments의 v2 전환·조회·삭제는 포함하지 않는다.
  • FanTalk 원글 작성, creator reply 전용 삭제 endpoint·hard delete, 일반 사용자 대리 작성, nested reply 작성과 FanTalk 원글 삭제 시 reply cascade 변경은 포함하지 않는다.
  • AudioContentCloudFront 복사/이동, signed URL 신규 dependency 추가는 포함하지 않는다.

5. Target Users

  • 운영자: AI 캐릭터를 대신해 캐릭터 프로필, 콘텐츠, 시리즈, 커뮤니티 게시글, 각 자산의 댓글과 FanTalk를 관리하는 관리자
  • 관리자 frontend: 신규 v2 AI 캐릭터 관리자 API만으로 In-Scope 작업을 수행해야 하는 클라이언트
  • 서버 개발자: 기존 creator 기능을 회귀시키지 않으면서 AI 캐릭터 대리 관리 경계를 유지해야 하는 개발자

6. User Stories

  • 운영자는 AI 캐릭터 목록을 검색하고 상세 정보를 확인한 뒤 생성, 수정, 비활성화하고 싶다.
  • 운영자는 캐릭터 등록 시 soft delete되지 않은 원작을 검색해 originalWorkId를 선택하고 싶다.
  • 운영자는 선택한 AI 캐릭터 소유 오디오 콘텐츠를 조회, 생성, 수정, soft delete하고 관리자 화면에서 재생 가능한 signed URL을 받고 싶다.
  • 운영자는 선택한 AI 캐릭터의 오디오 콘텐츠에 달린 댓글·답글을 조회하고, 캐릭터 명의로 작성·수정하며, 부적절한 댓글·답글을 soft delete하고 싶다.
  • 운영자는 선택한 AI 캐릭터 소유 시리즈를 조회, 생성, 수정, soft delete하고 콘텐츠 연결/해제/순서를 관리하고 싶다.
  • 운영자는 시리즈 등록 시 활성 장르 목록에서 genreId를 선택하고 싶다.
  • 운영자는 선택한 AI 캐릭터 소유 커뮤니티 게시글을 작성, 수정, 고정/해제, soft delete하고 싶다.
  • 운영자는 선택한 AI 캐릭터의 커뮤니티 게시글에 달린 댓글·답글을 조회하고, 캐릭터 명의로 작성·수정하며, 부적절한 댓글·답글을 soft delete하고 싶다.
  • 운영자는 선택한 AI 캐릭터의 FanTalk 목록과 기존 creator reply를 확인한 뒤 활성 root FanTalk에 답변하고, 기존 답변의 내용·활성 상태를 수정하거나 팬이 작성한 원글을 soft delete하고 싶다.
  • 서버는 다른 AI 캐릭터나 HUMAN creator의 resource ID가 전달되면 변경 없이 4xx로 거부해야 한다.

7. Core Features

Feature A. 공통 인증, 인가, target 해석

Requirements

  • 모든 신규 prefix endpoint는 JWT ROLE_ADMIN과 현재 DB Member.role == ADMIN을 독립적으로 모두 검증한다.
  • JWT가 없거나 잘못됐거나 만료·폐기된 경우는 401, JWT role과 현재 DB role 중 하나라도 ADMIN이 아닌 경우는 403으로 처리한다.
  • JWT에는 ROLE_ADMIN이 남아 있지만 현재 DB role이 강등된 stale claim도 403으로 거부한다.
  • 선택한 캐릭터 자원을 다루는 domain write/read는 characterIdChatCharacter를 조회한 뒤 연결된 creatorMember를 사용한다. 원작 검색과 시리즈 장르 목록은 target 없는 reference endpoint이므로 characterId를 받거나 target을 해석하지 않는다.
  • creatorMember는 도메인 소유권/작성자 판단에만 사용하고 Spring Security principal로 교체하지 않는다.
  • creatorMember 누락, role 불일치, memberKind 불일치 요청은 4xx로 거부한다.
  • 요청 중 누락 Member 생성, role/memberKind 자동 보정 같은 lazy repair는 하지 않는다.

Edge Cases

  • stale claim을 포함한 인증·인가 실패는 target resolver와 domain use-case 실행 전에 종료되어야 한다.
  • 유효하지 않은 characterId 요청은 DB write, S3 upload/delete, 외부 캐릭터 API 호출, 이벤트 발행 없이 실패해야 한다.
  • 다른 AI 캐릭터 또는 HUMAN creator 소유 resource ID는 조회/수정/삭제/연결/답변 모두 거부해야 한다.

Feature B. AI 캐릭터 관리

Requirements

  • 목록 조회, 검색, 상세 조회, 생성, 수정, 삭제 의미의 비활성화(isActive=false)를 제공한다.
  • 캐릭터 등록 화면에서 사용할 soft delete되지 않은 원작 검색을 characterId 없는 관리자 전용 endpoint로 제공한다.
  • 원작 검색은 레거시 AdminOriginalWorkController.search처럼 필수 searchTerm으로 제목·콘텐츠 타입·카테고리를 부분 검색하고 soft delete된 원작을 제외하며, 페이징 없는 OriginalWorkResponse 목록을 반환한다.
  • 레거시 플랫폼 관리자와 중복 이름 검증, 외부 캐릭터 API 연동, 대표 이미지 저장, 원작 연결, 언어 감지/번역 이벤트, AI 캐릭터용 creatorMember 생성 및 표시 정보 동기화 동작 parity를 유지한다.
  • 삭제는 soft delete이며 row, 연결 Member, 콘텐츠를 물리 삭제하지 않는다.

Edge Cases

  • 중복 이름, 외부 캐릭터 API 실패, 이미지 저장 실패는 기존 관리자 동작을 특성화 테스트로 고정한 뒤 유지한다.
  • 비활성화 실패 시 일부 관계만 변경된 상태로 남기지 않는다.
  • 원작 검색은 캐릭터 target을 해석하지 않지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 계약을 동일하게 적용한다.

Feature C. 오디오 콘텐츠 관리 및 signed URL

Requirements

  • 캐릭터 소유 콘텐츠 목록/검색/상세 조회, 생성, 수정, 기존 삭제 동작에 따른 soft delete를 제공한다.
  • 콘텐츠 생성 화면에서 사용할 활성 콘텐츠 테마(카테고리) 목록 조회를 제공한다. 기존 크리에이터 관리자 콘텐츠 등록 화면의 GetAudioContentThemeResponse와 같은 id, theme, image 필드명을 유지한다.
  • 기존 크리에이터 콘텐츠 관리의 검증, 파일 처리, content upload/processing pipeline, 가격, 공개/예약, 번역/알림 등 business behavior parity를 유지한다.
  • 관리자 화면 재생용 signed URL을 콘텐츠 목록/상세 응답에 제공한다.
  • signed URL은 공통 AudioContentCloudFront를 재사용하고 기존 크리에이터 관리자와 같은 만료 정책을 따른다.
  • 기존 signed URL 구현을 재사용하기 전에 creator admin 만료 계산식과 만료 계산·path 처리에서 실제로 관찰되는 edge case를 통과하는 특성화 테스트로 고정한다.
  • 응답에 private object path나 서명 키 정보를 노출하지 않는다.
  • 신규 관리자 오디오 생성 request는 timezone을 받지 않고 nullable releaseDate를 클라이언트가 변환한 ISO-8601 UTC(Z)로 받는다. 로컬 시각과 timezone을 함께 받는 레거시 생성 형식은 병행 지원하지 않는다.
  • 오디오 상세의 nullable releaseDate는 기존 필드명과 null/노출 조건을 유지하고, 값이 있으면 ISO-8601 UTC(Z)로 반환한다. 상세 조회는 timezone query를 받지 않는다.
  • target 소유의 활성 오디오 콘텐츠에 대해 댓글 목록, 답글 목록, 댓글·답글 작성, 캐릭터가 작성한 댓글·답글 수정과 모든 댓글·답글 soft delete를 제공한다.
  • 댓글·답글 작성자는 관리자 principal이 아니라 target creatorMember이며, optional parentId가 없으면 원댓글, 있으면 답글로 저장한다.
  • 댓글·답글 내용 수정은 작성자가 target creatorMember인 활성 row에만 허용한다.
  • 댓글·답글 삭제는 작성자와 관계없이 target 소유 콘텐츠에 연결된 row의 isActive=false만 반영하고 자식 답글을 cascade 변경하거나 물리 삭제하지 않는다.
  • 댓글·답글 조회는 timezone 없이 page, size를 받고 레거시 응답 필드를 유지하되, 각 date를 ISO-8601 UTC(Z)로 반환한다.

Edge Cases

  • 콘텐츠 소유자가 target creatorMember와 다르면 조회/수정/삭제 모두 거부한다.
  • 댓글 또는 답글이 target 소유 콘텐츠에 연결되지 않았거나, 답글 작성의 parentId가 같은 콘텐츠의 활성 원댓글이 아니면 mutation 전에 400으로 거부한다.
  • 댓글 수정은 팬이 작성한 row를 target 캐릭터 명의로 변경하지 않으며, 이미 비활성인 row도 수정하지 않는다.
  • 이미 비활성인 댓글·답글의 삭제는 성공 no-op으로 처리하고 추가 상태 변경이나 이벤트를 만들지 않는다.
  • 커뮤니티 오디오의 기존 30분 signed URL 정책은 이 콘텐츠 재생 정책과 임의 통합하지 않는다.

Feature D. 시리즈 관리

Requirements

  • 목록/상세 조회, 생성, 수정, isActive=false soft delete를 제공한다.
  • 시리즈 상세 응답 data는 목록 wrapper가 아니라 목록 items의 단일 항목과 동일한 schema를 사용한다. 필드는 seriesId, title, introduction, coverImageUrl, publishedDaysOfWeek, genreId, isAdult, state, isActive, writer, studio이며 기존 상세 전용 genre, keywords는 반환하지 않는다.
  • 콘텐츠 연결/해제, 시리즈 콘텐츠 조회/검색, 순서 관리를 제공한다.
  • 기존 creator series 관리의 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 관리 behavior를 먼저 통과하는 특성화 테스트로 고정하고 신규 v2 경로에서 parity를 유지한다.
  • 시리즈와 연결 콘텐츠는 모두 동일한 creatorMember 소유여야 한다.
  • 기존 updateSeriesOrders(ids)처럼 소유권 없는 ID-only 갱신은 신규 v2 경로에서 허용하지 않는다.
  • 시리즈 콘텐츠 조회는 관리자 연결 작업을 위해 검색어 기반 필터를 제공한다.
  • 시리즈 등록 화면에서 사용할 활성 장르 목록을 characterId 없는 관리자 전용 endpoint로 제공한다.
  • 장르 목록은 레거시 범용 관리자 API처럼 활성 장르 전체를 orders 오름차순으로 반환하며 각 항목에 id, genre, isAdult를 포함한다.

Edge Cases

  • 순서 변경 요청의 모든 series/content ID는 target character 소유 검증을 통과해야 한다.
  • inactive series는 일반 활성 조회에서 제외한다.
  • 장르 목록은 캐릭터 target을 해석하지 않지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 계약을 동일하게 적용한다.

Feature E. 커뮤니티 게시글 관리

Requirements

  • 등록, 수정, 공지 고정/해제(isFixed), 수정 요청의 isActive=false soft delete를 제공한다.
  • soft delete 시 현재 동작처럼 isFixed=false, fixedAt=null을 적용한다.
  • 기존 최대 고정 게시글 수 3개, 이미지/오디오/유료 게시글 검증, 알림/최근 소식 side effect를 유지한다.
  • 관리자 UI에 필요한 조회는 기존 v2 커뮤니티 조회 로직을 무비판적으로 복제하지 않고 신규 관리자 facade/endpoint에서 안전하게 재사용하거나 최소 query adapter를 둔다.
  • 관리자 커뮤니티 목록은 timezone query를 받지 않고 page, size만 받는다.
  • 목록 응답 datatotalCount, page, size, hasNext, items를 포함해 관리자 UI가 전체 개수와 다음 페이지 추가 로딩 필요 여부를 판단할 수 있어야 한다.
  • target 소유의 활성 커뮤니티 게시글에 대해 댓글 목록, 답글 목록, 댓글·답글 작성, 캐릭터가 작성한 댓글·답글 수정과 모든 댓글·답글 soft delete를 제공한다.
  • 댓글·답글 작성자는 관리자 principal이 아니라 target creatorMember이며, optional parentId가 없으면 원댓글, 있으면 답글로 저장한다.
  • 댓글·답글 내용 수정은 작성자가 target creatorMember인 활성 row에만 허용한다.
  • 댓글·답글 삭제는 작성자와 관계없이 target 소유 게시글에 연결된 row의 isActive=false만 반영하고 자식 답글을 cascade 변경하거나 물리 삭제하지 않는다.
  • 댓글·답글 조회는 timezone 없이 page, size를 받고 레거시 응답 필드를 유지하되, 각 date를 ISO-8601 UTC(Z)로 반환한다.

Edge Cases

  • 고정 게시글이 이미 3개인 상태에서 추가 고정은 기존 정책대로 실패한다.
  • soft delete된 고정 게시글은 고정 상태와 시간이 반드시 제거되어야 한다.
  • 댓글 또는 답글이 target 소유 게시글에 연결되지 않았거나, 답글 작성의 parentId가 같은 게시글의 활성 원댓글이 아니면 mutation 전에 400으로 거부한다.
  • 댓글 수정은 팬이 작성한 row를 target 캐릭터 명의로 변경하지 않으며, 이미 비활성인 row도 수정하지 않는다.
  • 이미 비활성인 댓글·답글의 삭제는 성공 no-op으로 처리하고 추가 상태 변경이나 이벤트를 만들지 않는다.

Feature F. FanTalk 관리

Requirements

  • 선택한 AI 캐릭터의 FanTalk 목록과 각 root 글의 creator reply 목록을 관리자 전용 endpoint로 조회한다.
  • 목록 응답 필드와 page 정책은 공개 v2 CreatorChannelFanTalkTabResponse를 유지하되, 공개 v2 endpoint를 직접 재사용하지 않고 characterId target 해석과 관리자 ownership 정책을 적용한다.
  • 선택한 AI 캐릭터가 자신의 활성 root FanTalk에 creator reply를 작성한다.
  • 요청은 characterId와 대상 root fanTalkId를 포함한다.
  • 대상 FanTalk가 존재하고 활성 상태이며, 대상 creator가 해석된 creatorMember와 일치하는 root 글인지 검증한다.
  • 언어 감지와 기존 응답 DTO 의미 등 검증 가능한 business behavior를 유지하고, 저장된 답변의 writer/creator는 해석된 creatorMember와 일관되어야 한다.
  • 선택한 AI 캐릭터가 작성한 기존 direct reply의 내용과 활성 상태를 수정하는 관리자 전용 endpoint를 제공한다.
  • 답변 수정 request는 레거시 PutWriteCheersRequest에서 path로 이동한 cheersId를 제외한 optional/nullable content, isActive를 그대로 받는다. 두 필드는 함께 입력할 수 있고 모두 생략하거나 null이면 성공 no-op이다.
  • 답변 수정 대상은 target creatorMember가 writer이자 creator인 direct reply여야 하며, fanTalkId로 지정한 target 채널의 활성 root에 직접 연결되어야 한다. 비활성 reply는 조회 대상에 포함해 isActive=true로 재활성화할 수 있다.
  • 답변 수정은 레거시와 같이 non-null field만 반영하며 저장된 languageCode를 변경하거나 언어 감지·이벤트를 발생시키지 않는다.
  • 답변 수정 성공 data는 레거시 CreatorChannelFanTalkResponse 필드 형태를 유지한다. 응답의 fanTalkId는 수정한 reply row ID이고 creatorReplies는 빈 배열이다.
  • 팬이 작성한 root FanTalk를 isActive=false로 soft delete하는 관리자 전용 endpoint를 제공한다.
  • FanTalk 삭제는 root row만 비활성화하고 연결된 creator reply row는 변경하지 않는다. 비활성 root가 목록에서 제외되므로 연결된 reply도 함께 노출되지 않는다.

Edge Cases

  • 다른 캐릭터의 FanTalk, reply에 대한 nested reply, 비활성 FanTalk, 미존재 FanTalk에는 답변하지 않는다.
  • 실패 시 reply 저장과 이벤트 발행이 없어야 한다.
  • 답변 수정 시 root·reply가 다른 target에 속하거나, reply가 지정한 root의 direct child가 아니거나, root가 비활성·미존재이거나, target AI가 작성하지 않은 팬 root/reply이면 400으로 거부하고 변경하지 않는다.
  • 답변 수정 대상 reply 자체의 비활성 상태는 거부 조건이 아니며, isActive=true 재활성화를 허용한다.
  • FanTalk 삭제 대상은 target creatorMember 채널에 연결된 parent=null의 fan 작성 root여야 한다. 같은 target의 이미 비활성인 fan root는 성공 no-op이고, creator가 작성한 root, reply, 다른 creator의 root, 미존재 root는 변경 없이 400으로 거부한다.
  • FanTalk 원글 삭제는 reply 삭제·수정이나 별도 이벤트를 발생시키지 않는다.

8. API Expectations

  • 신규 endpoint prefix는 기존 공개 /api/v2/creator-channels/*와 legacy /admin/*, /creator-admin/*를 변경하지 않기 위해 /api/v2/admin/ai-characters를 기본안으로 한다.
  • 성공 응답은 ApiResponse.ok(...), API application/controller/security filter 오류는 오류 의미에 맞는 HTTP status와 ApiResponse.error(...)를 사용한다.
  • 이 API 오류 응답은 success=false와 현지화된 message를 포함하며 2xx로 normalize하지 않는다.
  • Accept-Language: ko|en|ja에 따라 KO/EN/JA 메시지를 반환하고, 없거나 지원하지 않는 언어는 KO로 fallback한다.
  • security filter 단계의 오류도 MVC interceptor에 의존하지 않고 Accept-Language를 직접 해석해 동일한 응답 계약을 따른다.
  • 신규 prefix는 캐릭터 관리자 frontend Origin http://localhost:8888, https://test-character-admin.sodalive.net, https://character-admin.sodalive.net만 허용한다.
  • 기존 범용 관리자 frontend와 creator frontend Origin을 캐릭터 관리자 Origin 대신 허용하지 않는다.
  • 공유 인증 경로 /admin/member/login, /member/logout는 기존 전역 Origin과 위 캐릭터 관리자 Origin의 합집합만 허용한다. 이 path-specific 확장은 다른 legacy/public 경로의 CORS 허용 범위를 변경하지 않는다.
  • 위 관리자 Origin의 신규 prefix 오류와 preflight는 404 fallback 및 실제 mapped endpoint의 405/406/415 경로를 포함해 기존 전역 CORS 응답 계약을 유지하며, 두 공유 인증 경로에서도 허용·거부 Origin을 검증한다.
  • 허용되지 않은 Origin, method 또는 header를 Spring CORS 계층에서 정책 거부하는 경우는 handler 진입 전 403으로 종료되는 브라우저 보안 경계다. 이 403의 body, content type, 현지화 및 ApiResponse.error envelope는 신규 API 오류 계약의 예외로 두고 외부 계약으로 고정하지 않는다.
  • 표준 HTTP method가 MVC까지 도달했지만 해당 mapping이 없으면 기존 Spring MVC의 405와 Allow header를 유지한다.
  • StrictHttpFirewall이 신규 prefix에서 비표준 HTTP method 또는 위험 URL을 RequestRejectedException으로 거부하면, 캐릭터 관리자 허용 Origin에는 CORS header를 포함한 400 common.error.invalid_request와 현지화된 ApiResponse.error를 반환한다. 허용되지 않은 Origin은 기존 Spring CORS 정책과 같이 body 계약 없는 403으로 종료한다.
  • SecurityConfig는 기존 AiCharacterAdminSecurityErrorHandler를 global RequestRejectedHandler로 등록하되 신규 prefix만 위 400/CORS 계약으로 처리하고, legacy/public은 DefaultRequestRejectedHandler에 위임해 기존 RequestRejectedException 동작을 유지한다. Spring 5.3의 비표준 method enum 한계 때문에 CORS 검사 request만 GET wrapper를 사용하며 실제 firewall method 허용 범위는 확장하지 않고 setUnsafeAllowAnyHttpMethod(true)도 사용하지 않는다.
  • Phase 1 공통 오류는 인증 정보 없음·잘못됨·만료·폐기 401 common.error.bad_credentials, JWT 또는 현재 DB role의 ADMIN 불충족 403 common.error.access_denied, request/target 미존재·불변식 위반 400, 신규 prefix 미매핑 경로 404, 지원하지 않는 HTTP method 405, 응답 media type 406, 요청 media type 415를 common.error.invalid_request로 고정한다. 405는 표준 Allow header를, 415는 표준 Accept header를 유지한다. controller mapping의 필수 path variable 선언이 누락된 MissingPathVariableException과 예상하지 못한 controller/JWT filter 오류는 500 common.error.unknown으로 고정한다.
  • malformed JSON의 HttpMessageNotReadableException, handler에 전달된 MethodArgumentNotValidException, multipart 필수 part 누락의 MissingServletRequestPartException은 각각 400 common.error.invalid_request와 KO/EN/JA ApiResponse.error를 반환한다.
  • 이후 phase의 domain/client/server 오류는 각 task에서 정확한 HTTP status와 KO/EN/JA message key를 먼저 정의하고 같은 envelope를 적용한다.
  • 신규 prefix 전용 오류 처리는 legacy/public endpoint의 기존 성공·오류 응답에 적용하지 않는다.
  • request/response의 기계 검증 가능한 단일 기준은 같은 디렉터리의 api-contract.openapi.json이다. 설명과 레거시 근거는 api-contract.md에 기록한다.
  • 신규 endpoint는 레거시 API의 request/response 필드명, 타입, optional/nullable, 기본값과 성공 data 형태를 그대로 이관한다. characterId, contentId, seriesId, postId, fanTalkId, replyId처럼 신규 path로 이동한 ID만 request body에서 중복 제거한다.
  • 캐릭터 등록용 원작 검색은 GET /api/v2/admin/ai-characters/original-works/search?searchTerm=..., 시리즈 장르 목록은 GET /api/v2/admin/ai-characters/series-genres로 제공하며 두 endpoint 모두 characterId를 받지 않는다.
  • 오디오 콘텐츠 댓글은 /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments, 커뮤니티 댓글은 /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments 하위에서 각각 댓글 목록, 답글 목록, 작성, 수정, 삭제 5개 operation으로 제공한다. 답글 목록은 /{commentId}/replies, 수정·삭제는 /{commentId} 하위 path를 사용한다.
  • 오디오 댓글 작성 request는 comment, optional parentId, isSecret, languageCode, 커뮤니티 댓글 작성 request는 comment, optional parentId, isSecret을 받는다. 수정 request는 두 domain 모두 comment만 받으며, 삭제는 request body를 받지 않는다.
  • 두 댓글 domain의 목록과 답글 목록은 GetAudioContentCommentListResponse 또는 GetCommunityPostCommentListResponse에 해당하는 totalCount, items 형태를 유지한다. 두 목록은 timezone query를 받지 않고 각 댓글 date를 ISO-8601 UTC(Z)로 반환한다.
  • FanTalk 팬 원글 삭제는 DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}로 제공한다.
  • FanTalk 답변 수정은 PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}로 제공한다.
  • 위 14개 신규 operation을 기존 23개에 추가해 전체 관리자 계약은 37개 operation으로 관리한다.
  • GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}의 성공 dataGET /api/v2/admin/ai-characters/{characterId}/seriesitems 단일 항목과 동일한 schema를 참조한다.
  • 캐릭터 수정은 레거시 ChatCharacterUpdateRequest처럼 isActive=false와 다른 optional field의 동시 입력을 허용하며, 이 경우 레거시 service 의미대로 비활성화만 반영한다.
  • 목록 endpoint의 query와 page 동작은 각 레거시 API를 따른다. FanTalk 관리자 목록만 공개 v2 탭의 page 기본값 0, size 기본값 20, 최소 20, 최대 50 보정을 따른다.
  • 오디오 콘텐츠 생성 multipart의 requesttimezone을 포함하지 않는다. nullable releaseDate는 클라이언트가 UTC로 변환한 ISO-8601 date-time(Z)이며 기존 로컬 시각+timezone 형식은 신규 endpoint에서 받지 않는다.
  • 오디오 콘텐츠 상세, 오디오 댓글·답글 목록, 커뮤니티 댓글·답글 목록은 timezone query를 받지 않는다. 상세 releaseDate와 댓글 date는 기존 필드명 및 nullable/노출 조건을 유지한 ISO-8601 UTC(Z)다.
  • 2026-07-29 사용자 확정에 따라 커뮤니티 관리자 목록은 위 레거시 이관 원칙의 예외로 둔다. 사용하지 않는 timezone query를 제거하고 datatotalCount, page, size, hasNext, items pagination wrapper로 반환한다.
  • multipart 생성/수정은 기존 admin/creator-admin 관례대로 파일 part와 request JSON string part를 사용한다.
  • 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 응답 형태가 다르므로 각각 GET .../series/{seriesId}/contentsGET .../series/{seriesId}/contents/search?search_word=...로 분리한다.
  • 레거시 mutation이 ApiResponse.ok(null)을 반환하면 신규 endpoint도 data: null을 반환한다. 오디오 콘텐츠 생성은 레거시 CreateAudioContentResponse(contentId)를 유지한다. FanTalk 답변 작성만 신규 계획의 축약 응답 fanTalkId, replyId, creatorMemberId, content, createdAtUtc를 사용한다. FanTalk 답변 수정은 레거시 CreatorChannelFanTalkResponse 필드 형태를 유지한다.
  • 신규 댓글 작성·수정·삭제와 FanTalk 원글 삭제의 성공 응답은 모두 ApiResponse.ok(null)을 사용한다.
  • request/response 구현 DTO는 신규 v2 AI character admin API 전용으로 둘 수 있지만, JSON 외부 계약은 api-contract.openapi.json의 레거시 필드명과 형태를 유지한다.

9. Technical Constraints

  • Kotlin, Java 17, Spring Boot 2.7.14, Gradle Wrapper를 유지한다.
  • 신규 dependency를 추가하지 않는다.
  • 신규 DB schema/DDL을 만들지 않는다.
  • 기존 v2 API 조립 계층과 domain/application 의존 방향을 따른다.
  • controller 내부 호출, 서버 내부 legacy HTTP 호출, 기존 controller 역참조는 하지 않는다.
  • 신규 v2 application/domain 계층은 기존 controller와 v2 API response DTO를 역참조하지 않는다.
  • 기존 business method를 재사용하기 전 특성화/회귀 테스트를 작성한다.
  • 특성화/회귀 테스트는 신규 v2 use-case의 미구현 RED 테스트와 분리하고, 기존 legacy/creator-admin 구현을 대상으로 먼저 통과해야 한다.
  • 단순 복사-붙여넣기 대신 필요한 최소 추출 또는 v2 use-case 재개발을 선택한다.

10. Metrics

  • 신규 endpoint별 또는 controller slice별 JWT role × 현재 DB role 인가 매트릭스와 stale ADMIN claim 403 테스트 존재 여부
  • 신규 prefix의 각 API 오류 분기에 정확한 HTTP status, ApiResponse.error, KO/EN/JA와 405 Allow/415 Accept header 테스트 존재 여부
  • 신규 prefix 실제 mapped endpoint 및 공유 인증 경로의 허용·거부 Origin/preflight 테스트 존재 여부
  • 신규 prefix의 표준 method 미매핑 405 Allow 유지와 RequestRejectedException 400/i18n/ApiResponse.error/허용 Origin CORS header, 미허용 Origin body 계약 없는 403 테스트 존재 여부
  • legacy/public firewall 동작 불변 및 setUnsafeAllowAnyHttpMethod(true) 미사용 확인 여부
  • core controller security/error 계약의 production @SpringBootTest full-context 실행과 Redis token fixture cleanup 확인 여부
  • target resolver 조회 직후 creatorMember 초기화와 fetch join 제거 시 실패하는 non-vacuous 회귀 테스트 존재 여부
  • HttpMessageNotReadableException, MethodArgumentNotValidException, MissingServletRequestPartException의 exact exception type과 KO/EN/JA 400 envelope 직접 검증 여부
  • target/ownership 실패 시 no-side-effect 테스트 존재 여부
  • character/original-work/content/content-comment/series/series-genre/community/community-comment/FanTalk slice별 targeted test 통과 여부
  • 오디오 생성 request와 오디오 상세·댓글·답글 및 커뮤니티 댓글·답글 조회에서 timezone이 제거되고, releaseDate/date가 ISO-8601 UTC(Z)로 검증되는지 여부
  • 댓글 작성·수정의 target 작성자 제한, 소유 자산 댓글 삭제, parent 소유권·root 검증과 soft-delete no-cascade 테스트 존재 여부
  • FanTalk 팬 root 삭제, 비활성 팬 root no-op, creator reply row 보존·비노출 테스트 존재 여부
  • FanTalk 답변 수정의 optional/nullable content·isActive, 빈 객체 no-op, 비활성 reply 재활성화, target/root/direct-reply ownership과 레거시 성공 응답 테스트 존재 여부
  • signed URL TTL 계산식·edge case parity 및 private path 비노출 테스트 통과 여부
  • 기존 legacy/public endpoint의 성공·오류 status/body/message 회귀 테스트 통과 여부

11. Acceptance Criteria

  • JWT ROLE_ADMIN과 현재 DB Member.role == ADMIN을 모두 만족하는 요청만 유효한 AI character 대상으로 신규 endpoint를 호출할 수 있다.
  • 비로그인 또는 잘못된 JWT 요청은 401이고, JWT 비ADMIN + DB ADMIN과 JWT ADMIN + DB 비ADMIN stale claim은 모두 403이다.
  • 신규 prefix의 API application/controller/security filter 오류는 정확한 비2xx status, ApiResponse.error, Accept-Language에 따른 KO/EN/JA message를 반환한다. Spring CORS 계층의 정책 거부 403 body는 이 envelope 계약의 예외다.
  • 지원하지 않는 HTTP method는 405와 Allow header, 응답 media type은 406, 요청 media type은 415와 Accept header를 반환하고, MissingPathVariableException은 500 common.error.unknown을 반환한다.
  • 표준 HTTP method가 MVC에 도달한 뒤 mapping이 없을 때는 기존 405와 Allow header를 유지한다. 신규 prefix의 비표준 HTTP method 또는 위험 URL이 StrictHttpFirewall에서 RequestRejectedException으로 거부되면 허용된 캐릭터 관리자 Origin에는 CORS header와 현지화된 400 common.error.invalid_request ApiResponse.error를, 미허용 Origin에는 body 계약 없는 403을 반환한다. legacy/public firewall 동작은 변하지 않고 setUnsafeAllowAnyHttpMethod(true)는 사용하지 않는다.
  • 신규 prefix는 캐릭터 관리자 Origin만 허용하고, /admin/member/login, /member/logout는 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합을 허용한다. 실제 mapped endpoint와 공유 인증 경로의 CORS 허용·거부가 테스트로 고정된다.
  • core controller security/error 계약은 production @SpringBootTest full context에서 검증하고 Redis token fixture를 테스트 후 정리해 다음 테스트에 남기지 않는다.
  • target resolver의 repository 조회 결과는 반환 직후 creatorMember가 초기화되어 있어야 하며, fetch join 제거 시 실패하는 회귀 테스트로 고정한다.
  • malformed JSON, handler에 전달된 MethodArgumentNotValidException, multipart 필수 part 누락은 각각 정확한 MVC exception type과 현지화된 400 ApiResponse.error 계약을 만족한다.
  • character 미존재, creatorMember 미존재, role 불일치, memberKind 불일치 요청은 4xx이며 아무 side effect도 남기지 않는다.
  • 다른 character 소유 resource ID를 사용한 조회/수정/삭제/연결/답변은 4xx로 거부된다.
  • 캐릭터 생성/수정/비활성화는 레거시 관리자 behavior parity를 유지한다.
  • 캐릭터 등록용 원작 검색은 soft delete된 원작을 제외하고 제목·콘텐츠 타입·카테고리 부분 검색 결과를 반환한다.
  • 콘텐츠 생성/수정/soft delete와 signed URL 응답은 기존 creator/admin behavior parity를 유지한다.
  • 신규 관리자 오디오 생성은 timezone 없이 nullable releaseDate를 ISO-8601 UTC(Z)로 받고, 상세 조회도 timezone 없이 기존 nullable releaseDate를 ISO-8601 UTC(Z)로 반환한다.
  • 오디오 콘텐츠 댓글은 target 소유 활성 콘텐츠 범위에서 조회되고, target 명의 작성·작성자 한정 수정·소유자 moderation soft delete가 적용된다. 댓글·답글 목록은 timezone 없이 각 date를 ISO-8601 UTC(Z)로 반환한다.
  • 시리즈 CRUD, 콘텐츠 연결/해제/순서 변경은 target character 소유 범위를 벗어나지 않는다.
  • 시리즈 상세 data는 목록 items 한 건과 동일한 필드·타입을 반환하고 상세 전용 genre, keywords를 반환하지 않는다.
  • 시리즈 장르 목록은 활성 장르의 id, genre, isAdultorders 순으로 반환한다.
  • 커뮤니티 생성/수정/고정/해제/soft delete는 target creatorMember 소유 게시글에만 적용된다.
  • 커뮤니티 목록은 timezone 없이 조회되고 active owner 게시글의 전체 개수, 요청 page/size, 다음 페이지 여부와 기존 목록 item 필드를 반환한다.
  • 커뮤니티 댓글은 target 소유 활성 게시글 범위에서 조회되고, target 명의 작성·작성자 한정 수정·소유자 moderation soft delete가 적용된다. 댓글·답글 목록은 timezone 없이 각 date를 ISO-8601 UTC(Z)로 반환한다.
  • FanTalk 관리자 목록은 target AI character의 root 글과 creator reply를 공개 v2 응답 필드명으로 반환하며, 공개 v2의 viewer/block 필터나 creator 관리자 CORS 경계에 의존하지 않는다.
  • FanTalk 답변은 target AI character 자신의 활성 root FanTalk에만 저장된다.
  • FanTalk 답변 수정은 target AI character가 작성하고 해당 target의 활성 root에 직접 연결된 reply에만 적용된다. optional/nullable contentisActive의 레거시 상태 전이 및 빈 객체 no-op을 유지하며, 비활성 reply를 재활성화할 수 있고 성공 data는 레거시 CreatorChannelFanTalkResponse 필드 형태다.
  • FanTalk 원글 삭제는 target 채널의 팬 작성 root만 비활성화하고 이미 비활성이면 성공 no-op이며 연결 creator reply row를 변경하지 않는다.
  • 레거시 캐릭터 직접 댓글 API는 신규 v2 endpoint나 구현 계획에 포함되지 않는다.
  • 기존 legacy/public endpoint 테스트가 통과하고, 두 공유 인증 경로의 CORS 허용 Origin 확장 외 성공·오류 status/body/message를 포함한 request/response contract가 변경되지 않는다.
  • 신규 dependency, 신규 DDL, 관련 없는 리팩터링이 없다.

12. Decision Log

날짜 ID 상태 결정 근거 영향 범위
2026-07-29 DEC-COMMENT-001 확정 오디오 콘텐츠·커뮤니티 댓글은 관리자 principal이 아닌 target AI 캐릭터 명의로 작성하고, target 작성 댓글만 내용을 수정하며, target 소유 자산의 댓글은 작성자와 관계없이 soft delete한다. 사용자 승인과 기존 콘텐츠·게시글 소유자의 댓글 비활성화 동작 Feature C, Feature E, API Expectations
2026-07-29 DEC-CHAR-COMMENT-001 제외 사용하지 않는 레거시 캐릭터 직접 댓글 API는 v2로 전환하거나 관리자 삭제 기능을 추가하지 않는다. 사용자 확인 결과 v2 전환 후 미사용 Non-Goals, Acceptance Criteria
2026-07-29 DEC-FANTALK-DELETE-001 확정 팬 작성 FanTalk root 삭제는 원글만 soft delete하고 연결 creator reply row는 변경하지 않는다. 사용자 승인과 기존 CreatorCheers.isActive 동작 유지 Feature F, API Expectations
2026-07-29 DEC-FANTALK-REPLY-UPDATE-001 확정 FanTalk 답변 수정은 레거시 PUT /explorer/profile/cheers의 optional/nullable content, isActive, 빈 객체 no-op, 비활성 reply 재활성화와 CreatorChannelFanTalkResponse 성공 data를 유지한다. 신규 path의 characterId, root fanTalkId, replyId로 target AI 소유 direct reply를 한정한다. 사용자 요청과 “기존 계약과 동일” 확정, 레거시 ExplorerService.modifyCheers 동작 Feature F, API Expectations
2026-07-29 DEC-REGISTRATION-REFERENCE-001 확정 캐릭터 등록용 원작 검색과 시리즈 등록용 장르 목록을 target 없는 신규 v2 관리자 endpoint로 제공한다. 레거시 API는 캐릭터 관리자 배포 Origin에서 호출할 수 없고 신규 frontend는 v2 경계를 사용해야 함 Feature B, Feature D, API Expectations
2026-07-29 DEC-SERIES-DETAIL-001 확정 시리즈 상세 data를 목록 items의 단일 항목과 동일한 schema로 변경하고 기존 상세 전용 genre, keywords를 제거한다. 사용자 확정과 관리자 목록·상세 DTO 일관성 Feature D, API Expectations
2026-07-29 DEC-UTC-DATE-001 확정 신규 관리자 오디오 생성의 timezone body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 조회의 timezone query를 제거한다. 생성 releaseDate는 클라이언트가 UTC로 변환해 보내고, 상세 releaseDate와 댓글 date는 기존 필드명을 유지한 ISO-8601 UTC(Z)로 반환한다. 클라이언트별 timezone 표시 변환을 제거하고 단일 절대 시각 계약을 유지한다는 사용자 승인 Feature C, Feature E, API Expectations

13. Open Questions

  • 없음.