--- description: 웹소설 챕터를 씬/라인 데이터로 변환하여 D1 시드 SQL 생성 --- ## User Input ```text $ARGUMENTS ``` User input **must** be considered (if not empty). ## Overview 마크다운 원고를 인터랙티브 씬/라인 데이터로 변환하여 D1 시드 SQL을 생성합니다. **실행 방법:** - `/rovel.seed content/rovel/협회 소속 양호선생님/chapters/001.md` (단일 파일) - `/rovel.seed 협회 소속 양호선생님 1화` (작품명 + 화수) - `/rovel.seed 협회 소속 양호선생님 1-8화` (범위) - `/rovel.seed 협회 소속 양호선생님 전체` (모든 챕터) **워크플로우:** ``` ┌─────────────────────────────────────────────────────────┐ │ Phase 1: 입력 파싱 │ │ - 파일 경로 또는 작품명+화수 파싱 │ │ - 작품 폴더 및 캐릭터 ID 매핑 로드 │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ Phase 2: 마크다운 파싱 │ │ - 씬 구분 (*** 기준) │ │ - 라인 타입 감지 (대화/나레이션/효과음 등) │ │ - 시스템 카드 파싱 │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ Phase 3: 메타데이터 추출 │ │ - 화자 추론 (컨텍스트 + 말투 분석) │ │ - 씬 정보 추출 (시간/장소/분위기) │ │ - is_playable 판정 │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ Phase 4: SQL 생성 │ │ - INSERT 문 생성 │ │ - 파일 저장 및 안내 │ └─────────────────────────────────────────────────────────┘ ``` --- ## Phase 1: 입력 파싱 ### 입력 유형 분석 ```yaml Case A - 절대/상대 경로: 입력: content/rovel/협회 소속 양호선생님/chapters/001.md 해석: 해당 파일 직접 처리 Case B - 작품명 + 화수: 입력: 협회 소속 양호선생님 3화 해석: content/rovel/협회 소속 양호선생님/chapters/003.md Case C - 작품명 + 범위: 입력: 협회 소속 양호선생님 1-8화 해석: 001.md ~ 008.md 순차 처리 Case D - 작품명 + 전체: 입력: 협회 소속 양호선생님 전체 해석: chapters/ 폴더의 모든 .md 파일 ``` ### 캐릭터 ID 매핑 로드 작품 폴더의 `인물목록.md`를 읽어 캐릭터 이름 → ID 매핑 생성: ```yaml 매핑 생성 규칙: - 주인공 (한시우): char-{영문이름}-001 - 히로인/주요 인물: char-{영문이름}-001 - 조연: char-{영문이름}-001 예시 (협회 소속 양호선생님): 한시우: char-hansiuw-001 (주인공) 자화연: char-jahwayeon-001 박준혁: char-parkjunhyuk-001 김 과장: char-kimgwajang-001 루나: char-luna-001 ``` ### ID 체계 ```yaml 작품 ID: work-{작품영문코드}-001 예: work-yanghosam-001 챕터 ID: chapter-{3자리화수} 예: chapter-001, chapter-008 씬 ID: scene-{3자리화수}-{2자리순서} 예: scene-001-01, scene-008-03 라인 ID: line-{3자리화수}-{2자리씬순서}-{3자리라인순서} 예: line-001-01-001, line-008-03-015 시스템 카드 ID: syscard-{3자리화수}-{2자리씬순서}-{2자리순서} 예: syscard-001-02-01 ``` --- ## Phase 2: 마크다운 파싱 ### 라인 타입 감지 규칙 ```typescript // 우선순위 순서로 적용 1. 빈 줄만 있음 → skip 2. `***` → divider (씬 구분) 3. `[제목]` 로 시작 (단, [다음 제외) → system (시스템 카드 시작) 4. `- ` 로 시작 → sfx (효과음) 5. `"` 로 시작하고 `"` 로 끝남 → dialogue (대화) 6. `'` 로 시작하고 `'` 로 끝남 → thought (속마음) 7. 그 외 → narration (나레이션) ``` ### 시스템 카드 파싱 ```markdown 입력: [진맥 판독] [대상: 박준혁, 28세, B급 헌터] [외상: 좌측 어깨 베임 (깊이 1.2cm)] [내상: 마력 과다 사용으로 인한 피로 누적] 출력: { type: "diagnosis", title: "진맥 판독", content: [ { label: "대상", value: "박준혁, 28세, B급 헌터" }, { label: "외상", value: "좌측 어깨 베임 (깊이 1.2cm)" }, { label: "내상", value: "마력 과다 사용으로 인한 피로 누적" } ], summary: "대상: 박준혁", isPlayable: true } ``` ### 시스템 카드 타입 매핑 | 제목 키워드 | 타입 | isPlayable | |------------|------|------------| | 진맥 판독, 분석, 스캔 | diagnosis | true | | 잠재력 분석 | potential | true | | 환자 심리 분석, 환자 상태 | psychology | true | | 은밀 치유 | skill_active | true | | 신규 기능 감지, 잠재 해방 | skill_unlock | false | | 경고, 숨겨진 | warning | false | | 일일 진료 현황, 권장 행동 | info_update | false | | 그 외 | info_update | false | --- ## Phase 3: 메타데이터 추출 ### 화자 추론 규칙 ```yaml 1. 이전 3줄 컨텍스트 분석: "자화연이 고개를 들었다." "......" → speaker: char-jahwayeon-001 2. 대사 내 특징적 표현: "본좌" → 자화연 "선생님" → 박준혁 (주인공에게 말할 때) 3. 주인공 여부: - 내면 독백 ('생각') → isProtagonist: true - "나는", "내가" 주어 → isProtagonist: true ``` ### 씬 메타데이터 추출 ```yaml 시간 감지: - "밤 11시", "새벽 2시", "오후 5시" 등 - 첫 번째 매칭 사용 장소 감지: - "의무실", "협회", "던전" 등 키워드 - 씬 첫 부분에서 추출 분위기 감지: - "살기", "긴장", "평온", "따뜻" 등 - 시스템 카드나 나레이션에서 추출 ``` ### is_playable 판정 ```yaml 씬이 플레이 가능한 경우: 1. isPlayable: true인 시스템 카드가 1개 이상 2. 주요 캐릭터와의 대화 장면 3. 선택지가 제시될 수 있는 상황 is_playable: true 조건: - diagnosis, potential, psychology, skill_active 타입 카드 존재 - 주요 히로인과 첫 대면 장면 ``` --- ## Phase 4: SQL 생성 ### 출력 파일 ```yaml 단일 파일: 입력: 001.md 출력: 001-seed.sql 범위/전체: 입력: 1-8화 출력: chapters-seed.sql (병합된 파일) ``` ### SQL 템플릿 ```sql -- Chapter chapter-{NNN} scenes and lines -- Auto-generated from markdown -- Delete existing data for this chapter DELETE FROM system_cards WHERE line_id IN (SELECT id FROM lines WHERE scene_id IN (SELECT id FROM scenes WHERE chapter_id = 'chapter-{NNN}')); DELETE FROM lines WHERE scene_id IN (SELECT id FROM scenes WHERE chapter_id = 'chapter-{NNN}'); DELETE FROM scenes WHERE chapter_id = 'chapter-{NNN}'; -- 챕터가 없으면 생성 INSERT OR IGNORE INTO chapters (id, work_id, number, title, is_free, price, status, view_count, created_at, updated_at) VALUES ( 'chapter-{NNN}', '{work_id}', {number}, '{title}', {is_free}, {price}, 'published', 0, {timestamp}, {timestamp} ); -- Scene 1: {title} INSERT INTO scenes (id, chapter_id, "order", title, location, time, mood, is_playable, play_description, character_ids, created_at) VALUES ( 'scene-{NNN}-{NN}', 'chapter-{NNN}', {order}, '{title}', '{location}', '{time}', '{mood}', {is_playable}, '{play_description}', '{character_ids_json}', {timestamp} ); -- Lines for scene {N} INSERT INTO lines (id, scene_id, "order", type, content, speaker_id, is_protagonist, created_at) VALUES ('line-{NNN}-{NN}-{NNN}', 'scene-{NNN}-{NN}', {order}, '{type}', '{content}', {speaker_id}, {is_protagonist}, {timestamp}), ...; -- System cards for scene {N} INSERT INTO system_cards (id, line_id, type, title, content, summary, is_playable, choice_label, created_at) VALUES ('syscard-{NNN}-{NN}-{NN}', 'line-{NNN}-{NN}-{NNN}', '{type}', '{title}', '{content_json}', '{summary}', {is_playable}, '{choice_label}', {timestamp}); ``` ### SQL 이스케이프 규칙 ```yaml 작은따옴표: ' → '' 줄바꿈: \n → 그대로 (TEXT 필드) JSON: 쌍따옴표 사용, 이스케이프 NULL: speaker_id 없으면 NULL (문자열 'NULL' 아님) ``` --- ## 실행 예시 ### 예시 1: 단일 파일 ``` /rovel.seed content/rovel/협회 소속 양호선생님/chapters/008.md ``` **출력:** ``` ## 씬 변환 완료 **파일**: `content/rovel/협회 소속 양호선생님/chapters/008.md` **챕터 ID**: chapter-008 ### 생성된 데이터 | 항목 | 개수 | |------|------| | 씬 | 12개 | | 라인 | 156개 | | 시스템 카드 | 3개 | | 플레이 가능 씬 | 2개 | ### 등장 캐릭터 | 캐릭터 | ID | 대사 수 | |--------|-----|---------| | 루나 | char-luna-001 | 24 | | 한시우 | (주인공) | 18 | ### 생성된 SQL **파일**: `content/rovel/협회 소속 양호선생님/chapters/008-seed.sql` ### 다음 단계 1. SQL 확인: cat content/rovel/협회 소속 양호선생님/chapters/008-seed.sql 2. 로컬 D1 적용: npx wrangler d1 execute rovel-db --local --file=content/rovel/협회 소속 양호선생님/chapters/008-seed.sql 3. 원격 D1 적용: npx wrangler d1 execute rovel-db --remote --file=content/rovel/협회 소속 양호선생님/chapters/008-seed.sql ``` ### 예시 2: 범위 변환 ``` /rovel.seed 협회 소속 양호선생님 1-8화 ``` **출력:** ``` ## 씬 변환 완료 (8개 챕터) ### 변환 결과 | 화수 | 씬 | 라인 | 시스템 카드 | 플레이 가능 | |------|-----|------|-------------|-------------| | 1화 | 12 | 89 | 2 | 1 | | 2화 | 10 | 102 | 3 | 2 | | ... | | | | | | 8화 | 14 | 156 | 3 | 2 | | **합계** | **92** | **1,024** | **18** | **12** | ### 생성된 SQL **파일**: `content/rovel/협회 소속 양호선생님/chapters-seed.sql` ### 다음 단계 로컬 D1 적용: npx wrangler d1 execute rovel-db --local --file=content/rovel/협회 소속 양호선생님/chapters-seed.sql 원격 D1 적용: npx wrangler d1 execute rovel-db --remote --file=content/rovel/협회 소속 양호선생님/chapters-seed.sql ``` --- ## 캐릭터 ID 매핑 (협회 소속 양호선생님) ```yaml 주인공: 한시우: char-hansiuw-001 히로인: 자화연: char-jahwayeon-001 루나: char-luna-001 조연: 박준혁: char-parkjunhyuk-001 김 과장: char-kimgwajang-001 김수진: char-kimsujin-001 ``` ### 새 캐릭터 등장 시 인물목록.md에 없는 새 캐릭터가 등장하면: 1. 경고 메시지 출력 2. 임시 ID 생성 (`char-{이름영문}-temp`) 3. 인물목록.md 업데이트 권장 --- ## Reference Files | 유형 | 경로 | 용도 | |------|------|------| | 변환 가이드 | `rules/chapter-to-scene.md` | 마크다운 문법 참조 | | 인물목록 | `content/rovel/{작품명}/인물목록.md` | 캐릭터 ID 매핑 | | 기존 시드 | `scripts/seed-d1.sql` | SQL 형식 참조 | | DB 스키마 | `src/server/db/schema.ts` | 테이블 구조 참조 | --- ## Important Notes ### 변환 시 주의사항 ```yaml 1. 캐릭터 ID 매핑: - 인물목록.md 먼저 확인 - 없는 캐릭터는 경고 후 임시 ID 2. 시스템 카드: - [제목] 다음 줄들이 필드 - 빈 줄이나 다른 타입 만나면 종료 - JSON 형식으로 content 저장 3. 화자 추론: - 이전 컨텍스트 우선 - 말투 패턴으로 보조 - 불확실하면 NULL 4. SQL 이스케이프: - 작은따옴표는 ''로 - NULL은 문자열 아닌 키워드 ``` ### 스키마 호환성 ```yaml 현재 스키마: - scenes.is_playable: 플레이 가능 여부 - scenes.character_ids: JSON 배열 - system_cards: lines.id로 연결 체크 필요: - works 테이블에 작품 존재 여부 - characters 테이블에 캐릭터 존재 여부 ``` ### 작품별 설정 새 작품 추가 시: 1. 작품 ID 결정 (`work-{코드}-001`) 2. 캐릭터 ID 매핑 정의 3. 인물목록.md에 캐릭터 정보 확인