docs(aicharacter): 크리에이터 연결 계획을 추가한다
This commit is contained in:
235
docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql
Normal file
235
docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
-- AI 캐릭터 크리에이터 기능 최소 연결 운영 DB 반영 SQL
|
||||||
|
-- MySQL 기준. 운영 반영 전 백업과 트랜잭션/락 영향을 점검한다.
|
||||||
|
|
||||||
|
-- 1. member.member_kind 추가
|
||||||
|
SET @member_kind_column_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'member'
|
||||||
|
AND column_name = 'member_kind'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @add_member_kind_sql := IF(
|
||||||
|
@member_kind_column_exists = 0,
|
||||||
|
'ALTER TABLE member ADD COLUMN member_kind VARCHAR(30) NOT NULL DEFAULT ''HUMAN'' COMMENT ''Member 주체 종류: HUMAN, AI_CHARACTER'' AFTER role',
|
||||||
|
'SELECT ''member.member_kind already exists'' AS message'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE add_member_kind_stmt FROM @add_member_kind_sql;
|
||||||
|
EXECUTE add_member_kind_stmt;
|
||||||
|
DEALLOCATE PREPARE add_member_kind_stmt;
|
||||||
|
|
||||||
|
-- 2. chat_character.creator_member_id nullable 컬럼 추가
|
||||||
|
SET @creator_member_column_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'chat_character'
|
||||||
|
AND column_name = 'creator_member_id'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @add_creator_member_sql := IF(
|
||||||
|
@creator_member_column_exists = 0,
|
||||||
|
'ALTER TABLE chat_character ADD COLUMN creator_member_id BIGINT NULL COMMENT ''크리에이터 기능 주체 Member ID''',
|
||||||
|
'SELECT ''chat_character.creator_member_id already exists'' AS message'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE add_creator_member_stmt FROM @add_creator_member_sql;
|
||||||
|
EXECUTE add_creator_member_stmt;
|
||||||
|
DEALLOCATE PREPARE add_creator_member_stmt;
|
||||||
|
|
||||||
|
-- 3. 기존 chat_character별 AI 캐릭터용 Member 생성 및 매핑
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
||||||
|
CREATE TEMPORARY TABLE tmp_chat_character_creator_member (
|
||||||
|
chat_character_id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
creator_member_id BIGINT NULL
|
||||||
|
) COMMENT 'chat_character와 backfill member.id 임시 매핑';
|
||||||
|
|
||||||
|
INSERT INTO tmp_chat_character_creator_member (chat_character_id)
|
||||||
|
SELECT c.id
|
||||||
|
FROM chat_character c
|
||||||
|
WHERE c.creator_member_id IS NULL;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS backfill_chat_character_creator_member;
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
CREATE PROCEDURE backfill_chat_character_creator_member()
|
||||||
|
BEGIN
|
||||||
|
DECLARE done BOOLEAN DEFAULT FALSE;
|
||||||
|
DECLARE v_chat_character_id BIGINT;
|
||||||
|
DECLARE v_name VARCHAR(255);
|
||||||
|
DECLARE v_description TEXT;
|
||||||
|
DECLARE v_image_path VARCHAR(255);
|
||||||
|
|
||||||
|
DECLARE character_cursor CURSOR FOR
|
||||||
|
SELECT c.id, c.name, c.description, c.image_path
|
||||||
|
FROM chat_character c
|
||||||
|
INNER JOIN tmp_chat_character_creator_member m
|
||||||
|
ON m.chat_character_id = c.id
|
||||||
|
WHERE m.creator_member_id IS NULL
|
||||||
|
ORDER BY c.id;
|
||||||
|
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||||
|
|
||||||
|
OPEN character_cursor;
|
||||||
|
|
||||||
|
read_loop: LOOP
|
||||||
|
FETCH character_cursor INTO v_chat_character_id, v_name, v_description, v_image_path;
|
||||||
|
|
||||||
|
IF done THEN
|
||||||
|
LEAVE read_loop;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO member (
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
nickname,
|
||||||
|
profile_image,
|
||||||
|
provider,
|
||||||
|
gender,
|
||||||
|
role,
|
||||||
|
member_kind,
|
||||||
|
is_visible_donation_rank,
|
||||||
|
donation_ranking_period,
|
||||||
|
is_active,
|
||||||
|
container,
|
||||||
|
introduce,
|
||||||
|
instagram_url,
|
||||||
|
fancimm_url,
|
||||||
|
x_url,
|
||||||
|
youtube_url,
|
||||||
|
website_url,
|
||||||
|
blog_url,
|
||||||
|
pg_charge_can,
|
||||||
|
pg_reward_can,
|
||||||
|
google_charge_can,
|
||||||
|
google_reward_can,
|
||||||
|
apple_charge_can,
|
||||||
|
apple_reward_can,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
NULL,
|
||||||
|
'',
|
||||||
|
v_name,
|
||||||
|
v_image_path,
|
||||||
|
'EMAIL',
|
||||||
|
'NONE',
|
||||||
|
'CREATOR',
|
||||||
|
'AI_CHARACTER',
|
||||||
|
TRUE,
|
||||||
|
'CUMULATIVE',
|
||||||
|
TRUE,
|
||||||
|
'web',
|
||||||
|
COALESCE(v_description, ''),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE tmp_chat_character_creator_member
|
||||||
|
SET creator_member_id = LAST_INSERT_ID()
|
||||||
|
WHERE chat_character_id = v_chat_character_id;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE character_cursor;
|
||||||
|
END //
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL backfill_chat_character_creator_member();
|
||||||
|
DROP PROCEDURE IF EXISTS backfill_chat_character_creator_member;
|
||||||
|
|
||||||
|
UPDATE chat_character c
|
||||||
|
INNER JOIN tmp_chat_character_creator_member m
|
||||||
|
ON m.chat_character_id = c.id
|
||||||
|
SET c.creator_member_id = m.creator_member_id
|
||||||
|
WHERE c.creator_member_id IS NULL
|
||||||
|
AND m.creator_member_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- 4. unique index 추가
|
||||||
|
SET @creator_member_unique_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'chat_character'
|
||||||
|
AND index_name = 'uk_chat_character_creator_member'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @add_creator_member_unique_sql := IF(
|
||||||
|
@creator_member_unique_exists = 0,
|
||||||
|
'ALTER TABLE chat_character ADD UNIQUE INDEX uk_chat_character_creator_member (creator_member_id)',
|
||||||
|
'SELECT ''uk_chat_character_creator_member already exists'' AS message'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE add_creator_member_unique_stmt FROM @add_creator_member_unique_sql;
|
||||||
|
EXECUTE add_creator_member_unique_stmt;
|
||||||
|
DEALLOCATE PREPARE add_creator_member_unique_stmt;
|
||||||
|
|
||||||
|
-- 5. foreign key 추가
|
||||||
|
SET @creator_member_fk_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'chat_character'
|
||||||
|
AND constraint_name = 'fk_chat_character_creator_member'
|
||||||
|
AND constraint_type = 'FOREIGN KEY'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @add_creator_member_fk_sql := IF(
|
||||||
|
@creator_member_fk_exists = 0,
|
||||||
|
'ALTER TABLE chat_character ADD CONSTRAINT fk_chat_character_creator_member FOREIGN KEY (creator_member_id) REFERENCES member (id)',
|
||||||
|
'SELECT ''fk_chat_character_creator_member already exists'' AS message'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE add_creator_member_fk_stmt FROM @add_creator_member_fk_sql;
|
||||||
|
EXECUTE add_creator_member_fk_stmt;
|
||||||
|
DEALLOCATE PREPARE add_creator_member_fk_stmt;
|
||||||
|
|
||||||
|
-- 6. 운영 반영 전 필수 검증. 두 결과 모두 0이어야 한다.
|
||||||
|
SELECT COUNT(*) AS invalid_ai_character_member_count
|
||||||
|
FROM member
|
||||||
|
WHERE member_kind = 'AI_CHARACTER'
|
||||||
|
AND role <> 'CREATOR';
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS missing_creator_member_count
|
||||||
|
FROM chat_character
|
||||||
|
WHERE creator_member_id IS NULL;
|
||||||
|
|
||||||
|
-- 7. 검증 완료 후 creator_member_id NOT NULL 전환
|
||||||
|
SET @missing_creator_member_count := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM chat_character
|
||||||
|
WHERE creator_member_id IS NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @creator_member_nullable := (
|
||||||
|
SELECT is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'chat_character'
|
||||||
|
AND column_name = 'creator_member_id'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @modify_creator_member_not_null_sql := IF(
|
||||||
|
@missing_creator_member_count = 0 AND @creator_member_nullable = 'YES',
|
||||||
|
'ALTER TABLE chat_character MODIFY COLUMN creator_member_id BIGINT NOT NULL COMMENT ''크리에이터 기능 주체 Member ID''',
|
||||||
|
'SELECT ''chat_character.creator_member_id not modified; verify missing_creator_member_count is 0 and column is nullable'' AS message'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE modify_creator_member_not_null_stmt FROM @modify_creator_member_not_null_sql;
|
||||||
|
EXECUTE modify_creator_member_not_null_stmt;
|
||||||
|
DEALLOCATE PREPARE modify_creator_member_not_null_stmt;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
||||||
261
docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md
Normal file
261
docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# AI 캐릭터 크리에이터 기능 최소 연결 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 모든 `ChatCharacter`를 `Member(role = CREATOR, memberKind = AI_CHARACTER)`와 1:1로 연결해 로그인/DM을 제외한 기존 크리에이터 기능을 최소 변경으로 재사용한다.
|
||||||
|
|
||||||
|
**Architecture:** 기존 크리에이터 기능의 소유자는 계속 `Member`로 유지한다. `ChatCharacter`는 `creatorMember`를 단방향 `OneToOne`으로 가지며, AI 캐릭터용 Member의 표시 정보는 `ChatCharacter` 값에서 스냅샷으로 동기화한다. 사람/AI 주체 구분은 `Member.memberKind`로 처리하고, 로그인/DM만 명시적으로 차단한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Security, JPA/Hibernate, QueryDSL, MySQL, Gradle Wrapper, JUnit5, Mockito.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- 포함: `MemberKind` 추가, `ChatCharacter.creatorMember` 1:1 관계 추가, MySQL DDL/backfill SQL, AI 캐릭터용 Member 생성/표시 정보 동기화, 로그인 차단, DM 차단.
|
||||||
|
- 제외: `creator_identity` 도입, `ChatCharacter` 독립 소유자화, 검색 카테고리 개편, `Member:ChatCharacter = 1:N`, AI 캐릭터용 콘텐츠/라이브/커뮤니티 대리 생성 API 설계.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt`
|
||||||
|
- `MemberKind` enum과 `memberKind` 필드 추가.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt`
|
||||||
|
- `creatorMember: Member` 단방향 `OneToOne` 추가.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||||||
|
- `creatorMember` 조회/검증 메서드 추가.
|
||||||
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberService.kt`
|
||||||
|
- AI 캐릭터용 Member 생성 및 표시 정보 동기화 책임.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt`
|
||||||
|
- 캐릭터 생성/수정 시 `ChatCharacterCreatorMemberService` 호출.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`
|
||||||
|
- 일반 로그인에서 `memberKind = AI_CHARACTER` 차단.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberService.kt`
|
||||||
|
- 크리에이터 관리자 로그인에서 `memberKind = AI_CHARACTER` 차단.
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt`
|
||||||
|
- DM 생성/메시지 발송 대상이 `memberKind = AI_CHARACTER`이면 차단.
|
||||||
|
- Create: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql`
|
||||||
|
- 운영 DB 반영용 MySQL DDL/backfill/검증 SQL.
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberServiceTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: MemberKind 및 DB 마이그레이션 기반 추가
|
||||||
|
|
||||||
|
- [x] **Task 1.1: `MemberKind` 필드 추가**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt`
|
||||||
|
- Test: 기존 컴파일 회귀
|
||||||
|
- RED: `Member.memberKind`를 참조하는 최소 컴파일 테스트 또는 이후 task 테스트를 먼저 작성하면 현재 컴파일이 실패해야 한다.
|
||||||
|
- GREEN: `Member` 생성자에 기본값 `MemberKind.HUMAN`을 가진 non-null 필드를 추가한다.
|
||||||
|
- 구현 기준:
|
||||||
|
```kotlin
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
var memberKind: MemberKind = MemberKind.HUMAN
|
||||||
|
```
|
||||||
|
```kotlin
|
||||||
|
enum class MemberKind {
|
||||||
|
HUMAN, AI_CHARACTER
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- REFACTOR: `MemberRole`과 `MemberKind` 의미가 섞이지 않도록 주석은 최소화하고, 정책 판단은 각 서비스 task에서 명시한다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||||
|
- Expected: 기존 테스트 컴파일 및 통과.
|
||||||
|
|
||||||
|
- [x] **Task 1.2: 운영 DB DDL/backfill SQL 작성**
|
||||||
|
- Create: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql`
|
||||||
|
- TDD 예외 사유: 운영 DDL 문서 작성은 단위 테스트 대상이 아니다.
|
||||||
|
- 대체 검증 방법: SQL 문법과 PRD 요구사항을 수동 점검하고 검증 쿼리를 포함한다.
|
||||||
|
- SQL 작성 기준:
|
||||||
|
- `member.member_kind varchar(30) not null default 'HUMAN' comment 'Member 주체 종류: HUMAN, AI_CHARACTER'`
|
||||||
|
- `chat_character.creator_member_id bigint null comment '크리에이터 기능 주체 Member ID'`
|
||||||
|
- `uk_chat_character_creator_member` unique index
|
||||||
|
- `fk_chat_character_creator_member` foreign key
|
||||||
|
- 기존 모든 `chat_character`별 AI 캐릭터용 Member 생성
|
||||||
|
- 생성된 Member를 `chat_character.creator_member_id`에 연결
|
||||||
|
- 검증 후 `chat_character.creator_member_id not null` 전환
|
||||||
|
- SQL backfill은 `email = null`, `password = ''`, `role = 'CREATOR'`, `member_kind = 'AI_CHARACTER'`로 생성한다.
|
||||||
|
- MySQL에서 insert된 Member ID를 안전하게 매핑하기 위해 저장 프로시저 또는 임시 매핑 테이블을 사용한다. `email`을 임시 식별자로 사용하지 않는다.
|
||||||
|
- Verify:
|
||||||
|
- SQL 내 검증 쿼리 포함:
|
||||||
|
```sql
|
||||||
|
select count(*) as invalid_ai_character_member_count
|
||||||
|
from member
|
||||||
|
where member_kind = 'AI_CHARACTER' and role <> 'CREATOR';
|
||||||
|
|
||||||
|
select count(*) as missing_creator_member_count
|
||||||
|
from chat_character
|
||||||
|
where creator_member_id is null;
|
||||||
|
```
|
||||||
|
- Expected: 운영 반영 전 두 검증 쿼리 결과가 모두 0이어야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: ChatCharacter와 AI 캐릭터용 Member 연결
|
||||||
|
|
||||||
|
- [ ] **Task 2.1: `ChatCharacter.creatorMember` 관계 추가**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||||
|
- RED: 테스트에서 `ChatCharacter.creatorMember`에 접근하거나 `findByCreatorMemberId`를 호출하게 작성해 컴파일 실패를 확인한다.
|
||||||
|
- GREEN: `ChatCharacter`에 단방향 `OneToOne`을 추가한다.
|
||||||
|
```kotlin
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "creator_member_id", nullable = false, unique = true)
|
||||||
|
var creatorMember: Member? = null
|
||||||
|
```
|
||||||
|
- Repository 메서드 기준:
|
||||||
|
```kotlin
|
||||||
|
fun findByCreatorMemberId(creatorMemberId: Long): ChatCharacter?
|
||||||
|
fun existsByCreatorMemberId(creatorMemberId: Long): Boolean
|
||||||
|
```
|
||||||
|
- REFACTOR: `ChatCharacter`에서 `Member` import만 추가하고, 기존 캐릭터 필드/관계는 변경하지 않는다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||||
|
- Expected: 관계 접근 컴파일 및 테스트 통과.
|
||||||
|
|
||||||
|
- [ ] **Task 2.2: AI 캐릭터용 Member 생성/표시 정보 동기화 서비스 추가**
|
||||||
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberService.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||||
|
- RED: 아래 테스트를 먼저 작성한다.
|
||||||
|
- `shouldCreateAiCharacterMemberAndCopyDisplayFields`
|
||||||
|
- `shouldSyncAiCharacterMemberDisplayFields`
|
||||||
|
- `shouldNotOverwriteHumanCreatorDisplayFields`
|
||||||
|
- 테스트 기대:
|
||||||
|
- 생성 시 `role = CREATOR`, `memberKind = AI_CHARACTER`, `email = null`, `password = ""`
|
||||||
|
- `Member.nickname = ChatCharacter.name`
|
||||||
|
- `Member.profileImage = ChatCharacter.imagePath`
|
||||||
|
- `Member.introduce = ChatCharacter.description`
|
||||||
|
- 연결된 `creatorMember.memberKind = HUMAN`이면 표시 정보 덮어쓰기 없음.
|
||||||
|
- GREEN: 서비스 API를 아래 기준으로 구현한다.
|
||||||
|
```kotlin
|
||||||
|
fun ensureAiCharacterCreatorMember(chatCharacter: ChatCharacter): Member
|
||||||
|
fun syncAiCharacterCreatorMemberDisplayFields(chatCharacter: ChatCharacter)
|
||||||
|
```
|
||||||
|
- 구현 정책:
|
||||||
|
- `chatCharacter.creatorMember == null`이면 AI 캐릭터용 Member를 생성하고 연결한다.
|
||||||
|
- `chatCharacter.creatorMember.memberKind == AI_CHARACTER`이면 표시 정보를 동기화한다.
|
||||||
|
- `chatCharacter.creatorMember.memberKind == HUMAN`이면 사람 크리에이터 프로필을 덮어쓰지 않는다.
|
||||||
|
- REFACTOR: 동기화 로직은 `ChatCharacterService`에 직접 흩뿌리지 않고 이 서비스로 모은다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||||
|
- Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Task 2.3: 캐릭터 생성/수정 흐름에 AI 캐릭터용 Member 연결**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||||
|
- RED: 캐릭터 생성 후 `creatorMember`가 생성되고, 이미지 저장 후 `Member.profileImage`가 갱신되는 테스트를 작성한다.
|
||||||
|
- GREEN:
|
||||||
|
- `createChatCharacter` 또는 `createChatCharacterWithDetails` 저장 후 `ensureAiCharacterCreatorMember`를 호출한다.
|
||||||
|
- 관리자 등록 컨트롤러에서 이미지 저장 후 `chatCharacter.imagePath`를 설정하고 저장한 뒤 `syncAiCharacterCreatorMemberDisplayFields`를 호출한다.
|
||||||
|
- `updateChatCharacterWithDetails`에서 이름/설명/이미지 변경 후 `syncAiCharacterCreatorMemberDisplayFields`를 호출한다.
|
||||||
|
- REFACTOR: 외부 API 호출, S3 업로드, 원작 연결, 언어 감지 이벤트 흐름은 기존 순서를 유지한다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||||
|
- Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: 로그인 및 DM 차단
|
||||||
|
|
||||||
|
- [ ] **Task 3.1: 일반 로그인에서 AI 캐릭터용 Member 차단**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt`
|
||||||
|
- RED: `memberKind = AI_CHARACTER`인 Member가 일반 로그인 요청 시 인증 매니저 호출 전에 예외가 발생하는 테스트를 작성한다.
|
||||||
|
- GREEN: `MemberService.login(...)`의 Member 조회/활성 검증 직후 아래 정책을 추가한다.
|
||||||
|
```kotlin
|
||||||
|
if (member.memberKind == MemberKind.AI_CHARACTER) {
|
||||||
|
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- REFACTOR: 기존 `provider`, `isCreator`, `isAdmin` 검증 순서는 불필요하게 바꾸지 않는다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.member.MemberServiceTest`
|
||||||
|
- Expected: AI 캐릭터 로그인 차단 테스트 PASS.
|
||||||
|
|
||||||
|
- [ ] **Task 3.2: 크리에이터 관리자 로그인에서 AI 캐릭터용 Member 차단**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberService.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberServiceTest.kt`
|
||||||
|
- RED: `memberKind = AI_CHARACTER`, `role = CREATOR`인 Member가 크리에이터 관리자 로그인 요청 시 `common.error.bad_credentials` 예외가 발생하는 테스트를 작성한다.
|
||||||
|
- GREEN: `CreatorAdminMemberService.login(email, password)`에서 role 검증 전 또는 직후 AI 캐릭터용 Member를 차단한다.
|
||||||
|
```kotlin
|
||||||
|
if (member.memberKind == MemberKind.AI_CHARACTER) {
|
||||||
|
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- REFACTOR: `AGENT` 로그인 허용 정책은 변경하지 않는다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest`
|
||||||
|
- Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Task 3.3: 유저-크리에이터 DM에서 AI 캐릭터용 Member 차단**
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt`
|
||||||
|
- RED: `shouldRejectCreateRoomWhenCreatorIsAiCharacterMember` 테스트를 추가한다.
|
||||||
|
- `memberRepository.findById(creatorId)`는 `role = CREATOR`, `memberKind = AI_CHARACTER`인 Member를 반환한다.
|
||||||
|
- `service.createOrGetRoom(user, creatorId)`는 예외를 던진다.
|
||||||
|
- `roomRepository.save`와 `participantRepository.save`는 호출되지 않는다.
|
||||||
|
- GREEN: `validateRecipient` 또는 `createOrGetRoom`에서 recipient가 AI 캐릭터용 Member이면 차단한다.
|
||||||
|
```kotlin
|
||||||
|
if (recipient.memberKind == MemberKind.AI_CHARACTER) {
|
||||||
|
throw SodaException(messageKey = "message.error.recipient_not_found")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- REFACTOR: 기존 비활성/본인/차단 검증 메시지와 우선순위를 불필요하게 바꾸지 않는다.
|
||||||
|
- Verify:
|
||||||
|
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||||
|
- Expected: 기존 DM 테스트와 신규 차단 테스트 PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: 회귀 검증 및 문서 정리
|
||||||
|
|
||||||
|
- [ ] **Task 4.1: 핵심 단위 테스트 실행**
|
||||||
|
- Files: 변경 없음
|
||||||
|
- TDD 예외 사유: 구현 완료 후 회귀 검증 task다.
|
||||||
|
- 대체 검증 방법: 관련 단일 테스트를 모두 실행한다.
|
||||||
|
- Run:
|
||||||
|
```bash
|
||||||
|
./gradlew test \
|
||||||
|
--tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest \
|
||||||
|
--tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest \
|
||||||
|
--tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest \
|
||||||
|
--tests kr.co.vividnext.sodalive.member.MemberServiceTest
|
||||||
|
```
|
||||||
|
- Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Task 4.2: 정적 검증 및 전체 회귀**
|
||||||
|
- Files: 변경 없음
|
||||||
|
- TDD 예외 사유: 전체 회귀 검증 task다.
|
||||||
|
- 대체 검증 방법: Gradle 테스트와 ktlint를 실행한다.
|
||||||
|
- Run:
|
||||||
|
```bash
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
- Expected: 두 명령 모두 PASS.
|
||||||
|
|
||||||
|
- [ ] **Task 4.3: 검증 기록 누적**
|
||||||
|
- Modify: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md`
|
||||||
|
- TDD 예외 사유: 문서 기록 task다.
|
||||||
|
- 대체 검증 방법: 실행한 명령, 목적, 결과를 아래 검증 기록 섹션에 누적한다.
|
||||||
|
- 기록 형식:
|
||||||
|
```markdown
|
||||||
|
- `./gradlew test --tests ...`
|
||||||
|
- 목적: [무엇을 검증했는지]
|
||||||
|
- 결과: PASS 또는 실패 내용
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
- `./gradlew tasks --all`
|
||||||
|
- 목적: 문서 변경 후 Gradle 명령 유효성 확인.
|
||||||
|
- 결과: 최초 실행은 sandbox가 `/Users/.../gradle-8.1.1-bin.zip.lck` 파일에 접근하지 못해 실패. 권한 승인 후 재실행하여 `BUILD SUCCESSFUL in 13s`.
|
||||||
|
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||||
|
- 목적: `Member.memberKind` 추가 후 Phase 1 계획에 명시된 기존 DM 테스트 컴파일 및 회귀 확인.
|
||||||
|
- 결과: `BUILD SUCCESSFUL in 2m 3s`.
|
||||||
|
- `./gradlew tasks --all`
|
||||||
|
- 목적: Phase 1 문서 및 운영 DB 반영용 SQL 추가 후 Gradle 명령 유효성 재확인.
|
||||||
|
- 결과: `BUILD SUCCESSFUL in 8s`.
|
||||||
208
docs/20260611_AI캐릭터_크리에이터기능_최소연결/prd.md
Normal file
208
docs/20260611_AI캐릭터_크리에이터기능_최소연결/prd.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# PRD: AI 캐릭터 크리에이터 기능 최소 연결
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
`ChatCharacter`가 기존 크리에이터 기능을 최소 변경으로 사용할 수 있도록, 모든 `ChatCharacter`를 `Member(role = CREATOR)`와 1:1로 연결하고 실제 사람 Member와 AI 캐릭터용 Member를 `memberKind`로 구분한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 현재 `ChatCharacter`는 AI 대화 주체로만 동작하고, 라이브/콘텐츠/커뮤니티/채널 후원/정산/알림 등 크리에이터 기능의 주체가 될 수 없다.
|
||||||
|
- 기존 크리에이터 기능은 대부분 `Member(role = CREATOR)`와 `member.id` 기반 `creatorId`를 전제로 구현되어 있다.
|
||||||
|
- `ChatCharacter`를 독립 소유자로 직접 도입하면 콘텐츠, 라이브, 후원, 정산, 랭킹, 알림, 차단, 팔로우 등 넓은 범위의 소유자 모델 변경이 필요하다.
|
||||||
|
- 이번 변경은 기존 `Member` 기반 크리에이터 기능을 유지하면서, AI 캐릭터가 크리에이터 기능을 사용할 수 있는 최소 연결 구조가 필요하다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- `Member`에 `MemberKind`를 추가해 실제 사람 Member와 AI 캐릭터용 Member를 구분한다.
|
||||||
|
- 모든 기존 `ChatCharacter`에 대응되는 `Member(role = CREATOR, memberKind = AI_CHARACTER)`를 생성하고 1:1로 연결할 수 있는 마이그레이션 SQL을 준비한다.
|
||||||
|
- 신규/기존 `ChatCharacter`는 크리에이터 기능 주체인 `creatorMember`를 가진다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 로그인할 수 없도록 차단한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 유저-크리에이터 DM 생성 대상이 될 수 없도록 차단한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 로그인과 DM을 제외하고 `Member(role = CREATOR)`가 사용할 수 있는 기존 크리에이터 기능을 사용할 수 있어야 한다.
|
||||||
|
- AI 캐릭터용 Member의 표시 정보는 연결된 `ChatCharacter`의 이름, 프로필 이미지, 소개를 스냅샷으로 복사해 기존 Member 기반 화면과 쿼리를 재사용한다.
|
||||||
|
- 기존 사람 크리에이터의 콘텐츠/라이브/커뮤니티/채널 후원/정산/알림 동작은 유지한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- 이번 범위에서 `ChatCharacter`를 `Member`와 동급의 별도 소유자 타입으로 만들지 않는다.
|
||||||
|
- 이번 범위에서 `creator_identity` 같은 공통 크리에이터 소유자 테이블을 도입하지 않는다.
|
||||||
|
- 이번 범위에서 공개 API의 기존 `creatorId = member.id` 의미를 변경하지 않는다.
|
||||||
|
- 이번 범위에서 크리에이터 검색 결과 카테고리 개편은 구현하지 않는다.
|
||||||
|
- 이번 범위에서 `Member:ChatCharacter = 1:N` 관계를 허용하지 않는다.
|
||||||
|
- 이번 범위에서 AI 캐릭터용 Member의 직접 로그인, 직접 크리에이터 관리자 접속, 직접 DM 기능은 허용하지 않는다.
|
||||||
|
- 이번 범위에서 AI 캐릭터용 콘텐츠/라이브/커뮤니티 대리 생성 API를 새로 설계하지 않는다.
|
||||||
|
- 이번 범위에서 기존 정산 산식, 정산 비율, 랭킹 점수 산식은 변경하지 않는다.
|
||||||
|
- 이번 범위에서 AI 캐릭터용 Member를 정산 관리자 화면에서 사람 크리에이터와 별도 목록 또는 별도 필터로 분리하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 일반 사용자: AI 캐릭터와 AI 대화를 하고, AI 캐릭터 채널에 후원하거나 AI 캐릭터 콘텐츠를 소비하는 회원
|
||||||
|
- 사람 크리에이터: 필요 시 자신의 `Member`에 연결된 `ChatCharacter`를 통해 AI 대화 기능을 제공하는 크리에이터
|
||||||
|
- 운영/정산 담당자: AI 캐릭터용 Member를 기존 크리에이터 정산 흐름에서 식별하고 처리해야 하는 담당자
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 모든 활성 `ChatCharacter`와 AI 대화를 시작하고 싶다.
|
||||||
|
- 사용자는 AI 캐릭터 채널에도 기존 크리에이터 채널처럼 채널 후원을 하고 싶다.
|
||||||
|
- 사용자는 AI 캐릭터가 업로드한 콘텐츠를 기존 콘텐츠와 같은 방식으로 보고 싶다.
|
||||||
|
- 시스템은 AI 캐릭터용 Member가 실제 사람 계정처럼 로그인하거나 DM 대상이 되는 것을 막고 싶다.
|
||||||
|
- 운영자는 사람 크리에이터와 AI 캐릭터용 크리에이터 Member를 데이터에서 명확히 구분하고 싶다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Feature A. `MemberKind` 도입
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `Member`에 `memberKind` 필드를 추가한다.
|
||||||
|
- `MemberKind` 값은 최소 다음 2개를 가진다.
|
||||||
|
- `HUMAN`: 실제 사람 Member
|
||||||
|
- `AI_CHARACTER`: AI 캐릭터 기능을 위해 생성된 내부 크리에이터 Member
|
||||||
|
- `memberKind`는 `NOT NULL`이며 기본값은 `HUMAN`이다.
|
||||||
|
- 기존 모든 Member 데이터는 DDL 기본값에 의해 `memberKind = HUMAN`이 된다.
|
||||||
|
- 일반 회원가입, 관리자, 에이전트, 콘텐츠 관리자 등 실제 사람 계정은 `memberKind = HUMAN`을 사용한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member도 `role = CREATOR`를 가진다.
|
||||||
|
- 크리에이터 기능 가능 여부는 기존처럼 기본적으로 `role = CREATOR`를 기준으로 유지한다.
|
||||||
|
- 사람 크리에이터 전용 기능 가능 여부는 `role = CREATOR`와 `memberKind = HUMAN`을 함께 기준으로 판단한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 로그인과 DM을 제외한 팔로우, 채널 후원, 콘텐츠, 커뮤니티, 라이브, 정산, 알림 등 기존 CREATOR 기능의 대상이 될 수 있다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `memberKind = HUMAN`만으로 사람 크리에이터 여부를 판단하면 안 되며, 반드시 `role = CREATOR` 조건을 함께 확인해야 한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 반드시 `role = CREATOR`여야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feature B. `ChatCharacter`와 `Member` 1:1 연결
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `ChatCharacter`가 관계의 주인이며 `creatorMember`를 가진다.
|
||||||
|
- 관계는 초기에는 1:1로 제한한다.
|
||||||
|
- DB에는 `chat_character.creator_member_id`를 추가한다.
|
||||||
|
- `chat_character.creator_member_id`는 `member.id`를 참조한다.
|
||||||
|
- `chat_character.creator_member_id`에는 unique 제약을 둔다.
|
||||||
|
- `ChatCharacter.creatorMember.role`은 반드시 `CREATOR`여야 한다.
|
||||||
|
- 기존 모든 `ChatCharacter`는 마이그레이션 후 `creatorMember`가 있어야 한다.
|
||||||
|
- 기존 `ChatCharacter` 중 실제 사람 크리에이터와 연결해야 하는 데이터는 이번 마이그레이션 대상에 없다고 본다.
|
||||||
|
- 기존 모든 `ChatCharacter`는 새 `Member(role = CREATOR, memberKind = AI_CHARACTER)`를 생성해 연결한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 이미 연결된 `ChatCharacter`에 중복 `creatorMember`가 배정되면 안 된다.
|
||||||
|
- 하나의 `Member`에 여러 `ChatCharacter`가 연결되면 안 된다.
|
||||||
|
- 비활성 `ChatCharacter`도 기존 데이터 정합성을 위해 `creatorMember` 연결 대상에 포함한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feature C. 기존 `ChatCharacter`용 Member 생성 마이그레이션
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 운영 DB 반영용 MySQL 기준 DDL과 backfill SQL을 작성한다.
|
||||||
|
- 마이그레이션 SQL은 기존 `ChatCharacter`별로 AI 캐릭터용 `Member`를 생성할 수 있어야 한다.
|
||||||
|
- 생성되는 AI 캐릭터용 Member는 다음 정책을 따른다.
|
||||||
|
- `role = CREATOR`
|
||||||
|
- `memberKind = AI_CHARACTER`
|
||||||
|
- `email = null`
|
||||||
|
- `password = ""`
|
||||||
|
- `nickname`은 기본적으로 `ChatCharacter.name` 기준
|
||||||
|
- `profileImage`는 기본적으로 `ChatCharacter.imagePath` 기준
|
||||||
|
- `introduce`는 기본적으로 `ChatCharacter.description` 기준
|
||||||
|
- AI 캐릭터용 Member의 `nickname`, `profileImage`, `introduce`는 기존 콘텐츠/라이브/커뮤니티/후원/정산/알림/팔로우/AGENT 소속 화면에서 별도 `ChatCharacter` JOIN 없이 표시하기 위한 스냅샷이다.
|
||||||
|
- backfill 후 `chat_character.creator_member_id`가 없는 row가 0건인지 검증하는 SQL을 포함한다.
|
||||||
|
- 검증 완료 후 `chat_character.creator_member_id`를 `NOT NULL`로 전환할 수 있어야 한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `ChatCharacter.name`이 중복되더라도 Member 생성이 가능해야 한다.
|
||||||
|
- AI 캐릭터용 Member의 로그인 차단은 `email/password` 값이 아니라 `memberKind = AI_CHARACTER` 정책으로 보장해야 한다.
|
||||||
|
- 기존 `ChatCharacter`의 사람 크리에이터 수동 매핑은 이번 범위에서 제공하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feature D. AI 캐릭터 표시 정보 동기화
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- AI 캐릭터용 Member의 표시 정보는 연결된 `ChatCharacter` 값을 기준으로 유지한다.
|
||||||
|
- `ChatCharacter.name`은 `Member.nickname`에 동기화한다.
|
||||||
|
- `ChatCharacter.imagePath`는 `Member.profileImage`에 동기화한다.
|
||||||
|
- `ChatCharacter.description`은 `Member.introduce`에 동기화한다.
|
||||||
|
- `ChatCharacter` 생성 시 AI 캐릭터용 Member를 함께 생성하는 경우 같은 transaction 안에서 표시 정보를 복사한다.
|
||||||
|
- `ChatCharacter` 수정 시 연결된 AI 캐릭터용 Member의 표시 정보도 같은 transaction 안에서 갱신한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member의 표시 정보는 직접 수정 API가 아니라 `ChatCharacter` 생성/수정 흐름을 기준으로 관리한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 동기화 대상 `creatorMember`가 없으면 저장을 실패시켜 데이터 불일치를 막아야 한다.
|
||||||
|
- 연결된 `creatorMember.memberKind != AI_CHARACTER`인 경우, 사람 크리에이터의 프로필을 덮어쓰지 않도록 동기화 대상에서 제외하거나 별도 정책을 명확히 적용해야 한다.
|
||||||
|
- 기존 Member 기반 화면은 AI 캐릭터 표시 정보를 조회할 때 별도 `ChatCharacter` JOIN을 추가하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feature E. AI 캐릭터용 Member 로그인 차단
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member는 모든 일반 로그인 흐름에서 인증 성공 상태가 되면 안 된다.
|
||||||
|
- 크리에이터 관리자 로그인 흐름에서도 `memberKind = AI_CHARACTER`인 Member는 로그인할 수 없어야 한다.
|
||||||
|
- 소셜 로그인 또는 토큰 재발급 흐름에서 AI 캐릭터용 Member가 세션/토큰을 얻을 수 있는 경로가 있으면 차단한다.
|
||||||
|
- 차단 시 기존 인증 실패 응답 패턴을 우선 재사용한다.
|
||||||
|
- AI 캐릭터용 Member는 로그인에 사용하지 않으므로 `email`은 `null`을 허용하고, `password`는 기존 소셜 회원 생성 패턴과 같이 빈 문자열을 사용할 수 있다.
|
||||||
|
- 로그인 차단은 `email/password` 값이 아니라 `memberKind = AI_CHARACTER` 정책으로 보장한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 기존 토큰을 이미 가진 AI 캐릭터용 Member가 있을 수 없도록 마이그레이션 시점과 배포 순서를 점검한다.
|
||||||
|
- 후속 범위에서 관리자/콘텐츠 관리자가 AI 캐릭터용 콘텐츠를 등록하더라도, AI 캐릭터용 Member 자체가 로그인하는 것은 허용하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feature F. AI 캐릭터용 Member DM 차단
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 유저-크리에이터 DM 생성 대상이 `memberKind = AI_CHARACTER`이면 DM 방을 생성하지 않는다.
|
||||||
|
- 기존 사람 크리에이터는 `ChatCharacter` 연결 여부와 무관하게 DM이 가능해야 한다.
|
||||||
|
- DM 차단 기준은 `ChatCharacter` 연결 여부가 아니라 `Member.memberKind`이다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `memberKind = HUMAN`인 사람 크리에이터가 `ChatCharacter`를 가진 경우에도 DM은 가능해야 한다.
|
||||||
|
- `memberKind = AI_CHARACTER`인 Member가 `role = CREATOR`이더라도 DM은 불가능해야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Technical Constraints
|
||||||
|
- Kotlin, Spring Boot 2.7.14, Java 17, Gradle Wrapper 구조를 유지한다.
|
||||||
|
- 기존 공개 API의 `creatorId` 의미는 이번 범위에서 변경하지 않는다.
|
||||||
|
- 기존 콘텐츠/라이브/커뮤니티/후원/정산 테이블의 소유자 컬럼은 `Member` 기준을 유지한다.
|
||||||
|
- `ChatCharacter`와 `Member` 관계는 초기에는 `ChatCharacter` 단방향 `OneToOne`으로 구현한다.
|
||||||
|
- 운영 DB 반영용 DDL은 MySQL 기준으로 작성한다.
|
||||||
|
- DDL 컬럼에는 가능한 경우 `COMMENT`를 추가한다.
|
||||||
|
- `memberKind` 기반 정책 판단은 중복 분기를 줄이기 위해 정책 함수 또는 명확한 서비스 검증으로 모은다.
|
||||||
|
- 검색 결과 카테고리 개편은 이번 구현에서 제외하되, 향후 `memberKind`를 활용할 수 있도록 데이터 모델만 준비한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Data Migration Requirements
|
||||||
|
- Phase 1 DDL
|
||||||
|
- `member.member_kind`를 `NOT NULL DEFAULT 'HUMAN'`으로 추가
|
||||||
|
- `chat_character.creator_member_id` nullable 추가
|
||||||
|
- `chat_character.creator_member_id` FK 및 unique index 추가
|
||||||
|
- Phase 2 backfill
|
||||||
|
- 기존 모든 `ChatCharacter`별로 AI 캐릭터용 `Member(role = CREATOR, memberKind = AI_CHARACTER)`를 생성한다.
|
||||||
|
- 생성된 Member를 `chat_character.creator_member_id`에 연결한다.
|
||||||
|
- Phase 3 검증 및 제약 강화
|
||||||
|
- `member_kind = 'AI_CHARACTER' and role <> 'CREATOR'` row가 0건인지 확인한다.
|
||||||
|
- `chat_character.creator_member_id is null` row가 0건인지 확인한다.
|
||||||
|
- `chat_character.creator_member_id`를 `NOT NULL`로 변경한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Metrics
|
||||||
|
- 기존 `ChatCharacter` 중 `creator_member_id` 누락 0건
|
||||||
|
- `memberKind = AI_CHARACTER`이면서 `role != CREATOR`인 Member 0건
|
||||||
|
- `memberKind = AI_CHARACTER` Member 로그인 성공 0건
|
||||||
|
- `memberKind = AI_CHARACTER` Member 대상 DM 생성 성공 0건
|
||||||
|
- 기존 사람 크리에이터의 DM, 콘텐츠 등록, 채널 후원 흐름 회귀 실패 0건
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions
|
||||||
|
- AI 캐릭터용 콘텐츠/라이브/커뮤니티 등록 운영 흐름은 이번 범위에서 구현하지 않으므로, 후속 범위에서 `chatCharacterId` 기반 대리 생성 API 정책을 별도로 정해야 한다.
|
||||||
Reference in New Issue
Block a user