fix(study): split migration 164 into 10 single-statement files (asyncpg)
asyncpg prepared statement 는 single-command 만 허용. 원래 한 파일이던 study_sessions 스키마(CREATE TABLE x2 + CREATE INDEX x8)를 143~146 분할 패턴 따라 10개로 분리. 164: CREATE TABLE study_sessions 165~169: study_sessions 인덱스 5개 (partial) 170: CREATE TABLE study_session_assets 171~173: study_session_assets 인덱스 3개 문제: cannot insert multiple commands into a prepared statement 원인: _run_migrations 가 conn.exec_driver_sql 로 단일 prepared statement 실행
This commit is contained in:
@@ -1,137 +1,53 @@
|
|||||||
-- 164_study_sessions.sql
|
-- 164_study_sessions.sql
|
||||||
-- iPad 손글씨 학습 세션 + 모바일 암기노트/퀴즈 — Phase 1 MVP
|
-- iPad 손글씨 학습 세션 + 모바일 암기노트/퀴즈 — Phase 1 MVP (1/10)
|
||||||
-- plan: ~/.claude/plans/scalable-chasing-stonebraker.md
|
-- plan: ~/.claude/plans/scalable-chasing-stonebraker.md
|
||||||
--
|
--
|
||||||
-- 목적: 자격증(산업안전기사 등) + 어학(일본어/한자 우선) 두 도메인을 모두 받는 일반 학습 세션.
|
-- asyncpg prepared statement 는 single-command 만 허용.
|
||||||
-- iPad write 모드(필사) / 모바일 review 모드(암기노트) / quiz 모드(SRS) 가 같은 데이터를 공유.
|
-- 원래 한 파일이던 학습 세션 스키마를 10개로 분리:
|
||||||
|
-- 164: CREATE TABLE study_sessions
|
||||||
|
-- 165~169: study_sessions 인덱스 5개
|
||||||
|
-- 170: CREATE TABLE study_session_assets
|
||||||
|
-- 171~173: study_session_assets 인덱스 3개
|
||||||
|
--
|
||||||
|
-- 자격증(certification) + 어학(language) 두 도메인을 모두 받는 일반 학습 세션.
|
||||||
|
-- iPad write / 모바일 review / 모바일 quiz 가 같은 데이터를 공유.
|
||||||
--
|
--
|
||||||
-- 핵심 원칙:
|
-- 핵심 원칙:
|
||||||
-- - study_sessions: 학습 메타 + stroke JSON 원본 + 도메인별 자유 metadata
|
-- - study_type 으로 도메인 분기. metadata jsonb 가 도메인별 자유 메타.
|
||||||
-- - study_session_assets: documents 의 스캔/필기PNG/오디오/영상/자막을 연결 (단일 *_id 컬럼 금지)
|
-- - 단일 *_document_id 컬럼 금지. 모든 미디어 연결은 study_session_assets.
|
||||||
-- - documents 원본은 절대 삭제하지 않음 (assets 만 cascade)
|
-- - documents 본체는 절대 삭제하지 않음 (assets 만 cascade).
|
||||||
-- - Phase 1 미사용 필드 (review_state / quiz / ocr / ai_summary / prompt) 는 NULL 허용, 자동 로직 X
|
-- - Phase 1 미사용 필드 (review_state / quiz / ocr / ai_summary / prompt) 는 NULL 허용,
|
||||||
--
|
-- 자동 로직은 Phase 2~4 별도 PR 에서 활성.
|
||||||
-- 인덱스 전략:
|
|
||||||
-- - 자격증/어학 카드 그룹: partial index (study_type 별)
|
|
||||||
-- - SRS 복습 큐: review_state IS NOT NULL 만
|
|
||||||
-- - assets join: study_session_id, document_id, asset_type 별
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS study_sessions (
|
CREATE TABLE IF NOT EXISTS study_sessions (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
-- 도메인 분기
|
|
||||||
study_type VARCHAR(30) NOT NULL DEFAULT 'certification',
|
study_type VARCHAR(30) NOT NULL DEFAULT 'certification',
|
||||||
-- 'certification' | 'language' (Phase 2+ 'general' 등 확장 여지)
|
certification VARCHAR(120),
|
||||||
|
language_code VARCHAR(20),
|
||||||
-- 자격증 메타
|
learning_level VARCHAR(80),
|
||||||
certification VARCHAR(120), -- 예: "산업안전기사" (study_type='certification' 시)
|
subject VARCHAR(120),
|
||||||
-- 어학 메타
|
topic VARCHAR(200),
|
||||||
language_code VARCHAR(20), -- 'ja' | 'en' | 'zh' (study_type='language' 시)
|
source_text TEXT,
|
||||||
-- 공통: 학습 레벨 (도메인 무관)
|
|
||||||
learning_level VARCHAR(80), -- 예: "JLPT N3", "TOEIC 750", "산업안전기사 1차"
|
|
||||||
|
|
||||||
-- 공통: 과목/주제
|
|
||||||
subject VARCHAR(120), -- 예: "산업안전보건법" / "漢字"
|
|
||||||
topic VARCHAR(200), -- 예: "안전보건관리책임자의 직무" / "安全"
|
|
||||||
|
|
||||||
-- 원문 텍스트 snapshot (assets 의 source_scan 과 별개로 발췌 텍스트만 보존)
|
|
||||||
source_text TEXT, -- 예: "安全" / 법령 본문 발췌
|
|
||||||
source_page INTEGER,
|
source_page INTEGER,
|
||||||
|
|
||||||
-- 학습 모드
|
|
||||||
mode VARCHAR(30) NOT NULL DEFAULT 'copy',
|
mode VARCHAR(30) NOT NULL DEFAULT 'copy',
|
||||||
-- 공통: 'copy'(필사)/'trace'(트레이싱)/'blank-repeat'(깜지)
|
prompt_question TEXT,
|
||||||
-- 어학: 'dictation'(받아쓰기)/'shadowing'(쉐도잉)
|
expected_answer TEXT,
|
||||||
-- Phase 2+: 'quiz'/'flashcard'
|
|
||||||
prompt_question TEXT, -- Phase 2: AI 역질문
|
|
||||||
expected_answer TEXT, -- Phase 2: 기대 정답
|
|
||||||
|
|
||||||
-- 도메인별 자유 메타
|
|
||||||
-- 어학 예: {"reading":"あんぜん","meaning":"안전","romaji":"anzen","furigana":"...",
|
|
||||||
-- "example_sentence":"安全第一です。","grammar_point":"...","unit_type":"kanji"}
|
|
||||||
-- unit_type: 'kanji' | 'vocabulary' | 'sentence' | 'listening' | 'shadowing'
|
|
||||||
-- 자격증 예: {"law_article":"산업안전보건법 제15조","formula":"...","keywords":[...]}
|
|
||||||
metadata JSONB,
|
metadata JSONB,
|
||||||
|
|
||||||
-- 횟수 카운트 (보조)
|
|
||||||
target_count INTEGER,
|
target_count INTEGER,
|
||||||
repetition_count INTEGER NOT NULL DEFAULT 0,
|
repetition_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
strokes_json JSONB,
|
||||||
-- 필기 데이터 (원본) — Phase 1 핵심
|
|
||||||
strokes_json JSONB, -- perfect-freehand input points + style
|
|
||||||
canvas_width INTEGER,
|
canvas_width INTEGER,
|
||||||
canvas_height INTEGER,
|
canvas_height INTEGER,
|
||||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
-- 필기 파생 텍스트 — Phase 2 채움 (Phase 1 NULL)
|
|
||||||
ocr_text TEXT,
|
ocr_text TEXT,
|
||||||
user_corrected_text TEXT,
|
user_corrected_text TEXT,
|
||||||
ai_summary TEXT, -- 모바일 카드 view 용 (classify worker 동기화)
|
ai_summary TEXT,
|
||||||
|
review_state VARCHAR(20),
|
||||||
-- SRS / 퀴즈 통계 — Phase 4 활성, Phase 1 NULL
|
|
||||||
review_state VARCHAR(20), -- 'new' | 'learning' | 'weak' | 'mastered'
|
|
||||||
next_review_at TIMESTAMPTZ,
|
next_review_at TIMESTAMPTZ,
|
||||||
last_quiz_at TIMESTAMPTZ,
|
last_quiz_at TIMESTAMPTZ,
|
||||||
correct_count INTEGER NOT NULL DEFAULT 0,
|
correct_count INTEGER NOT NULL DEFAULT 0,
|
||||||
incorrect_count INTEGER NOT NULL DEFAULT 0,
|
incorrect_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 도메인+사용자별 최근 세션 조회 (자격증/어학 공통 목록)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_study_sessions_type_user_created
|
|
||||||
ON study_sessions (user_id, study_type, created_at DESC);
|
|
||||||
|
|
||||||
-- 자격증 카드 그룹 (Phase 3)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_study_sessions_cert
|
|
||||||
ON study_sessions (user_id, certification, subject, topic)
|
|
||||||
WHERE study_type = 'certification';
|
|
||||||
|
|
||||||
-- 어학 카드 그룹 (Phase 3)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_study_sessions_lang
|
|
||||||
ON study_sessions (user_id, language_code, learning_level, subject, topic)
|
|
||||||
WHERE study_type = 'language';
|
|
||||||
|
|
||||||
-- SRS 복습 큐 (Phase 4) — review_state 가 있는 행만
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_study_sessions_review
|
|
||||||
ON study_sessions (user_id, review_state, next_review_at)
|
|
||||||
WHERE review_state IS NOT NULL;
|
|
||||||
|
|
||||||
-- 퀴즈 통계 (Phase 4) — last_quiz_at 가 있는 행만
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_study_sessions_quiz_stats
|
|
||||||
ON study_sessions (user_id, study_type, last_quiz_at)
|
|
||||||
WHERE last_quiz_at IS NOT NULL;
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS study_session_assets (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
study_session_id BIGINT NOT NULL REFERENCES study_sessions(id) ON DELETE CASCADE,
|
|
||||||
document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
||||||
|
|
||||||
-- asset_type: 'source_scan' | 'handwriting_png' | 'audio' | 'video' | 'transcript' | 'reference'
|
|
||||||
asset_type VARCHAR(30) NOT NULL,
|
|
||||||
-- role: 'prompt' | 'answer' | 'pronunciation' | 'lecture' | 'listening_source'
|
|
||||||
-- | 'shadowing_source' | 'reference'
|
|
||||||
role VARCHAR(40),
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
|
|
||||||
-- 같은 (세션, 문서, asset_type, role) 조합 중복 금지 → POST /assets 의 409 근거
|
|
||||||
-- role 이 NULL 인 경우 NULL 끼리는 unique 비교 시 다른 값으로 취급 (Postgres 기본). 의도된 동작.
|
|
||||||
UNIQUE (study_session_id, document_id, asset_type, role)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 세션별 assets 정렬 조회
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_assets_session
|
|
||||||
ON study_session_assets (study_session_id, sort_order);
|
|
||||||
|
|
||||||
-- 문서가 어느 세션에 연결됐는지 역참조 (filter ?document_id=...)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_assets_document
|
|
||||||
ON study_session_assets (document_id);
|
|
||||||
|
|
||||||
-- 특정 asset_type 보유 세션 조회 (filter ?asset_type=audio)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_assets_type
|
|
||||||
ON study_session_assets (study_session_id, asset_type);
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 165_study_sessions_idx_type_user.sql (2/10)
|
||||||
|
-- 도메인+사용자별 최근 세션 목록 조회 (자격증/어학 공통)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_study_sessions_type_user_created
|
||||||
|
ON study_sessions (user_id, study_type, created_at DESC);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 166_study_sessions_idx_cert.sql (3/10)
|
||||||
|
-- 자격증 카드 그룹 (Phase 3 모바일 암기노트)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_study_sessions_cert
|
||||||
|
ON study_sessions (user_id, certification, subject, topic)
|
||||||
|
WHERE study_type = 'certification';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 167_study_sessions_idx_lang.sql (4/10)
|
||||||
|
-- 어학 카드 그룹 (Phase 3 모바일 암기노트)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_study_sessions_lang
|
||||||
|
ON study_sessions (user_id, language_code, learning_level, subject, topic)
|
||||||
|
WHERE study_type = 'language';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 168_study_sessions_idx_review.sql (5/10)
|
||||||
|
-- SRS 복습 큐 (Phase 4) — review_state 가 있는 행만
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_study_sessions_review
|
||||||
|
ON study_sessions (user_id, review_state, next_review_at)
|
||||||
|
WHERE review_state IS NOT NULL;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 169_study_sessions_idx_quiz.sql (6/10)
|
||||||
|
-- 퀴즈 통계 (Phase 4) — last_quiz_at 가 있는 행만
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_study_sessions_quiz_stats
|
||||||
|
ON study_sessions (user_id, study_type, last_quiz_at)
|
||||||
|
WHERE last_quiz_at IS NOT NULL;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- 170_study_session_assets.sql (7/10)
|
||||||
|
-- documents 의 스캔 교재 / 필기 PNG / 오디오 / 영상 / 자막을 study_sessions 에 연결하는 테이블.
|
||||||
|
--
|
||||||
|
-- 단일 *_document_id 컬럼 금지 — 한 세션에 여러 오디오/영상/스캔/필기 PNG 가능.
|
||||||
|
-- documents 원본은 assets 와 별도 자원 — 본 테이블 cascade 는 study_sessions / documents 둘 중
|
||||||
|
-- 어느 쪽이 사라지면 연결 행 정리 (orphan 방지).
|
||||||
|
--
|
||||||
|
-- UNIQUE (study_session_id, document_id, asset_type, role): POST /assets 의 409 근거.
|
||||||
|
-- role 이 NULL 인 경우 NULL 끼리는 Postgres 기본대로 다른 값으로 취급 (의도된 동작).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS study_session_assets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
study_session_id BIGINT NOT NULL REFERENCES study_sessions(id) ON DELETE CASCADE,
|
||||||
|
document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||||
|
asset_type VARCHAR(30) NOT NULL,
|
||||||
|
role VARCHAR(40),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (study_session_id, document_id, asset_type, role)
|
||||||
|
);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 171_study_session_assets_idx_session.sql (8/10)
|
||||||
|
-- 세션별 assets 정렬 조회 (UI rendering)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_assets_session
|
||||||
|
ON study_session_assets (study_session_id, sort_order);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 172_study_session_assets_idx_document.sql (9/10)
|
||||||
|
-- 문서가 어느 세션에 연결됐는지 역참조 (filter ?document_id=...)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_assets_document
|
||||||
|
ON study_session_assets (document_id);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 173_study_session_assets_idx_type.sql (10/10)
|
||||||
|
-- 특정 asset_type 보유 세션 조회 (filter ?asset_type=audio)
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_assets_type
|
||||||
|
ON study_session_assets (study_session_id, asset_type);
|
||||||
Reference in New Issue
Block a user