[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:
@@ -21,6 +21,7 @@
|
|||||||
| `frame_pattern` | 동 문서 §"언어 프레이밍" | L0→L1/L2/L3 변환 예시 |
|
| `frame_pattern` | 동 문서 §"언어 프레이밍" | L0→L1/L2/L3 변환 예시 |
|
||||||
| `reward_menu_item` | 동 문서 §"리워드 시스템" | 추천 보상 메뉴 |
|
| `reward_menu_item` | 동 문서 §"리워드 시스템" | 추천 보상 메뉴 |
|
||||||
| `reference` | 모든 SoT의 출처 필드 | DOI/URL/책 인용 |
|
| `reference` | 모든 SoT의 출처 필드 | DOI/URL/책 인용 |
|
||||||
|
| `diet_pattern` | `nutrition/diet-protocols.md` §2 | 식이 패턴 5개 (지중해/저탄수/IF/Plant-Based/한식) — 의견 분열 영역, 사용자 선택 메뉴 |
|
||||||
|
|
||||||
### User Data (사용자가 생성·변경)
|
### User Data (사용자가 생성·변경)
|
||||||
| 엔티티 | 설명 | 핵심 제약 |
|
| 엔티티 | 설명 | 핵심 제약 |
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
> **상태**: Accepted
|
> **상태**: Accepted
|
||||||
> **날짜**: 2026-06-11 · **결정자**: 사용자 (joungmin) + AI 합의 · **관련 이슈**: #204
|
> **날짜**: 2026-06-11 · **결정자**: 사용자 (joungmin) + AI 합의 · **관련 이슈**: #204
|
||||||
|
> **관련 ADR**: [ADR-0002 — dose-variants 정규화 (어떻게 저장)](./0002-dose-variants-normalized.md)
|
||||||
|
|
||||||
## 맥락 (Context)
|
## 맥락 (Context)
|
||||||
|
|
||||||
|
|||||||
111
docs/adr/0002-dose-variants-normalized.md
Normal file
111
docs/adr/0002-dose-variants-normalized.md
Normal 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 진입점 + 인덱스).
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# 02 — Catalog 테이블 7 개 Drift 정의 (#204)
|
# 02 — Catalog 테이블 8 개 Drift 정의 (#204)
|
||||||
|
|
||||||
> 부모 설계서: [README.md](./README.md)
|
> 부모 설계서: [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.
|
인덱스: `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)
|
## 시드 카운트 추산 (05-seed-data.md 와 sync)
|
||||||
|
|
||||||
| 테이블 | 추산 행 수 | 비고 |
|
| 테이블 | 추산 행 수 | 비고 |
|
||||||
@@ -187,8 +217,9 @@ class References extends Table {
|
|||||||
| `methodologies` | 21 | habit-todo-methodologies.md §1~§21 |
|
| `methodologies` | 21 | habit-todo-methodologies.md §1~§21 |
|
||||||
| `frame_patterns` | ~30 | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" |
|
| `frame_patterns` | ~30 | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" |
|
||||||
| `reward_menu_items` | ~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
|
## 카탈로그는 read-only
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 03 — User-Data 테이블 11 개 Drift 정의 + R 강제 매트릭스 (#204)
|
# 03 — User-Data 테이블 11 개 Drift 정의 + R 강제 매트릭스 (#204)
|
||||||
|
|
||||||
> 부모 설계서: [README.md](./README.md)
|
> 부모 설계서: [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. 공통 규약
|
## 0. 공통 규약
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class Habits extends Table {
|
|||||||
- `IDX_habits_user_status_type(user_id, status, type)` — R1/R2 빠른 카운트.
|
- `IDX_habits_user_status_type(user_id, status, type)` — R1/R2 빠른 카운트.
|
||||||
- `IDX_habits_phase(phase_id)`.
|
- `IDX_habits_phase(phase_id)`.
|
||||||
|
|
||||||
## 4. `habit_dose_variants` (정규화 — ADR 후보)
|
## 4. `habit_dose_variants` (정규화 부속 — [ADR-0002](../../adr/0002-dose-variants-normalized.md))
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
class HabitDoseVariants extends Table {
|
class HabitDoseVariants extends Table {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
| version | 범위 | 작성자 | 비고 |
|
| 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 등) |
|
| 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` 콜백 작성.
|
- schema 가 바뀌면 **반드시 schemaVersion 증가** + `onUpgrade` 콜백 작성.
|
||||||
- 컬럼 추가만은 가능하나, drop/rename 은 destructive — `m.alterTable(TableMigration(...))` 사용.
|
- 컬럼 추가만은 가능하나, drop/rename 은 destructive — `m.alterTable(TableMigration(...))` 사용.
|
||||||
@@ -18,10 +20,10 @@
|
|||||||
|
|
||||||
```dart
|
```dart
|
||||||
@DriftDatabase(tables: [
|
@DriftDatabase(tables: [
|
||||||
// catalog
|
// catalog (8)
|
||||||
Protocols, BreakProtocols, CommonFrames, Methodologies,
|
Protocols, BreakProtocols, CommonFrames, Methodologies,
|
||||||
FramePatterns, RewardMenuItems, References,
|
FramePatterns, RewardMenuItems, References, DietPatterns,
|
||||||
// user
|
// user (11) + 부속 (1, ADR-0002)
|
||||||
Users, Phases, Habits, HabitDoseVariants, IfThenRules,
|
Users, Phases, Habits, HabitDoseVariants, IfThenRules,
|
||||||
TrackerEntries, LapseLogs, UrgeLogs,
|
TrackerEntries, LapseLogs, UrgeLogs,
|
||||||
RewardDeclarations, RewardClaims, Reflections,
|
RewardDeclarations, RewardClaims, Reflections,
|
||||||
@@ -68,6 +70,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
'CREATE INDEX IDX_references_kind ON "references"(kind)'));
|
'CREATE INDEX IDX_references_kind ON "references"(kind)'));
|
||||||
await m.createIndex(Index('IDX_references_doi',
|
await m.createIndex(Index('IDX_references_doi',
|
||||||
'CREATE INDEX IDX_references_doi ON "references"(doi) WHERE doi IS NOT NULL'));
|
'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
|
// user
|
||||||
await m.createIndex(Index('IDX_phases_user_status',
|
await m.createIndex(Index('IDX_phases_user_status',
|
||||||
'CREATE INDEX IDX_phases_user_status ON phases(user_id, status)'));
|
'CREATE INDEX IDX_phases_user_status ON phases(user_id, status)'));
|
||||||
|
|||||||
@@ -22,9 +22,12 @@ app/assets/seed/
|
|||||||
├── methodologies.json # Array<Methodology> — 21 항목
|
├── methodologies.json # Array<Methodology> — 21 항목
|
||||||
├── frame_patterns.json # Array<FramePattern> — ~30 항목
|
├── frame_patterns.json # Array<FramePattern> — ~30 항목
|
||||||
├── reward_menu_items.json # Array<RewardMenuItem> — ~30 항목
|
├── reward_menu_items.json # Array<RewardMenuItem> — ~30 항목
|
||||||
|
├── diet_patterns.json # Array<DietPattern> — 5 항목 (OQ-3 결정)
|
||||||
└── references.json # Array<Reference> — ~50 항목
|
└── references.json # Array<Reference> — ~50 항목
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 총 **8 시드 파일** (Catalog 카탈로그 1:1). user-data 테이블은 시드 없음.
|
||||||
|
|
||||||
- 각 파일 형식 = `schema/<entity>.schema.json` 을 만족하는 객체 배열.
|
- 각 파일 형식 = `schema/<entity>.schema.json` 을 만족하는 객체 배열.
|
||||||
- 인코딩 = UTF-8 (한국어 텍스트).
|
- 인코딩 = UTF-8 (한국어 텍스트).
|
||||||
- `pubspec.yaml` 의 `flutter.assets:` 에 `assets/seed/` 등록.
|
- `pubspec.yaml` 의 `flutter.assets:` 에 `assets/seed/` 등록.
|
||||||
@@ -33,12 +36,13 @@ app/assets/seed/
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. references.json ← 다른 시드가 reference_ids 로 참조
|
1. references.json ← 다른 시드가 reference_ids 로 참조
|
||||||
2. protocols.json
|
2. protocols.json ← diet_patterns 가 linked_protocol_ids 로 참조
|
||||||
3. break_protocols.json
|
3. break_protocols.json
|
||||||
4. common_frames.json
|
4. common_frames.json
|
||||||
5. methodologies.json
|
5. methodologies.json
|
||||||
6. frame_patterns.json
|
6. frame_patterns.json
|
||||||
7. reward_menu_items.json
|
7. reward_menu_items.json
|
||||||
|
8. diet_patterns.json ← protocols + references 참조 (마지막)
|
||||||
```
|
```
|
||||||
|
|
||||||
- 각 파일 batch insert 는 단일 트랜잭션.
|
- 각 파일 batch insert 는 단일 트랜잭션.
|
||||||
@@ -85,8 +89,19 @@ Future<void> importIfNeeded() async {
|
|||||||
| methodologies.json | habit-todo-methodologies.md §1~§21 | `###` 헤더 단위 | 21 |
|
| methodologies.json | habit-todo-methodologies.md §1~§21 | `###` 헤더 단위 | 21 |
|
||||||
| frame_patterns.json | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" | 표 row 단위 | ~30 |
|
| frame_patterns.json | habit-todo-methodologies.md "흔한 끊기 목표 변환 30선" | 표 row 단위 | ~30 |
|
||||||
| reward_menu_items.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 |
|
| 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 자동 추출
|
## 7. 손 작성 vs 자동 추출
|
||||||
|
|
||||||
본 Phase 는 **손 작성** (Developer 가 SoT 마크다운을 보고 JSON 작성). 이유:
|
본 Phase 는 **손 작성** (Developer 가 SoT 마크다운을 보고 JSON 작성). 이유:
|
||||||
@@ -118,6 +133,7 @@ test/fixtures/seed/
|
|||||||
├── methodologies_small.json # 3 항목 (core engine 3 개)
|
├── methodologies_small.json # 3 항목 (core engine 3 개)
|
||||||
├── frame_patterns_small.json # 2 항목
|
├── frame_patterns_small.json # 2 항목
|
||||||
├── reward_menu_items_small.json # 5 항목 (T0~T4 각 1)
|
├── reward_menu_items_small.json # 5 항목 (T0~T4 각 1)
|
||||||
|
├── diet_patterns_small.json # 2 항목 (k_diet + mediterranean)
|
||||||
└── references_small.json # 2 항목
|
└── references_small.json # 2 항목
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
> **작성**: [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)
|
> · 구현 파일: `app/` (TBD by Developer) · 테스트: `app/test/` (TBD by Developer)
|
||||||
> **하위 문서**:
|
> **하위 문서**:
|
||||||
> - [01-project-structure.md](./01-project-structure.md) — Flutter 레이아웃 + pubspec + codegen
|
> - [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 강제 위치 표
|
> - [03-drift-schema-user.md](./03-drift-schema-user.md) — User-data 테이블 11개 + R1~R10 강제 위치 표
|
||||||
> - [04-migrations.md](./04-migrations.md) — Drift schemaVersion 전략
|
> - [04-migrations.md](./04-migrations.md) — Drift schemaVersion 전략
|
||||||
> - [05-seed-data.md](./05-seed-data.md) — 시드 로딩 전략
|
> - [05-seed-data.md](./05-seed-data.md) — 시드 로딩 전략
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
> Planner 목표: "Huberman/방법론/끊기/식이 SoT 를 앱에서 쓸 수 있는 로컬-우선 Flutter 앱의 데이터 계층 + 최소 vertical slice 를 만든다."
|
> 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)
|
## 2. 범위 (Scope)
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 18 개 엔
|
|||||||
|
|
||||||
- Flutter 프로젝트 부트스트랩 (`app/` 서브디렉토리, iOS + Android 빌드 타깃).
|
- Flutter 프로젝트 부트스트랩 (`app/` 서브디렉토리, iOS + Android 빌드 타깃).
|
||||||
- Drift (sqlite) ORM 도입 + 코드 생성 파이프라인 (`build_runner`).
|
- Drift (sqlite) ORM 도입 + 코드 생성 파이프라인 (`build_runner`).
|
||||||
- 18 개 테이블 Dart 정의 (Catalog 7 + User 11) — `schema/*.json` 19 개 SoT 와 1:1 매핑.
|
- **19 개 SoT 테이블 Dart 정의 (Catalog 8 + User 11)** — `schema/*.json` 19 개 SoT 와 1:1 매핑 + 정규화 부속 `habit_dose_variants` 1 개.
|
||||||
- `dose_variants[]` 저장 형태 결정 — **별도 `habit_dose_variant` 테이블로 정규화** (근거는 §11, 상세는 03-drift-schema-user.md).
|
- `dose_variants[]` 저장 형태 결정 — **별도 `habit_dose_variants` 테이블로 정규화** (ADR-0002 참조, 상세는 03-drift-schema-user.md).
|
||||||
- `tracker_entry.variant_id` + `context_snapshot` 저장.
|
- `tracker_entry.variant_id` + `context_snapshot` 저장.
|
||||||
- R1~R10 운영 규칙별 강제 위치 (schema CHECK · index · trigger · app layer) 매트릭스.
|
- R1~R10 운영 규칙별 강제 위치 (schema CHECK · index · trigger · app layer) 매트릭스.
|
||||||
- Drift `schemaVersion = 1` 마이그레이션 진입점 + 향후 변경 패턴.
|
- Drift `schemaVersion = 1` 마이그레이션 진입점 + 향후 변경 패턴.
|
||||||
@@ -58,9 +58,9 @@ Phase 1 의 단일 과제는 **"머릿속/마크다운에만 있는 18 개 엔
|
|||||||
QA 가 다음 항목을 모두 통과 표시할 수 있어야 한다.
|
QA 가 다음 항목을 모두 통과 표시할 수 있어야 한다.
|
||||||
|
|
||||||
- [ ] **AC-1**: `flutter pub get && dart run build_runner build` 가 새 환경에서 에러 없이 통과한다 (Drift codegen 포함).
|
- [ ] **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**: 앱 실행 직후 첫 부팅 시 19 개 SoT 테이블 + 정규화 부속 `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-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-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-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).
|
- [ ] **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]
|
│ [data/db/daos/HabitDao.insertWithVariants]
|
||||||
│ │
|
│ │
|
||||||
│ ▼ (insert 트랜잭션)
|
│ ▼ (insert 트랜잭션)
|
||||||
│ habit + habit_dose_variant 동시 commit
|
│ habit + habit_dose_variants 동시 commit
|
||||||
▼
|
▼
|
||||||
[features/daily_checkin] ──► location + condition 1 탭씩
|
[features/daily_checkin] ──► location + condition 1 탭씩
|
||||||
│
|
│
|
||||||
@@ -195,7 +195,7 @@ life-helper/
|
|||||||
|
|
||||||
## 6. 데이터 모델
|
## 6. 데이터 모델
|
||||||
|
|
||||||
### 18 개 테이블 한눈에
|
### 19 개 SoT 테이블 + 정규화 부속 1 한눈에
|
||||||
|
|
||||||
| # | 테이블 | 분류 | SoT JSON | 행 수 (시드) | Drift 정의 위치 |
|
| # | 테이블 | 분류 | SoT JSON | 행 수 (시드) | Drift 정의 위치 |
|
||||||
|---|--------|------|----------|-------------|-----------------|
|
|---|--------|------|----------|-------------|-----------------|
|
||||||
@@ -206,10 +206,10 @@ life-helper/
|
|||||||
| 5 | `frame_pattern` | catalog | frame_pattern.schema.json | ~30 (32 변환표) | 02-catalog |
|
| 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 |
|
| 6 | `reward_menu_item` | catalog | reward_menu_item.schema.json | ~30 (메뉴 30선) | 02-catalog |
|
||||||
| 7 | `reference` | catalog | reference.schema.json | ~50 (출처 통합 합계) | 02-catalog |
|
| 7 | `reference` | catalog | reference.schema.json | ~50 (출처 통합 합계) | 02-catalog |
|
||||||
| 8 | `user` | user | user.schema.json | 1 (자동) | 03-user |
|
| 8 | `diet_pattern` | catalog | diet_pattern.schema.json | 5 (지중해/케토/TRE/식물성/한식) | 02-catalog |
|
||||||
| 9 | `phase` | user | phase.schema.json | 0 | 03-user |
|
| 9 | `user` | user | user.schema.json | 1 (자동) | 03-user |
|
||||||
| 10 | `habit` | user | habit.schema.json | 0 | 03-user |
|
| 10 | `phase` | user | phase.schema.json | 0 | 03-user |
|
||||||
| 11 | `habit_dose_variant` | user (정규화 산출) | habit.schema.json `dose_variants[]` 분해 | 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 |
|
| 12 | `if_then_rule` | user | if_then_rule.schema.json | 0 | 03-user |
|
||||||
| 13 | `tracker_entry` | user | tracker_entry.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 |
|
| 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 |
|
| 16 | `reward_declaration` | user | reward_declaration.schema.json | 0 | 03-user |
|
||||||
| 17 | `reward_claim` | user | reward_claim.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 |
|
| 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).
|
4. UI: dose_variants 입력 (최소 1 개, 강제 X. 본 vertical slice 는 기본 variant 1 개 자동 생성: label='기본', dose_text=protocol.min_dose_for_start, is_minimum=true).
|
||||||
5. `HabitDao.insertWithVariants` 트랜잭션:
|
5. `HabitDao.insertWithVariants` 트랜잭션:
|
||||||
- `habit` insert.
|
- `habit` insert.
|
||||||
- `habit_dose_variant` 다중 insert.
|
- `habit_dose_variants` 다중 insert.
|
||||||
- `if_then_rule` 다중 insert (있다면).
|
- `if_then_rule` 다중 insert (있다면).
|
||||||
6. commit → UI 가 habit list 갱신.
|
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. | ✗ |
|
| **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) 은 그대로 보존.
|
**결정 근거**: 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 후보
|
### 되돌리기 어려운 결정 → ADR 후보
|
||||||
|
|
||||||
- `habit_dose_variant` 정규화: 본 설계서에 명시했고 schema/*.json 은 변경하지 않으므로 ADR 별도 분리는 옵션. **Phase 1 종료 후 ADR-0002 로 승격 권장** (DB 형태 결정 = 멀티 디바이스 sync 시 갈등 가능).
|
- `habit_dose_variants` 정규화: **ADR-0002 로 승격 완료** (2026-06-11). 본 설계서는 ADR-0002 결정을 참조.
|
||||||
- Riverpod 채택: 코드 분량 큼. **ADR-0003 후보**.
|
- 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 이 명시적이라 추적 유리.
|
| OQ | 질문 | 결정 | 반영 위치 |
|
||||||
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 안에 반영 필요.
|
| **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 추가 |
|
||||||
4. **OQ-4**: `frame_pattern.schema.json` 에 ko/en 두 언어 컬럼 없음 — 한국어만으로 충분한지. 본 설계서는 한국어 only 가정.
|
| **OQ-2** | 시드 JSON 자동 추출 vs 손 작성? | **손 작성** (Phase 1). Phase 2 에서 `scripts/extract_seed.py` 재검토. | `05-seed-data.md` §7 유지 |
|
||||||
5. **OQ-5**: 5-Tier 의 "Never miss twice" 정의 — "연속 2 일 blank → 스트릭 0" 으로 본 설계서가 박았는데, 사용자가 "주 단위 2 회 blank" 같은 다른 해석을 원하는지 확인 필요. 본 설계서의 정의는 fn-compute-streak.md §"동작" 에 상세.
|
| **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 테이블 목록 갱신 |
|
||||||
6. **OQ-6**: `tracker_entry.variant_id` 가 nullable 인가? — 본 설계서는 nullable (variant 없는 habit 도 허용). 다만 dose_variants 권장이라면 not-null 강제도 가능. 결정 필요.
|
| **OQ-4** | `frame_pattern` 한국어 only? | **한국어 only OK** — i18n 은 v2. | `02-drift-schema-catalog.md` §5 유지 |
|
||||||
7. **OQ-7**: vertical slice UI 의 화면 갯수 — habit 생성 폼 + 오늘 카드 + 체크인 시트 + (선택) habit 목록 화면. 본 설계서는 4 개. Designer 단계에서 줄이거나 늘릴지 결정.
|
| **OQ-5** | "Never miss twice" 정의? | **연속 2 일 blank → tier 강등 + streak 0. 1 일 blank → streak 0 유지하되 tier 유지**. | `fn-compute-streak.md` 본문 + AC-12 (R10 외 streak 동작) |
|
||||||
8. **OQ-8**: 앱 ID, 패키지명 — `kr.cloud_handson.life_helper` 가정. iOS bundle id 도 동일. 사용자 확정 필요.
|
| **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 가 작업 종료 시 검증)
|
## 부록: 자가 점검 (Architect 가 작업 종료 시 검증)
|
||||||
|
|
||||||
- [x] `_TEMPLATE.md` 12 개 섹션 모두 비어있지 않음
|
- [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] 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] 복잡 함수 6 개 각각 fn-*.md 존재 (recommend-variant, compute-streak, weekly-minimum-ratio, validate-frame-level, active-habit-quota, seed-importer)
|
||||||
- [x] §7 함수 명세 표에 모든 함수 등재 (단순 13 + 복잡 6)
|
- [x] §7 함수 명세 표에 모든 함수 등재 (단순 13 + 복잡 6). diet_pattern 은 read-only 카탈로그라 신규 함수 추가 없음 (`fn-seed-importer` 가 처리)
|
||||||
- [x] AC §3 — QA 판정 가능한 17 항목 (AC-1 ~ AC-16)
|
- [x] AC §3 — QA 판정 가능한 16 항목 (AC-1 ~ AC-16). AC-2 에 diet_pattern = 5 시드 검증 추가
|
||||||
|
- [x] §12 모든 OQ (OQ-1 ~ OQ-8) Resolved 표로 정리
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Future<double?> weeklyMinimumRatio({
|
|||||||
|
|
||||||
## 2. 책임
|
## 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. 입력
|
## 3. 입력
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"methodology": "./methodology.schema.json",
|
"methodology": "./methodology.schema.json",
|
||||||
"frame_pattern": "./frame_pattern.schema.json",
|
"frame_pattern": "./frame_pattern.schema.json",
|
||||||
"reward_menu_item": "./reward_menu_item.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_data": {
|
||||||
"user": "./user.schema.json",
|
"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": "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": "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": "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": {
|
"id_format": {
|
||||||
"scheme": "ULID with type prefix",
|
"scheme": "ULID with type prefix",
|
||||||
|
|||||||
60
schema/diet_pattern.schema.json
Normal file
60
schema/diet_pattern.schema.json
Normal 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 테이블)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user