# 02 — Catalog 테이블 8 개 Drift 정의 (#204) > 부모 설계서: [README.md](./README.md) > SoT: `schema/protocol.schema.json`, `break_protocol.schema.json`, `common_frame.schema.json`, `methodology.schema.json`, `frame_pattern.schema.json`, `reward_menu_item.schema.json`, `reference.schema.json`, `diet_pattern.schema.json`. ## 공통 규약 - 모든 카탈로그 테이블 PK = `TEXT id` (ULID 또는 의미 있는 식별자). - 모든 enum 값은 `TextColumn().named('xxx').withCheckConstraint("xxx IN ('...', ...)")` 로 CHECK 박는다. - 다차원 배열·중첩 객체 (procedure[], phases[], frame_examples[], etc.) 는 **JSON TEXT** 단일 컬럼에 저장. 카탈로그는 read-only 라서 JSON 쿼리 불필요 (단순 read → freezed 모델로 deserialize). - `reference_ids[]` 도 JSON TEXT. M:N 조인 테이블은 본 Phase 에서 만들지 않음 (실용 카운트 작음, Phase 2 에서 join 필요 시 추가). - `additionalProperties: false` 인 schema 의 모든 명시 필드는 컬럼화. 단 nested object 는 JSON. ## 1. `protocols` (← `protocol.schema.json`) ```dart class Protocols extends Table { TextColumn get id => text()(); TextColumn get category => text().withCheckConstraint( "category IN ('health','meditation','motivation','habit','learning','diet')")(); TextColumn get title => text()(); TextColumn get titleEn => text().nullable()(); TextColumn get what => text()(); TextColumn get whenText => text().named('when')(); TextColumn get dose => text()(); TextColumn get why => text()(); TextColumn get howJson => text()(); // List JSON TextColumn get check => text()(); TextColumn get caution => text().nullable()(); TextColumn get defaultAnchorJson => text().nullable()(); // {when,after_what,where} TextColumn get minDoseForStart => text().nullable()(); TextColumn get referenceIdsJson => text().nullable()(); // List JSON TextColumn get evidenceStrength => text().nullable().withCheckConstraint( "evidence_strength IS NULL OR evidence_strength IN " "('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')")(); TextColumn get sourceDoc => text().nullable().withCheckConstraint( "source_doc IS NULL OR source_doc IN " "('huberman-protocols.md','diet-protocols.md')")(); @override Set get primaryKey => {id}; } ``` 인덱스: - `IDX_protocols_category(category)` — UI 의 카테고리 탭 필터. ## 2. `break_protocols` (← `break_protocol.schema.json`) ```dart class BreakProtocols extends Table { TextColumn get id => text()(); TextColumn get category => text().withCheckConstraint( "category IN ('alcohol','nicotine','porn_masturbation','social_media'," "'sugar','caffeine','cannabis','behavioral')")(); TextColumn get title => text()(); TextColumn get hubermanSummary => text()(); TextColumn get frameExamplesJson => text().nullable()(); // [{level,text}] TextColumn get phasesJson => text()(); // [{week,goal,...}] TextColumn get defaultCommonFramesJson => text()(); // ['dopamine_reset',...] TextColumn get toolsJson => text().nullable()(); TextColumn get medicalWarning => text().nullable()(); TextColumn get referenceIdsJson => text().nullable()(); @override Set get primaryKey => {id}; } ``` 인덱스: `IDX_break_protocols_category(category)` UNIQUE — break protocol 은 카테고리당 1 개. ## 3. `common_frames` (← `common_frame.schema.json`) ```dart class CommonFrames extends Table { TextColumn get id => text().withCheckConstraint( "id IN ('dopamine_reset','urge_surf','environment_design'," "'relapse_recovery','recovery_stack')")(); TextColumn get title => text()(); TextColumn get what => text()(); TextColumn get why => text()(); TextColumn get dose => text().nullable()(); TextColumn get howJson => text().nullable()(); TextColumn get check => text()(); TextColumn get applicableBreakCategoriesJson => text().nullable()(); TextColumn get referenceIdsJson => text().nullable()(); @override Set get primaryKey => {id}; } ``` 행 수 시드 = 5 (CommonFrameId enum 5 값). ## 4. `methodologies` (← `methodology.schema.json`) ```dart class Methodologies extends Table { TextColumn get id => text()(); TextColumn get name => text()(); TextColumn get originator => text()(); TextColumn get oneLineDefinition => text()(); TextColumn get coreUnit => text()(); TextColumn get procedureJson => text().nullable()(); TextColumn get toolsJson => text().nullable()(); TextColumn get strengthsJson => text().nullable()(); TextColumn get weaknessesJson => text().nullable()(); TextColumn get goodFor => text().nullable()(); IntColumn get hubermanFitScore => integer().withCheckConstraint( "huberman_fit_score BETWEEN 1 AND 5")(); BoolColumn get isCoreEngine => boolean().withDefault(const Constant(false))(); TextColumn get referenceIdsJson => text().nullable()(); @override Set get primaryKey => {id}; } ``` 인덱스: `IDX_methodologies_core(is_core_engine)` partial WHERE true — 핵심 엔진 3 개 (atomic_habits, tiny_habits, implementation_intentions) 빠른 조회. ## 5. `frame_patterns` (← `frame_pattern.schema.json`) ```dart class FramePatterns extends Table { TextColumn get id => text()(); TextColumn get domain => text().nullable().withCheckConstraint( "domain IS NULL OR domain IN ('food','drink','smoking','screen','porn'," "'sleep','exercise','general')")(); TextColumn get avoidanceKeyword => text()(); TextColumn get l0Example => text()(); TextColumn get l1SimpleReplace => text().nullable()(); TextColumn get l2Suggestion => text()(); TextColumn get l3Identity => text().nullable()(); @override Set get primaryKey => {id}; } ``` 인덱스: `IDX_frame_patterns_keyword(avoidance_keyword)` — `detectAvoidanceKeywords` 가 사전 검색. ## 6. `reward_menu_items` (← `reward_menu_item.schema.json`) ```dart class RewardMenuItems extends Table { TextColumn get id => text()(); TextColumn get tierRecommended => text().withCheckConstraint( "tier_recommended IN ('T0','T1','T2','T3','T4')")(); TextColumn get title => text()(); TextColumn get description => text().nullable()(); IntColumn get estimatedCostKrwMin => integer().nullable()(); IntColumn get estimatedCostKrwMax => integer().nullable()(); BoolColumn get isEffortTied => boolean().nullable()(); TextColumn get tagsJson => text().nullable()(); TextColumn get avoidForBreakHabitsJson => text().nullable()(); @override Set get primaryKey => {id}; } ``` 인덱스: `IDX_reward_menu_tier(tier_recommended)`. ## 7. `references` (← `reference.schema.json`) ```dart class References extends Table { TextColumn get id => text()(); TextColumn get kind => text().withCheckConstraint( "kind IN ('paper','podcast_episode','book','url','korean_explainer')")(); TextColumn get title => text()(); TextColumn get authorsJson => text().nullable()(); IntColumn get year => integer().nullable().withCheckConstraint( "year IS NULL OR (year BETWEEN 1900 AND 2100)")(); TextColumn get journal => text().nullable()(); TextColumn get doi => text().nullable()(); TextColumn get url => text().nullable()(); IntColumn get episodeNumber => integer().nullable()(); TextColumn get publisher => text().nullable()(); TextColumn get evidenceStrength => text().nullable().withCheckConstraint( "evidence_strength IS NULL OR evidence_strength IN " "('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')")(); BoolColumn get verified => boolean().nullable()(); TextColumn get note => text().nullable()(); @override Set get primaryKey => {id}; } ``` 인덱스: `IDX_references_kind(kind)`, `IDX_references_doi(doi)` partial WHERE doi IS NOT NULL. ## 8. `diet_patterns` (← `diet_pattern.schema.json`) > OQ-3 결정 (2026-06-11): `protocol(category='diet')` 와 분리된 별도 카탈로그. > 의견 분열 영역의 합리적 선택지 (지중해/케토/TRE/식물성/한식) — 단일 정답 없음, 사용자 체질·문화에 맞춰 *조합* 대상. ```dart class DietPatterns extends Table { TextColumn get id => text()(); TextColumn get name => text()(); TextColumn get core => text()(); TextColumn get strengthsJson => text().nullable()(); // List JSON TextColumn get weaknessesJson => text().nullable()(); // List JSON TextColumn get evidenceStrength => text().withCheckConstraint( "evidence_strength IN ('strong','moderate','mixed','weak')")(); TextColumn get koreanContextFit => text().nullable().withCheckConstraint( "korean_context_fit IS NULL OR korean_context_fit IN ('high','medium','low')")(); TextColumn get starterLeversJson => text().nullable()(); // List JSON, ≤3 권장 TextColumn get medicalWarning => text().nullable()(); TextColumn get linkedProtocolIdsJson => text().nullable()(); // List JSON, protocol(category='diet') id 참조 TextColumn get referenceIdsJson => text().nullable()(); // List JSON @override Set get primaryKey => {id}; } ``` 인덱스: - `IDX_diet_patterns_evidence(evidence_strength)` — UI 의 "근거 강한 순" 정렬 / 필터. - `IDX_diet_patterns_kfit(korean_context_fit)` partial WHERE korean_context_fit IS NOT NULL — 한국 식문화 친화도 필터. > `linked_protocol_ids` 는 JSON TEXT 로 유지. M:N 조인 테이블 미생성 (행 수 작음 — diet_pattern = 5, linked protocol ≤ 6). Phase 2 에서 조인 쿼리 빈도 ↑ 시 정규화 검토. ## 시드 카운트 추산 (05-seed-data.md 와 sync) | 테이블 | 추산 행 수 | 비고 | |--------|-----------|------| | `references` | ~50 | huberman §8 + methodologies §출처 + breaking §5 + diet §7 합계 | | `protocols` | ~34 | huberman 28 (§1.9 stub 제외) + diet 1.1~1.6 (6) | | `break_protocols` | 8 | 알코올/니코틴/포르노/SNS/설탕/카페인/대마/행동 | | `common_frames` | 5 | dopamine_reset/urge_surf/environment_design/relapse_recovery/recovery_stack | | `methodologies` | 21 | habit-todo-methodologies.md §1~§21 | | `frame_patterns` | ~30 | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" | | `reward_menu_items` | ~30 | habit-todo-methodologies.md "권장 리워드 메뉴 30선" | | `diet_patterns` | 5 | nutrition/diet-protocols.md §2 (mediterranean / low_carb_keto / tre_if / plant_based / k_diet) | > OQ-3 결정 (2026-06-11): diet §2 의 식이 패턴 5 개는 별도 `diet_patterns` 카탈로그로 분리 (위 §8). `protocols(category='diet')` 는 diet §1 의 *원칙* 6 항목만 담는다 (두 카탈로그가 `linked_protocol_ids` 로 약한 연결). ## 카탈로그는 read-only - 모든 카탈로그 테이블에 대한 INSERT 는 `SeedImporter` 와 마이그레이션만 수행. - App 코드 (features/) 에서는 SELECT 만. - 사용자 커스텀 protocol 은 v2 의 `user_protocol` 별도 테이블 (out of scope).