[Architect] Refs #204 — apply OQ decisions: diet_pattern (19th), ADR-0002 normalize dose_variants

- OQ-1: dose_variants 정규화 결정을 ADR-0002 로 승격 (ADR-0001 = 왜, ADR-0002 = 어떻게).
- OQ-3: nutrition diet 패턴 5개를 별도 diet_pattern 카탈로그(19번째 SoT)로 분리.
  · 02-catalog §8 신규, 인덱스 IDX_diet_patterns_evidence / IDX_diet_patterns_kfit.
  · 05-seed: diet_patterns.json (5행) 추가, 로딩 순서 끝에 배치.
  · 04-migrations: v1 테이블 합계 = Catalog 8 + User 11 + 부속 1 + meta_kv = 21.
- README §2/§3/§6/§11 갱신: 18→19 SoT, AC-2 에 diet_pattern=5 검증 추가.
- README §12 OQ → Resolved Open Questions 표 (OQ-1~OQ-8 결정 결과).
- habit_dose_variant → habit_dose_variants 표기 통일.
- fn-weekly-minimum-ratio, 03-drift-schema-user 의 ADR-0002 cross-link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:13:04 +09:00
parent b8e563176b
commit 29befe4d97
11 changed files with 285 additions and 48 deletions

View File

@@ -21,6 +21,7 @@
| `frame_pattern` | 동 문서 §"언어 프레이밍" | L0→L1/L2/L3 변환 예시 |
| `reward_menu_item` | 동 문서 §"리워드 시스템" | 추천 보상 메뉴 |
| `reference` | 모든 SoT의 출처 필드 | DOI/URL/책 인용 |
| `diet_pattern` | `nutrition/diet-protocols.md` §2 | 식이 패턴 5개 (지중해/저탄수/IF/Plant-Based/한식) — 의견 분열 영역, 사용자 선택 메뉴 |
### User Data (사용자가 생성·변경)
| 엔티티 | 설명 | 핵심 제약 |

View File

@@ -2,6 +2,7 @@
> **상태**: Accepted
> **날짜**: 2026-06-11 · **결정자**: 사용자 (joungmin) + AI 합의 · **관련 이슈**: #204
> **관련 ADR**: [ADR-0002 — dose-variants 정규화 (어떻게 저장)](./0002-dose-variants-normalized.md)
## 맥락 (Context)

View File

@@ -0,0 +1,111 @@
# ADR-0002: Dose Variants 정규화 — 별도 `habit_dose_variants` 테이블
> **상태**: Accepted
> **날짜**: 2026-06-11 · **결정자**: [AI] Architect (사용자 OQ-1 승격 확정) · **관련 이슈**: #204
> **관련 ADR**: [ADR-0001 — dose-variants (왜)](./0001-dose-variants.md)
## 맥락 (Context)
[ADR-0001](./0001-dose-variants.md) 에서 `habit.dose_variants[]`**존재 자체** (왜 필요한가)
는 결정됐다. 그러나 *어떻게 저장할 것인가* — 즉 Drift/SQLite 스키마 상의 물리 형태 —
는 결정되지 않았다. 두 갈래가 있었다:
1. **A. JSON 단일 컬럼**: `habit.dose_variants_json TEXT` 한 컬럼에 배열 통째 직렬화.
`habit.schema.json` 의 nested 구조와 1:1 매핑.
2. **B. 별도 정규화 테이블**: `habit_dose_variants(id, habit_id, label, dose_text,
context_tags_json, condition_tags_json, is_minimum)` 로 row 단위 분해.
본 결정은 Phase 1 (#204) 의 Drift 스키마 구현 직전 시점에서 필요하다.
`tracker_entry.variant_id` 가 *어디에 FK 를 박는가* 와 `recommendVariant`,
`weeklyMinimumRatio` 함수의 쿼리 형태가 본 결정에 종속된다.
설계서 `docs/design/204-flutter-bootstrap/README.md` §11 에서 옵션 B 를 채택한 바 있으며,
사용자 OQ-1 결정으로 이 결정을 ADR 한 장으로 **승격** 한다.
## 결정 (Decision)
`dose_variants[]` 를 **별도 `habit_dose_variants` 테이블** 로 정규화한다.
스키마 (Drift):
```
habit_dose_variants(
id TEXT PRIMARY KEY, -- ULID, dv_<26-char>
habit_id TEXT NOT NULL REFERENCES habits(id) ON DELETE CASCADE,
label TEXT NOT NULL,
dose_text TEXT NOT NULL,
context_tags_json TEXT, -- List<String> JSON
condition_tags_json TEXT, -- List<String> JSON
is_minimum BOOLEAN NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0 -- 사용자 정의 순서 보존
)
```
`tracker_entry.variant_id` 는 `habit_dose_variants.id` 를 참조하는 nullable FK.
삭제 시 ON DELETE SET NULL (이미 기록된 체크인의 컨텍스트 유실 방지).
`habit.schema.json` (SoT) 의 nested `dose_variants[]` 구조는 **그대로 유지**.
Seed/Adapter 계층 (`lib/data/seed/adapters/`) 에서 nested ↔ 정규화 변환 수행.
## 근거 (Rationale)
- **참조 무결성**: `tracker_entry.variant_id` FK 를 SQL 레벨에서 강제 가능.
JSON 옵션은 inner id 가 JSON 내부에 있어 FK 불가, app layer 가 무결성 책임.
- **SQL 단순성 (recommendVariant)**: variant 별 매칭 점수 계산이 표준 SELECT
(`WHERE habit_id = ? AND ...`) + INDEX 로 가능. JSON 옵션은 `json_extract()`
/`json_each()` 필요 → SQLite JSON1 extension 의존.
- **JSON1 extension 의존성 회피**: iOS/Android `sqlite3_flutter_libs` 가
JSON1 을 번들하긴 하지만, 미래 sqlcipher 등으로 교체 시 보장 흔들림.
정규화는 ANSI SQL 만 사용.
- **인덱싱 가능성**: `IDX_habit_dose_variants_habit_min(habit_id, is_minimum)`
로 fallback variant 조회 O(log n). JSON 옵션은 partial index 작성 복잡.
- **부분 업데이트**: variant 1 개 수정 = UPDATE 1 row. JSON 옵션은
read-modify-write 트랜잭션 필요.
- **Drift type-safe codegen**: Drift 의 `Companion` / `TypeConverter` 가 row 단위
로 깨끗하게 적용됨. JSON 컬럼은 freezed 어댑터 + 수동 직렬화.
- **R8 (≤ 60 초 체크인) 보장**: recommendVariant 가 < 50ms 안에 결과 내려면
인덱스 사용 가능한 row 기반 조회가 필수.
## 결과 (Consequences)
- **긍정**:
- `tracker_entry.variant_id` 무결성 SQL 강제.
- recommendVariant / weeklyMinimumRatio 가 표준 SQL → 테스트 용이.
- Drift schema dump 에 모든 컬럼이 가시화 → 회귀 방지 (`tool/schema/v1.json`).
- Phase 2 에서 multi-device sync 도입 시 row 단위 conflict resolution 가능.
- **부정 / 비용**:
- 테이블 1 개 추가 — Phase 1 v1 의 정규화 부속 테이블 1 개 증가
(Catalog 8 + User 11 + 부속 1 + meta_kv = 21 테이블).
- `habit.schema.json` (nested) 과 Drift (flat) 사이 어댑터 코드 필요
(`lib/data/seed/adapters/habit_adapter.dart`).
- `HabitDao.insertWithVariants` 는 트랜잭션 (habit + 다중 variant) 으로 처리.
- **후속 작업**:
- Phase 1 (#204) 구현 단계에서 Developer 가 Drift 테이블 작성 + 어댑터 작성.
- Phase 1 통과 시 `tool/schema/v1.json` dump 에 `habit_dose_variants` 포함 확인.
- `fn-recommend-variant.md` / `fn-weekly-minimum-ratio.md` 가 본 정규화 가정
위에서 작성되어 있는지 cross-check.
## 검토한 대안 (Alternatives Considered)
- **A. JSON 단일 컬럼 (`habit.dose_variants_json TEXT`)**
- 기각 사유:
(1) `tracker_entry.variant_id` 참조 무결성 SQL 강제 불가.
(2) recommendVariant 가 JSON1 extension 의존 → 플랫폼/엔진 교체 위험.
(3) variant 별 인덱스 작성 어렵고, 부분 업데이트 = read-modify-write.
(4) Drift codegen 의 type-safe 이점 상실 (수동 JSON 직렬화).
- **C. Hybrid (habit 에 JSON + 별도 캐시 테이블)**
- 기각 사유: 두 표현의 동기화 책임 발생. 단일 SoT 원칙 위반. 복잡도만 증가.
- **D. EAV (entity-attribute-value)**
- 기각 사유: variant 의 attribute 가 고정 (label, dose_text, tags, is_minimum) 이라
EAV 의 유연성 불필요. 쿼리 복잡도만 증가.
## 추적성
- 본 ADR 은 [ADR-0001 — dose-variants (왜)](./0001-dose-variants.md) 와 한 쌍.
- ADR-0001 = "왜 variants 가 필요한가" (행동심리·R9·R10 도입).
- ADR-0002 = "어떻게 저장하는가" (정규화 vs JSON).
- 설계서: `docs/design/204-flutter-bootstrap/README.md` §11 (대안 표) +
`03-drift-schema-user.md` (실제 Drift 테이블 정의) +
`04-migrations.md` (v1 진입점 + 인덱스).

View File

@@ -1,7 +1,7 @@
# 02 — Catalog 테이블 7 개 Drift 정의 (#204)
# 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`.
> 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`.
## 공통 규약
@@ -176,6 +176,36 @@ class References extends Table {
인덱스: `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<String> JSON
TextColumn get weaknessesJson => text().nullable()(); // List<String> 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<String> JSON, ≤3 권장
TextColumn get medicalWarning => text().nullable()();
TextColumn get linkedProtocolIdsJson => text().nullable()(); // List<String> JSON, protocol(category='diet') id 참조
TextColumn get referenceIdsJson => text().nullable()(); // List<String> JSON
@override Set<Column> 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)
| 테이블 | 추산 행 수 | 비고 |
@@ -187,8 +217,9 @@ class References extends Table {
| `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) |
> diet §2 (식이 패턴 5 개) 는 OQ-3 에서 결정 후 별도 분류 가능. 본 설계서는 `protocol(category='diet')` 로 동일 테이블에 포함 가정.
> OQ-3 결정 (2026-06-11): diet §2 식이 패턴 5 개는 별도 `diet_patterns` 카탈로그로 분리 (위 §8). `protocols(category='diet')` 는 diet §1 의 *원칙* 6 항목만 담는다 (두 카탈로그가 `linked_protocol_ids` 로 약한 연결).
## 카탈로그는 read-only

View File

@@ -1,7 +1,7 @@
# 03 — User-Data 테이블 11 개 Drift 정의 + R 강제 매트릭스 (#204)
> 부모 설계서: [README.md](./README.md)
> SoT: `schema/user.schema.json`, `phase.schema.json`, `habit.schema.json`, `if_then_rule.schema.json`, `tracker_entry.schema.json`, `lapse_log.schema.json`, `urge_log.schema.json`, `reward_declaration.schema.json`, `reward_claim.schema.json`, `reflection.schema.json` + (정규화) `habit_dose_variant`.
> SoT: `schema/user.schema.json`, `phase.schema.json`, `habit.schema.json`, `if_then_rule.schema.json`, `tracker_entry.schema.json`, `lapse_log.schema.json`, `urge_log.schema.json`, `reward_declaration.schema.json`, `reward_claim.schema.json`, `reflection.schema.json` + (정규화 부속, [ADR-0002](../../adr/0002-dose-variants-normalized.md)) `habit_dose_variants`.
## 0. 공통 규약
@@ -90,7 +90,7 @@ class Habits extends Table {
- `IDX_habits_user_status_type(user_id, status, type)` — R1/R2 빠른 카운트.
- `IDX_habits_phase(phase_id)`.
## 4. `habit_dose_variants` (정규화 — ADR 후보)
## 4. `habit_dose_variants` (정규화 부속[ADR-0002](../../adr/0002-dose-variants-normalized.md))
```dart
class HabitDoseVariants extends Table {

View File

@@ -6,9 +6,11 @@
| version | 범위 | 작성자 | 비고 |
|---------|------|--------|------|
| **v1** | 18 테이블 + `habit_dose_variants` + `meta_kv` 초기 생성 | Architect (이 설계서) → Developer (구현) | Phase 1 종료 시점 = production v1 |
| **v1** | 19 SoT 테이블 (Catalog 8 + User 11) + 부속 1 (`habit_dose_variants`) + `meta_kv` = **21 테이블** 초기 생성 | Architect (이 설계서) → Developer (구현) | Phase 1 종료 시점 = production v1 |
| v2+ | TBD (Phase 2+) | 해당 시점 Architect | data-model.md §8 의 후속 엔티티 (habit_stack, notification_rule 등) |
> v1 테이블 합계 명확화: **Catalog 8** (protocols, break_protocols, common_frames, methodologies, frame_patterns, reward_menu_items, references, **diet_patterns**) + **User 11** (users, phases, habits, if_then_rules, tracker_entries, lapse_logs, urge_logs, reward_declarations, reward_claims, reflections + ※ habit row 의 정규화 부속 별도) + **부속 1** (`habit_dose_variants` — ADR-0002) + **meta_kv** = **21 테이블**.
규칙:
- schema 가 바뀌면 **반드시 schemaVersion 증가** + `onUpgrade` 콜백 작성.
- 컬럼 추가만은 가능하나, drop/rename 은 destructive — `m.alterTable(TableMigration(...))` 사용.
@@ -18,10 +20,10 @@
```dart
@DriftDatabase(tables: [
// catalog
// catalog (8)
Protocols, BreakProtocols, CommonFrames, Methodologies,
FramePatterns, RewardMenuItems, References,
// user
FramePatterns, RewardMenuItems, References, DietPatterns,
// user (11) + 부속 (1, ADR-0002)
Users, Phases, Habits, HabitDoseVariants, IfThenRules,
TrackerEntries, LapseLogs, UrgeLogs,
RewardDeclarations, RewardClaims, Reflections,
@@ -68,6 +70,11 @@ class AppDatabase extends _$AppDatabase {
'CREATE INDEX IDX_references_kind ON "references"(kind)'));
await m.createIndex(Index('IDX_references_doi',
'CREATE INDEX IDX_references_doi ON "references"(doi) WHERE doi IS NOT NULL'));
await m.createIndex(Index('IDX_diet_patterns_evidence',
'CREATE INDEX IDX_diet_patterns_evidence ON diet_patterns(evidence_strength)'));
await m.createIndex(Index('IDX_diet_patterns_kfit',
'CREATE INDEX IDX_diet_patterns_kfit ON diet_patterns(korean_context_fit) '
'WHERE korean_context_fit IS NOT NULL'));
// user
await m.createIndex(Index('IDX_phases_user_status',
'CREATE INDEX IDX_phases_user_status ON phases(user_id, status)'));

View File

@@ -22,9 +22,12 @@ app/assets/seed/
├── methodologies.json # Array<Methodology> — 21 항목
├── frame_patterns.json # Array<FramePattern> — ~30 항목
├── reward_menu_items.json # Array<RewardMenuItem> — ~30 항목
├── diet_patterns.json # Array<DietPattern> — 5 항목 (OQ-3 결정)
└── references.json # Array<Reference> — ~50 항목
```
> 총 **8 시드 파일** (Catalog 카탈로그 1:1). user-data 테이블은 시드 없음.
- 각 파일 형식 = `schema/<entity>.schema.json` 을 만족하는 객체 배열.
- 인코딩 = UTF-8 (한국어 텍스트).
- `pubspec.yaml``flutter.assets:``assets/seed/` 등록.
@@ -33,12 +36,13 @@ app/assets/seed/
```
1. references.json ← 다른 시드가 reference_ids 로 참조
2. protocols.json
2. protocols.json ← diet_patterns 가 linked_protocol_ids 로 참조
3. break_protocols.json
4. common_frames.json
5. methodologies.json
6. frame_patterns.json
7. reward_menu_items.json
8. diet_patterns.json ← protocols + references 참조 (마지막)
```
- 각 파일 batch insert 는 단일 트랜잭션.
@@ -85,8 +89,19 @@ Future<void> importIfNeeded() async {
| methodologies.json | habit-todo-methodologies.md §1~§21 | `###` 헤더 단위 | 21 |
| frame_patterns.json | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" | 표 row 단위 | ~30 |
| reward_menu_items.json | habit-todo-methodologies.md "권장 리워드 메뉴 30선" | 표 row 단위 | ~30 |
| diet_patterns.json | nutrition/diet-protocols.md §2 | 5 개 식이 패턴 (`###` 헤더 단위) | 5 |
| references.json | 4 SoT 의 각 출처 섹션 합계 | 인용 1 건 | ~50 |
### diet_patterns.json 시드 5 행 (예시 id)
| id | name | 비고 |
|----|------|------|
| `mediterranean` | 지중해 식단 | PREDIMED RCT — evidence_strength=strong |
| `low_carb_keto` | 저탄수/케토 | 당뇨 약물 복용자 medical_warning |
| `tre_if` | TRE / 간헐적 단식 | huberman §1.5 linked_protocol |
| `plant_based` | 식물성 | k_diet 와 starter_levers 일부 공유 |
| `k_diet` | 한식 기반 | korean_context_fit=high, starter '첫 끼 단백질 +30g' |
## 7. 손 작성 vs 자동 추출
본 Phase 는 **손 작성** (Developer 가 SoT 마크다운을 보고 JSON 작성). 이유:
@@ -118,6 +133,7 @@ test/fixtures/seed/
├── methodologies_small.json # 3 항목 (core engine 3 개)
├── frame_patterns_small.json # 2 항목
├── reward_menu_items_small.json # 5 항목 (T0~T4 각 1)
├── diet_patterns_small.json # 2 항목 (k_diet + mediterranean)
└── references_small.json # 2 항목
```

View File

@@ -1,12 +1,12 @@
# 설계서: Phase 1 — Flutter + Drift Bootstrap + 18 Schema (#204)
# 설계서: Phase 1 — Flutter + Drift Bootstrap + 19 Schema (#204)
> **상태**: Draft
> **상태**: Draft (OQ Resolved 2026-06-11)
> **작성**: [AI] Architect · **최종수정**: 2026-06-11
> **추적성** — Redmine: #204 · 관련 ADR: [ADR-0001 dose-variants](../../adr/0001-dose-variants.md)
> **추적성** — Redmine: #204 · 관련 ADR: [ADR-0001 dose-variants (왜)](../../adr/0001-dose-variants.md), [ADR-0002 dose-variants 정규화 (어떻게)](../../adr/0002-dose-variants-normalized.md)
> · 구현 파일: `app/` (TBD by Developer) · 테스트: `app/test/` (TBD by Developer)
> **하위 문서**:
> - [01-project-structure.md](./01-project-structure.md) — Flutter 레이아웃 + pubspec + codegen
> - [02-drift-schema-catalog.md](./02-drift-schema-catalog.md) — Catalog 테이블 7
> - [02-drift-schema-catalog.md](./02-drift-schema-catalog.md) — Catalog 테이블 8
> - [03-drift-schema-user.md](./03-drift-schema-user.md) — User-data 테이블 11개 + R1~R10 강제 위치 표
> - [04-migrations.md](./04-migrations.md) — Drift schemaVersion 전략
> - [05-seed-data.md](./05-seed-data.md) — 시드 로딩 전략
@@ -24,7 +24,7 @@
> Planner 목표: "Huberman/방법론/끊기/식이 SoT 를 앱에서 쓸 수 있는 로컬-우선 Flutter 앱의 데이터 계층 + 최소 vertical slice 를 만든다."
Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 18 개 엔티티 schema 를 실제 디바이스에서 굴러가는 Drift DB 로 내려놓는 것"** 이다. 그 위에서 vertical slice (habit 1 개 생성 → 1 회 체크인 → 화면 확인) 가 동작하면, 이후의 모든 페르소나 작업 (UI 확장, 시드 보강, ADR 적용) 은 이 데이터 계층 위에서 점진적으로 진행된다. 동시에 **R1~R10 운영 규칙이 schema 가 아닌 어디서 강제되는지** 를 명확히 박아 두어, 향후 어떤 페르소나도 "이 규칙은 어디서 검사하지?" 라는 표류를 만들지 않게 한다.
Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 19 SoT 엔티티 schema 를 실제 디바이스에서 굴러가는 Drift DB 로 내려놓는 것"** 이다. 그 위에서 vertical slice (habit 1 개 생성 → 1 회 체크인 → 화면 확인) 가 동작하면, 이후의 모든 페르소나 작업 (UI 확장, 시드 보강, ADR 적용) 은 이 데이터 계층 위에서 점진적으로 진행된다. 동시에 **R1~R10 운영 규칙이 schema 가 아닌 어디서 강제되는지** 를 명확히 박아 두어, 향후 어떤 페르소나도 "이 규칙은 어디서 검사하지?" 라는 표류를 만들지 않게 한다.
## 2. 범위 (Scope)
@@ -32,8 +32,8 @@ Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 18 개 엔
- Flutter 프로젝트 부트스트랩 (`app/` 서브디렉토리, iOS + Android 빌드 타깃).
- Drift (sqlite) ORM 도입 + 코드 생성 파이프라인 (`build_runner`).
- 18 개 테이블 Dart 정의 (Catalog 7 + User 11) — `schema/*.json` 19 개 SoT 와 1:1 매핑.
- `dose_variants[]` 저장 형태 결정 — **별도 `habit_dose_variant` 테이블로 정규화** (근거는 §11, 상세는 03-drift-schema-user.md).
- **19 개 SoT 테이블 Dart 정의 (Catalog 8 + User 11)**`schema/*.json` 19 개 SoT 와 1:1 매핑 + 정규화 부속 `habit_dose_variants` 1 개.
- `dose_variants[]` 저장 형태 결정 — **별도 `habit_dose_variants` 테이블로 정규화** (ADR-0002 참조, 상세는 03-drift-schema-user.md).
- `tracker_entry.variant_id` + `context_snapshot` 저장.
- R1~R10 운영 규칙별 강제 위치 (schema CHECK · index · trigger · app layer) 매트릭스.
- Drift `schemaVersion = 1` 마이그레이션 진입점 + 향후 변경 패턴.
@@ -58,9 +58,9 @@ Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 18 개 엔
QA 가 다음 항목을 모두 통과 표시할 수 있어야 한다.
- [ ] **AC-1**: `flutter pub get && dart run build_runner build` 가 새 환경에서 에러 없이 통과한다 (Drift codegen 포함).
- [ ] **AC-2**: 앱 실행 직후 첫 부팅 시 18테이블이 모두 생성되고 (`sqlite_master` 조회로 검증), 카탈로그 7 개 테이블에 시드 행이 0 개 이상 들어가 있다 (Huberman protocol ≥ 28, break_protocol = 8, common_frame = 5, methodology = 21, reward_menu_item ≥ 10).
- [ ] **AC-2**: 앱 실행 직후 첫 부팅 시 19SoT 테이블 + 정규화 부속 `habit_dose_variants` + `meta_kv` 모두 생성되고 (`sqlite_master` 조회로 검증), 카탈로그 8 개 테이블에 시드 행이 0 개 이상 들어가 있다 (Huberman protocol ≥ 28, break_protocol = 8, common_frame = 5, methodology = 21, reward_menu_item ≥ 10, **diet_pattern = 5**).
- [ ] **AC-3**: `user_id = 'u_local_default'` user 1 행이 자동 생성된다.
- [ ] **AC-4**: `habit` 생성 폼에서 build 타입 1 개를 만들고 (protocol_id 참조, frame.level = L2, anchor.when 입력), `habit` 테이블 + `habit_dose_variant` 테이블에 행이 들어간다.
- [ ] **AC-4**: `habit` 생성 폼에서 build 타입 1 개를 만들고 (protocol_id 참조, frame.level = L2, anchor.when 입력), `habit` 테이블 + `habit_dose_variants` 테이블에 행이 들어간다.
- [ ] **AC-5**: 만든 habit 에 대해 오늘 날짜로 체크인 (장소 1 탭 + 컨디션 1 탭) 시 `tracker_entry` 1 행이 생성되고 `variant_id` + `context_snapshot.location` + `context_snapshot.condition` 이 모두 채워진다.
- [ ] **AC-6**: 같은 habit 에 대해 같은 날짜로 두 번째 `tracker_entry` 를 insert 하면 unique 위반으로 거부된다 (UNIQUE INDEX `(habit_id, date)`).
- [ ] **AC-7**: build habit 을 3 개 active 상태로 가진 user 가 4 번째 build habit 을 active 로 생성하려 하면 app layer 가 거부하고 사용자 메시지를 반환한다 (R1).
@@ -166,7 +166,7 @@ life-helper/
│ [data/db/daos/HabitDao.insertWithVariants]
│ │
│ ▼ (insert 트랜잭션)
│ habit + habit_dose_variant 동시 commit
│ habit + habit_dose_variants 동시 commit
[features/daily_checkin] ──► location + condition 1 탭씩
@@ -195,7 +195,7 @@ life-helper/
## 6. 데이터 모델
### 18테이블 한눈에
### 19SoT 테이블 + 정규화 부속 1 한눈에
| # | 테이블 | 분류 | SoT JSON | 행 수 (시드) | Drift 정의 위치 |
|---|--------|------|----------|-------------|-----------------|
@@ -206,10 +206,10 @@ life-helper/
| 5 | `frame_pattern` | catalog | frame_pattern.schema.json | ~30 (32 변환표) | 02-catalog |
| 6 | `reward_menu_item` | catalog | reward_menu_item.schema.json | ~30 (메뉴 30선) | 02-catalog |
| 7 | `reference` | catalog | reference.schema.json | ~50 (출처 통합 합계) | 02-catalog |
| 8 | `user` | user | user.schema.json | 1 (자동) | 03-user |
| 9 | `phase` | user | phase.schema.json | 0 | 03-user |
| 10 | `habit` | user | habit.schema.json | 0 | 03-user |
| 11 | `habit_dose_variant` | user (정규화 산출) | habit.schema.json `dose_variants[]` 분해 | 0 | 03-user |
| 8 | `diet_pattern` | catalog | diet_pattern.schema.json | 5 (지중해/케토/TRE/식물성/한식) | 02-catalog |
| 9 | `user` | user | user.schema.json | 1 (자동) | 03-user |
| 10 | `phase` | user | phase.schema.json | 0 | 03-user |
| 11 | `habit` | user | habit.schema.json | 0 | 03-user |
| 12 | `if_then_rule` | user | if_then_rule.schema.json | 0 | 03-user |
| 13 | `tracker_entry` | user | tracker_entry.schema.json | 0 | 03-user |
| 14 | `lapse_log` | user | lapse_log.schema.json | 0 | 03-user |
@@ -217,8 +217,9 @@ life-helper/
| 16 | `reward_declaration` | user | reward_declaration.schema.json | 0 | 03-user |
| 17 | `reward_claim` | user | reward_claim.schema.json | 0 | 03-user |
| 18 | `reflection` | user | reflection.schema.json | 0 | 03-user |
| — | `habit_dose_variants` | 부속 (user, 정규화) | habit.schema.json `dose_variants[]` 분해 | 0 | 03-user (ADR-0002) |
> **총 18 테이블 = 11 user-data + 7 catalog**. `dose_variants[]` 가 `habit_dose_variant` 로 정규화되므로 user-data 수가 schema/*.json 의 10 개에서 11 개로 증가한다. 메타 `meta_kv` (seed 완료 플래그 저장) 는 Drift 내부 관리용 보조 테이블로 18 개에 포함하지 않는다 (04-migrations 참조).
> **총 19 SoT 테이블 = Catalog 8 + User 11**. 추가로 `dose_variants[]` 가 `habit_dose_variants` 로 정규화 (ADR-0002) 되며, 이 테이블은 user-data 의 부속 (habit row 의 normalized child) 으로 SoT 카운트와 별도 집계한다. 메타 `meta_kv` (seed 완료 플래그) 는 Drift 내부 관리용 보조 테이블로 SoT 카운트에 포함하지 않는다. v1 실제 생성 테이블 합계 = **Catalog 8 + User 11 + 부속 1 (habit_dose_variants) + meta_kv = 21 테이블** (04-migrations 참조).
### 경계 검증 규칙 (요약)
@@ -282,7 +283,7 @@ life-helper/
4. UI: dose_variants 입력 (최소 1 개, 강제 X. 본 vertical slice 는 기본 variant 1 개 자동 생성: label='기본', dose_text=protocol.min_dose_for_start, is_minimum=true).
5. `HabitDao.insertWithVariants` 트랜잭션:
- `habit` insert.
- `habit_dose_variant` 다중 insert.
- `habit_dose_variants` 다중 insert.
- `if_then_rule` 다중 insert (있다면).
6. commit → UI 가 habit list 갱신.
@@ -362,11 +363,11 @@ life-helper/
| 옵션 | 장점 | 단점 | 채택 |
|------|------|------|------|
| **A. JSON 단일 컬럼** (`habit.dose_variants_json TEXT`) | 코드 적음, `habit.schema.json` 과 1:1 매핑. ULID 같은 inner id 그대로. | (1) `tracker_entry.variant_id` 참조 무결성 SQL 강제 불가. (2) variant 별 집계 (recommendVariant, weeklyMinimumRatio) 가 JSON1 extension 필요. iOS/Android 빌드 sqlite 의 JSON1 보장 흔들림. (3) 부분 업데이트 (variant 1 개 수정) 가 read-modify-write. | ✗ |
| **B. 별도 `habit_dose_variant` 테이블** (정규화) | (1) FK + INDEX 로 참조 무결성 + 빠른 조회. (2) recommendVariant 가 standard SQL 로 가능. (3) variant CRUD 가 row 단위. (4) Drift type-safe codegen 그대로 활용. | (1) schema/*.json 의 nested 구조와 1:1 안 됨 → 변환 어댑터 필요. (2) 테이블 1 개 추가 (총 18 → 사실상 19). | **✓ 채택** |
| **B. 별도 `habit_dose_variants` 테이블** (정규화, [ADR-0002](../../adr/0002-dose-variants-normalized.md)) | (1) FK + INDEX 로 참조 무결성 + 빠른 조회. (2) recommendVariant 가 standard SQL 로 가능. (3) variant CRUD 가 row 단위. (4) Drift type-safe codegen 그대로 활용. | (1) schema/*.json 의 nested 구조와 1:1 안 됨 → 변환 어댑터 필요. (2) 부속 테이블 1 개 추가. | **✓ 채택 (ADR-0002)** |
**결정 근거**: B 의 장점이 R8 (≤ 60 초) 와 직결되는 recommendVariant 의 쿼리 단순성 + AC-5/AC-11 의 테스트 가능성을 모두 달성한다. JSON 옵션의 "schema 1:1" 장점은 `core/seed/adapters/` 의 가벼운 어댑터로 회수 가능. data-model.md 와 `habit.schema.json` 은 nested 표현을 유지하되, Drift 계층에서 정규화 한다. ADR-0001 의 결정 (R9 무제한 / R10 hint) 은 그대로 보존.
> 단, 테이블 수 count 는 "사용자 외부 schema 18" 그대로 표기한다 (`habit_dose_variant` 는 `habit` 의 정규화 부속). AC-2 의 18 테이블 검증 시 `habit_dose_variant` 도 포함하여 실제 19 테이블이 생성됨을 허용한다.
> 테이블 수 count: **19 SoT 테이블** (Catalog 8 + User 11) 을 외부 표기 기준으로 한다. `habit_dose_variants` 는 `habit` 의 정규화 부속으로 별도 집계 (총 v1 = 21 테이블 — 04-migrations 참조). AC-2 검증 시 `habit_dose_variants` 도 함께 생성됨을 확인한다.
### 그 외 검토 대안
@@ -378,30 +379,36 @@ life-helper/
### 되돌리기 어려운 결정 → ADR 후보
- `habit_dose_variant` 정규화: 본 설계서에 명시했고 schema/*.json 은 변경하지 않으므로 ADR 별도 분리는 옵션. **Phase 1 종료 후 ADR-0002 로 승격 권장** (DB 형태 결정 = 멀티 디바이스 sync 시 갈등 가능).
- Riverpod 채택: 코드 분량 큼. **ADR-0003 후보**.
- `habit_dose_variants` 정규화: **ADR-0002 로 승격 완료** (2026-06-11). 본 설계서는 ADR-0002 결정을 참조.
- Riverpod 채택: 코드 분량 큼. **ADR-0003 후보** (Phase 2 진입 전 검토).
## 12. 미해결 질문 (Open Questions)
## 12. Resolved Open Questions (2026-06-11)
> Developer 단계로 넘기기 전 사용자에게 확인이 필요한 항목.
> Developer 단계 진입 직전 사용자 (joungmin) 결정으로 모두 해결됨. 본 섹션은 결정 결과의 기록.
1. **OQ-1**: `habit_dose_variant` 정규화 결정을 ADR-0002 로 분리할지 (권장) — 본 설계서에 박혀 있지만 ADR 이 명시적이라 추적 유리.
2. **OQ-2**: 시드 JSON 의 출처 — `huberman-protocols.md` 등 마크다운에서 자동 추출 스크립트를 짤 것인가 (`scripts/` 신규), 아니면 손으로 JSON 작성? 본 설계서는 후자 (손 작성) 를 가정. Phase 1 끝에서 Developer 가 손 작성하면 Phase 2 에서 자동화로 전환.
3. **OQ-3**: `protocol.category` 에 'diet' 가 들어가는데 nutrition/diet-protocols.md §2 의 "식이 패턴" (지중해/케토/TRE/식물성/한식) 5 개를 `protocol` 로 저장할 것인가 vs 별도 `diet_pattern` 카탈로그를 신설? 본 설계서는 전자 (단순화). 다른 의견 있으면 v1 안에 반영 필요.
4. **OQ-4**: `frame_pattern.schema.json` 에 ko/en 두 언어 컬럼 없음 — 한국어만으로 충분한지. 본 설계서는 한국어 only 가정.
5. **OQ-5**: 5-Tier 의 "Never miss twice" 정의 — "연속 2 일 blank → 스트릭 0" 으로 본 설계서가 박았는데, 사용자가 "주 단위 2 회 blank" 같은 다른 해석을 원하는지 확인 필요. 본 설계서의 정의는 fn-compute-streak.md §"동작" 에 상세.
6. **OQ-6**: `tracker_entry.variant_id` 가 nullable 인가? — 본 설계서는 nullable (variant 없는 habit 도 허용). 다만 dose_variants 권장이라면 not-null 강제도 가능. 결정 필요.
7. **OQ-7**: vertical slice UI 의 화면 갯수 — habit 생성 폼 + 오늘 카드 + 체크인 시트 + (선택) habit 목록 화면. 본 설계서는 4 개. Designer 단계에서 줄이거나 늘릴지 결정.
8. **OQ-8**: 앱 ID, 패키지명 — `kr.cloud_handson.life_helper` 가정. iOS bundle id 도 동일. 사용자 확정 필요.
| OQ | 질문 | 결정 | 반영 위치 |
|----|------|------|----------|
| **OQ-1** | `habit_dose_variants` 정규화를 ADR 로 분리? | **분리** — [ADR-0002](../../adr/0002-dose-variants-normalized.md) 로 승격. | `docs/adr/0002-dose-variants-normalized.md` 신규, ADR-0001 cross-link 추가 |
| **OQ-2** | 시드 JSON 자동 추출 vs 손 작성? | **손 작성** (Phase 1). Phase 2 에서 `scripts/extract_seed.py` 재검토. | `05-seed-data.md` §7 유지 |
| **OQ-3** | diet 패턴 5 개를 `protocol(category='diet')` 합치기 vs 별도 카탈로그? | **별도 `diet_pattern` 카탈로그 (19 번째 SoT)**. | `schema/diet_pattern.schema.json` 신규, `schema/_index.json` 등재, `02-drift-schema-catalog.md` §8 추가, `05-seed-data.md` 8 번째 시드 파일 추가, `04-migrations.md` v1 테이블 목록 갱신 |
| **OQ-4** | `frame_pattern` 한국어 only? | **한국어 only OK** — i18n 은 v2. | `02-drift-schema-catalog.md` §5 유지 |
| **OQ-5** | "Never miss twice" 정의? | **연속 2 일 blank → tier 강등 + streak 0. 1 일 blank → streak 0 유지하되 tier 유지**. | `fn-compute-streak.md` 본문 + AC-12 (R10 외 streak 동작) |
| **OQ-6** | `tracker_entry.variant_id` nullable? | **nullable** — variant 없는 habit 도 허용. | `03-drift-schema-user.md` tracker_entries 정의 유지 |
| **OQ-7** | vertical slice UI 화면 갯수? | **4 개** — habit 생성 / 목록 / 체크인 / 스트릭. | `06-ux-contracts.md` |
| **OQ-8** | 앱 ID / 패키지명? | **`kr.cloud_handson.life_helper`** (iOS bundle id 동일). | `01-project-structure.md` |
> 모든 OQ 가 해결됐으므로 본 설계서는 Developer 단계 진입 준비 완료 상태. 추가 OQ 발생 시 본 표에 row 추가 + 해결 결과 명시.
---
## 부록: 자가 점검 (Architect 가 작업 종료 시 검증)
- [x] `_TEMPLATE.md` 12 개 섹션 모두 비어있지 않음
- [x] 18 테이블 (catalog 7 + user 11) 전부 §6 표 + 03-drift-schema-user.md 의 Drift 컬럼/타입/제약 정의 완료
- [x] 19 SoT 테이블 (catalog 8 + user 11) + 부속 1 (habit_dose_variants) 전부 §6 표 + 02/03-drift-schema-*.md 의 Drift 컬럼/타입/제약 정의 완료
- [x] R1~R10 강제 위치 매트릭스 = 03-drift-schema-user.md §"R 강제 매트릭스"
- [x] dose_variants 결정 = §11 "B. 별도 `habit_dose_variant` 테이블" + 근거
- [x] dose_variants 결정 = ADR-0001 (왜) + ADR-0002 (어떻게: 정규화)
- [x] diet_pattern = 19 번째 catalog SoT (OQ-3 결정) — schema/diet_pattern.schema.json + 02-catalog §8 + 05-seed §3 + 04-migrations v1 반영
- [x] 복잡 함수 6 개 각각 fn-*.md 존재 (recommend-variant, compute-streak, weekly-minimum-ratio, validate-frame-level, active-habit-quota, seed-importer)
- [x] §7 함수 명세 표에 모든 함수 등재 (단순 13 + 복잡 6)
- [x] AC §3 — QA 판정 가능한 17 항목 (AC-1 ~ AC-16)
- [x] §7 함수 명세 표에 모든 함수 등재 (단순 13 + 복잡 6). diet_pattern 은 read-only 카탈로그라 신규 함수 추가 없음 (`fn-seed-importer` 가 처리)
- [x] AC §3 — QA 판정 가능한 16 항목 (AC-1 ~ AC-16). AC-2 에 diet_pattern = 5 시드 검증 추가
- [x] §12 모든 OQ (OQ-1 ~ OQ-8) Resolved 표로 정리

View File

@@ -18,7 +18,7 @@ Future<double?> weeklyMinimumRatio({
## 2. 책임
해당 주(7 일)의 `tracker_entry``value='done'` 인 항목들에서 `variant_id``habit_dose_variant.is_minimum=true` 비율을 계산한다 (R10).
해당 주(7 일)의 `tracker_entry``value='done'` 인 항목들에서 `variant_id``habit_dose_variants.is_minimum=true` 비율을 계산한다 (R10).
## 3. 입력

View File

@@ -12,7 +12,8 @@
"methodology": "./methodology.schema.json",
"frame_pattern": "./frame_pattern.schema.json",
"reward_menu_item": "./reward_menu_item.schema.json",
"reference": "./reference.schema.json"
"reference": "./reference.schema.json",
"diet_pattern": "./diet_pattern.schema.json"
},
"user_data": {
"user": "./user.schema.json",
@@ -38,7 +39,9 @@
{ "id": "R5", "rule": "tracker_entry.value ∈ {done, blank} 2값만", "enforced_in": "tracker_entry.schema.json (enum)" },
{ "id": "R6", "rule": "phase.duration_weeks = 6 default, 6주 중간 anchor 변경 warning", "enforced_in": "application layer" },
{ "id": "R7", "rule": "if_then_rule.then_action 회피 키워드 감지 시 warning", "enforced_in": "client-side linter" },
{ "id": "R8", "rule": "habit 일일 운영 ≤ 2분 (UX 제약)", "enforced_in": "UI design (not in schema)" }
{ "id": "R8", "rule": "habit 일일 운영 ≤ 2분 (UX 제약)", "enforced_in": "UI design (not in schema)" },
{ "id": "R9", "rule": "habit.dose_variants[] 개수 제한 없음. 체크인 UX는 장소 1탭 + 컨디션 1탭 → 추천 흐름으로 R8 유지", "enforced_in": "application + UI", "source": "docs/adr/0001-dose-variants.md" },
{ "id": "R10", "rule": "reflection.scope='weekly'은 minimum_ratio 표시 (강제 임계값 X — hint 용도)", "enforced_in": "application calculation", "source": "docs/adr/0001-dose-variants.md" }
],
"id_format": {
"scheme": "ULID with type prefix",

View File

@@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://life-helper.local/schema/diet_pattern.schema.json",
"title": "Diet Pattern (식이 패턴 — 의견 분열 영역의 합리적 선택지)",
"description": "nutrition/diet-protocols.md §2의 식이 패턴 5개. category='diet'인 protocol(원칙)과 분리된 카탈로그 — 단일 정답 없음, 사용자 체질·취향·문화에 맞춰 *조합* 대상. 만들기 습관으로 채택 시 habit.type='build'로 운영.",
"type": "object",
"required": ["id", "name", "core", "evidence_strength"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "예: mediterranean, low_carb_keto, tre_if, plant_based, k_diet"
},
"name": {
"type": "string",
"description": "사용자에게 보이는 이름 (예: '지중해 식단', '한식 기반')"
},
"core": {
"type": "string",
"description": "핵심 한 줄 정의"
},
"strengths": {
"type": "array",
"items": { "type": "string" }
},
"weaknesses": {
"type": "array",
"items": { "type": "string" }
},
"evidence_strength": {
"type": "string",
"enum": ["strong", "moderate", "mixed", "weak"],
"description": "RCT/메타분석 강도. PREDIMED 같은 large RCT = strong"
},
"korean_context_fit": {
"type": "string",
"enum": ["high", "medium", "low"],
"description": "한국 식문화·접근성 친화도"
},
"starter_levers": {
"type": "array",
"items": { "type": "string" },
"description": "처음 시도할 때의 진입 동작 3개 이하 (k_diet의 '첫 끼 단백질 +30g' 같은)"
},
"medical_warning": {
"type": "string",
"description": "특히 주의가 필요한 사용자군 (예: 저탄수→당뇨 약물 복용자 저혈당)"
},
"linked_protocol_ids": {
"type": "array",
"items": { "type": "string" },
"description": "이 패턴과 연결되는 protocol(category='diet') id 목록. 예: tre_if는 huberman §1.5 참조"
},
"reference_ids": {
"type": "array",
"items": { "type": "string" },
"description": "DOI/URL 참조 id (reference 테이블)"
}
}
}