[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:
@@ -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**: 앱 실행 직후 첫 부팅 시 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-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 개 테이블 한눈에
|
||||
### 19 개 SoT 테이블 + 정규화 부속 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 표로 정리
|
||||
|
||||
Reference in New Issue
Block a user