From 082d8457eb91b1cb0d6b53e47da9009c6ab92500 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 12 Jun 2026 13:57:52 +0900 Subject: [PATCH] =?UTF-8?q?docs(aicharacter):=20=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=9D=B4=ED=84=B0=20=EC=97=B0=EA=B2=B0=20DDL=EC=9D=84?= =?UTF-8?q?=20=EB=B3=B4=EA=B0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alter-existing-tables.sql | 236 ++++++++++-------- .../plan-task.md | 11 +- 2 files changed, 144 insertions(+), 103 deletions(-) diff --git a/docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql b/docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql index 93617b41..d5991f68 100644 --- a/docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql +++ b/docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql @@ -20,6 +20,13 @@ PREPARE add_member_kind_stmt FROM @add_member_kind_sql; EXECUTE add_member_kind_stmt; DEALLOCATE PREPARE add_member_kind_stmt; +-- 1번 결과 확인: varchar(30), NOT NULL, DEFAULT 'HUMAN' +SELECT column_name, column_type, is_nullable, column_default, column_comment +FROM information_schema.columns +WHERE table_schema = DATABASE() + AND table_name = 'member' + AND column_name = 'member_kind'; + -- 2. chat_character.creator_member_id nullable 컬럼 추가 SET @creator_member_column_exists := ( SELECT COUNT(*) @@ -31,7 +38,7 @@ SET @creator_member_column_exists := ( 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''', + 'ALTER TABLE chat_character ADD COLUMN creator_member_id BIGINT NULL COMMENT ''크리에이터 기능 주체 Member ID'' AFTER character_type', 'SELECT ''chat_character.creator_member_id already exists'' AS message' ); @@ -39,117 +46,99 @@ PREPARE add_creator_member_stmt FROM @add_creator_member_sql; EXECUTE add_creator_member_stmt; DEALLOCATE PREPARE add_creator_member_stmt; +-- 2번 결과 확인: bigint, NULL 허용 +SELECT column_name, column_type, is_nullable, column_comment +FROM information_schema.columns +WHERE table_schema = DATABASE() + AND table_name = 'chat_character' + AND column_name = 'creator_member_id'; + -- 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, + migration_email VARCHAR(255) NOT NULL UNIQUE, creator_member_id BIGINT NULL ) COMMENT 'chat_character와 backfill member.id 임시 매핑'; -INSERT INTO tmp_chat_character_creator_member (chat_character_id) -SELECT c.id +INSERT INTO tmp_chat_character_creator_member (chat_character_id, migration_email) +SELECT + c.id, + CONCAT('__ai_character_creator_', c.id, '@migration.local') FROM chat_character c WHERE c.creator_member_id IS NULL; -DROP PROCEDURE IF EXISTS backfill_chat_character_creator_member; +-- member.email은 nullable이므로 backfill 중에만 임시 식별자로 사용하고, 매핑 후 NULL로 되돌린다. +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 +) +SELECT + m.migration_email, + '', + c.name, + c.image_path, + 'EMAIL', + 'NONE', + 'CREATOR', + 'AI_CHARACTER', + TRUE, + 'CUMULATIVE', + c.is_active, + 'web', + COALESCE(c.description, ''), + '', + '', + '', + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +FROM tmp_chat_character_creator_member m +INNER JOIN chat_character c + ON c.id = m.chat_character_id +LEFT JOIN member existing_member + ON existing_member.email = m.migration_email +WHERE existing_member.id IS NULL; -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 tmp_chat_character_creator_member m +INNER JOIN member mb + ON mb.email = m.migration_email +SET m.creator_member_id = mb.id +WHERE m.creator_member_id IS NULL + AND m.chat_character_id IS NOT NULL; UPDATE chat_character c INNER JOIN tmp_chat_character_creator_member m @@ -158,6 +147,12 @@ SET c.creator_member_id = m.creator_member_id WHERE c.creator_member_id IS NULL AND m.creator_member_id IS NOT NULL; +UPDATE member mb +INNER JOIN tmp_chat_character_creator_member m + ON m.creator_member_id = mb.id +SET mb.email = NULL +WHERE mb.email = m.migration_email; + -- 4. unique index 추가 SET @creator_member_unique_exists := ( SELECT COUNT(*) @@ -207,6 +202,10 @@ SELECT COUNT(*) AS missing_creator_member_count FROM chat_character WHERE creator_member_id IS NULL; +SELECT COUNT(*) AS remaining_migration_email_count +FROM member +WHERE email LIKE '__ai_character_creator_%@migration.local'; + -- 7. 검증 완료 후 creator_member_id NOT NULL 전환 SET @missing_creator_member_count := ( SELECT COUNT(*) @@ -233,3 +232,38 @@ 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; + +-- Rollback 참고용. 운영 반영 후 문제가 있으면 백업 복구를 우선 검토한다. +-- 아래 SQL은 이 마이그레이션으로 연결된 AI_CHARACTER Member와 제약/컬럼을 되돌리는 전체 롤백 예시다. +-- 신규 기능을 이미 운영에서 사용한 뒤에는 후속 데이터 의존성이 생길 수 있으므로 실행 전 영향 범위를 재확인한다. +-- 1) FK 제거 +-- ALTER TABLE chat_character DROP FOREIGN KEY fk_chat_character_creator_member; +-- 2) unique index 제거 +-- ALTER TABLE chat_character DROP INDEX uk_chat_character_creator_member; +-- 3) creator_member_id를 NULL 허용으로 복구 +-- ALTER TABLE chat_character MODIFY COLUMN creator_member_id BIGINT NULL COMMENT '크리에이터 기능 주체 Member ID'; +-- 4) backfill로 연결된 AI 캐릭터용 Member 삭제 준비 +-- DROP TEMPORARY TABLE IF EXISTS tmp_rollback_ai_character_member; +-- CREATE TEMPORARY TABLE tmp_rollback_ai_character_member ( +-- member_id BIGINT NOT NULL PRIMARY KEY +-- ) COMMENT 'AI 캐릭터 크리에이터 backfill 롤백 대상 Member'; +-- INSERT INTO tmp_rollback_ai_character_member (member_id) +-- SELECT DISTINCT mb.id +-- FROM member mb +-- INNER JOIN chat_character c +-- ON c.creator_member_id = mb.id +-- WHERE mb.member_kind = 'AI_CHARACTER' +-- AND mb.role = 'CREATOR'; +-- 5) chat_character 연결 해제 후 Member 삭제 +-- UPDATE chat_character c +-- INNER JOIN tmp_rollback_ai_character_member r +-- ON r.member_id = c.creator_member_id +-- SET c.creator_member_id = NULL; +-- DELETE mb +-- FROM member mb +-- INNER JOIN tmp_rollback_ai_character_member r +-- ON r.member_id = mb.id; +-- DROP TEMPORARY TABLE IF EXISTS tmp_rollback_ai_character_member; +-- 6) 컬럼 제거가 필요한 전체 스키마 롤백인 경우에만 실행 +-- ALTER TABLE chat_character DROP COLUMN creator_member_id; +-- ALTER TABLE member DROP COLUMN member_kind; diff --git a/docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md b/docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md index 5321a515..0f115938 100644 --- a/docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md +++ b/docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md @@ -74,8 +74,9 @@ - 기존 모든 `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`을 임시 식별자로 사용하지 않는다. + - SQL backfill은 최종적으로 `email = null`, `password = ''`, `role = 'CREATOR'`, `member_kind = 'AI_CHARACTER'` 상태가 되도록 생성한다. + - `member.email`은 nullable이므로 저장 프로시저 대신 backfill 중 임시 식별자로 사용할 수 있다. 단, `chat_character.creator_member_id` 매핑 후 임시 email 값은 반드시 `NULL`로 되돌린다. + - 운영 반영 후 문제에 대비해 FK/index/연결 데이터/컬럼 제거 순서의 롤백 방법을 SQL 문서에 함께 기록한다. - Verify: - SQL 내 검증 쿼리 포함: ```sql @@ -308,3 +309,9 @@ - `./gradlew test` - 목적: 리뷰 보완 후 전체 회귀 테스트 확인. - 결과: `BUILD SUCCESSFUL in 1m 15s`. +- `rg -n "CREATE PROCEDURE|CURSOR|CALL backfill_chat_character_creator_member|LAST_INSERT_ID|Rollback|임시 식별자" docs/20260611_AI캐릭터_크리에이터기능_최소연결` + - 목적: 운영 DB 반영 SQL을 저장 프로시저 없는 단순 SQL로 변경했고, 임시 email 식별자 기준과 롤백 절차가 문서에 남았는지 확인. + - 결과: 저장 프로시저/커서/CALL/LAST_INSERT_ID 패턴은 미검출. `alter-existing-tables.sql`에 임시 식별자 정리와 Rollback 절차가 존재함을 확인. +- `./gradlew tasks --all` + - 목적: 문서 변경 후 Gradle 명령 유효성 확인. + - 결과: `BUILD SUCCESSFUL in 942ms`.