Phase 1 설계서 작성 완료. docs/design/204-flutter-bootstrap/ 13 개 파일: - README.md (12 섹션 모두 채움, 함수 19 개 명세, AC 16 항) - 01-project-structure.md (feature-first + layer-first 하이브리드) - 02-drift-schema-catalog.md (Catalog 7 테이블 Dart 정의) - 03-drift-schema-user.md (User 11 테이블 + R1~R10 강제 매트릭스) - 04-migrations.md (schemaVersion v1 + 인덱스 17 개) - 05-seed-data.md (assets/seed/*.json + first-run import) - 06-ux-contracts.md (체크인 R8 ≤ 60 초 흐름) - fn-recommend-variant / fn-compute-streak / fn-weekly-minimum-ratio - fn-validate-frame-level / fn-active-habit-quota / fn-seed-importer 핵심 결정: dose_variants 는 별도 habit_dose_variant 테이블로 정규화 (FK 무결성 + recommendVariant SQL 단순성). ADR-0002 승격 권장. Refs #204 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.9 KiB
02 — Catalog 테이블 7 개 Drift 정의 (#204)
부모 설계서: 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.
공통 규약
- 모든 카탈로그 테이블 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)
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<String> 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<String> 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<Column> get primaryKey => {id};
}
인덱스:
IDX_protocols_category(category)— UI 의 카테고리 탭 필터.
2. break_protocols (← break_protocol.schema.json)
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<Column> get primaryKey => {id};
}
인덱스: IDX_break_protocols_category(category) UNIQUE — break protocol 은 카테고리당 1 개.
3. common_frames (← common_frame.schema.json)
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<Column> get primaryKey => {id};
}
행 수 시드 = 5 (CommonFrameId enum 5 값).
4. methodologies (← methodology.schema.json)
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<Column> 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)
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<Column> get primaryKey => {id};
}
인덱스: IDX_frame_patterns_keyword(avoidance_keyword) — detectAvoidanceKeywords 가 사전 검색.
6. reward_menu_items (← reward_menu_item.schema.json)
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<Column> get primaryKey => {id};
}
인덱스: IDX_reward_menu_tier(tier_recommended).
7. references (← reference.schema.json)
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<Column> get primaryKey => {id};
}
인덱스: IDX_references_kind(kind), IDX_references_doi(doi) partial WHERE doi IS NOT NULL.
시드 카운트 추산 (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 §2 (식이 패턴 5 개) 는 OQ-3 에서 결정 후 별도 분류 가능. 본 설계서는
protocol(category='diet')로 동일 테이블에 포함 가정.
카탈로그는 read-only
- 모든 카탈로그 테이블에 대한 INSERT 는
SeedImporter와 마이그레이션만 수행. - App 코드 (features/) 에서는 SELECT 만.
- 사용자 커스텀 protocol 은 v2 의
user_protocol별도 테이블 (out of scope).