남은 P1/P2 3건. - ChatScreen 빈 상태: 아이콘 + 한 줄 설명 + 예시 prompt 4개 (tap → _textCtrl 자동 채움, 자동 send X). - CheckIn 날짜: '2026-06-15' raw → '6월 15일 (월)' 한국식. DB 저장은 _ymd 유지. - HabitCreate '프레임 레벨' → '표현 방식' + helperText. 아이템: '조건부 행동 (예: 아침에 햇빛 받기)' / '정체성 (예: 나는 일찍 자는 사람)'. - 설계서 #342 README — D 섹션 + AC-D1/D2/D3 추가. - CHANGELOG v0.4.2 UX round 2 블록. 167 tests passed, analyze clean. Refs #342
117 lines
8.1 KiB
Markdown
117 lines
8.1 KiB
Markdown
# 설계서: v0.4.2 hotfix — ChatScreen SafeArea + LLM 진단 + UX round 1 (#342)
|
|
|
|
> **상태**: Approved
|
|
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
|
|
> **추적성** — Redmine: #342 · 관련 ADR: 없음
|
|
> · 구현 파일: `app/lib/ui/screens/chat_screen.dart`, `app/lib/state/chat_providers.dart`, `app/lib/ui/labels.dart`, `app/lib/ui/screens/habit_list_screen.dart`, `app/lib/ui/screens/streak_screen.dart`, `app/lib/ui/screens/habit_create_screen.dart`
|
|
> · 테스트: 기존 167 회귀 (신규 추가 없음 — string label / SafeArea wrap 라 단위 가치 낮음)
|
|
|
|
## 1. 목적 (Why)
|
|
v0.4.1 실 단말 (Android, 사용자 본인) 첫 테스트에서 발견된 사용성·진단 격차 묶음. 모두 dev 단계 신속 hotfix.
|
|
|
|
1. **A — ChatScreen 입력창 가림**: edge-to-edge 모드의 시스템 nav bar 가 send 버튼/입력창을 덮어 사용 불가.
|
|
2. **B — LLM 실패 원인 불명**: warm-up 은 Ready 까지 가지만 send 실패 시 빨간 배너에 `LLM 응답 실패: <Type>` 만 떠 원인 진단 불가.
|
|
3. **C — UX round 1 (raw enum 노출)**: 습관 카드/스트릭/추가 화면이 Drift row 의 `'build'` / `RewardTier.dbValue('T0')` / `'Never miss twice'` 같은 식별자를 그대로 사용자에 노출.
|
|
|
|
## 2. 범위 (Scope)
|
|
- **포함**:
|
|
- A. `ChatScreen` Scaffold.body → `SafeArea(top: false, …)`.
|
|
- B. `userTurn` catch 가 `e.toString() + stack` 전체를 error state 에 저장. ChatScreen 빨간 배너를 `SingleChildScrollView + SelectableText` (monospace, 12pt, 최대 1/3 높이) 로 교체.
|
|
- C. `app/lib/ui/labels.dart` 신규 — `habitTypeLabel(HabitType)`, `habitTypeLabelFromDb(String)`, `rewardTierLabel(RewardTier)`. P0 3건 + P1 2건.
|
|
- D. UX round 2 — chat 빈 상태 안내 (예시 prompt 4 tap-to-fill), check_in 한국식 날짜 (`6월 15일 (월)`), habit_create "프레임 레벨" → "표현 방식" + 예시 부연.
|
|
- **제외 (out of scope)**:
|
|
- LLM `BackendInitException: model may be invalid` 실 원인 (GPU KV cache RESOURCE_EXHAUSTED 후보) — 단말 빌드/설치 비용 때문에 별도 사이클로 분리. follow-up 이슈 발행 예정.
|
|
- release 빌드에서 stack 숨김 (사용자 친화 메시지로 좁히기) — #342 종료 후 follow-up.
|
|
|
|
## 3. 인수조건 (Acceptance Criteria)
|
|
- [x] **AC-A1** Pixel/Galaxy gesture nav 단말에서 입력창 + send 버튼이 nav bar 와 겹치지 않음.
|
|
- [x] **AC-B1** send 실패 시 빨간 배너에 `LLM 응답 실패: <Type>\n<message>\n--- STACK ---\n<stack>` 표시. SelectableText 라 복사 가능.
|
|
- [x] **AC-B2** 빨간 배너 높이는 화면의 1/3 을 넘지 않고, 내부 스크롤로 전체 노출.
|
|
- [x] **AC-C1** 습관 카드 부제가 `build · L3 · …` → `만들기 · …` (frameLevel 식별자 제거).
|
|
- [x] **AC-C2** 스트릭 화면 현재 티어가 `T0` / `T1` (raw) → `🌱 새싹` / `🥉 3회 도전` ….
|
|
- [x] **AC-C3** 스트릭 화면 강등 경고가 `Never miss twice 발동 — 티어 강등` → `이틀 연속 빠졌어요. 한 단계 강등됐습니다.` (영문 잠언 제거).
|
|
- [x] **AC-C4** 습관 추가 드롭다운이 `만들기 (build)` → `만들기` (식별자 병기 제거).
|
|
- [x] **AC-C5** 스트릭 화면의 현재 스트릭이 `displayLarge` hero + 티어 라벨로 시각 위계 강조.
|
|
- [x] **AC-D1** ChatScreen 첫 진입 시 빈 메시지 리스트 대신 안내 (아이콘 + 한 줄 설명 + 예시 prompt 4개). tap → 입력창 자동 채움 (자동 send X — 사용자 수정 여지).
|
|
- [x] **AC-D2** CheckIn 화면 날짜 `2026-06-15` raw → `6월 15일 (월)` 한국식. DB 저장은 `_ymd` 유지.
|
|
- [x] **AC-D3** HabitCreate 의 `프레임 레벨` → `표현 방식` (+ helperText `행동 위주 vs 정체성 위주`). 아이템 라벨 `L2 · 조건부 긍정` → `조건부 행동 (예: 아침에 햇빛 받기)` 식 예시 포함.
|
|
- [x] **AC-D** 167 기존 테스트 회귀 없음, `flutter analyze` clean.
|
|
|
|
## 4. 컨텍스트 & 제약
|
|
- **의존성**: flutter_gemma 0.16.5 (B 변경 안 함), Riverpod 2.x, Drift row 의 raw String enum.
|
|
- **제약**:
|
|
- dev 단계 hotfix — release 노출 가능한 stack 도 허용 (사용자 본인 단말 진단 우선).
|
|
- C 의 라벨 매핑은 UI 레이어 단일 지점 (`ui/labels.dart`) — domain enum 에 `koreanLabel` 두지 않음 (관심사 분리).
|
|
- **가정**:
|
|
- `h.type` 은 Drift row 의 String — `HabitTypeX.dbValue` 와 동일한 wire 값 (`'build'` / `'break'`).
|
|
- `RewardTier` 의 사용자 명칭은 메모리상 5-Tier 정의 — 🌱 새싹 / 🥉 3회 / 🥈 7일 / 🥇 30일 / 🏆 6주 완주.
|
|
|
|
## 5. 아키텍처 개요
|
|
순수 string 매핑 + Widget tree 재구성. 신규 모듈 없음.
|
|
|
|
```
|
|
ChatScreen
|
|
├─ Scaffold.body — SafeArea(top: false) ← AC-A1
|
|
│ └─ Column
|
|
│ ├─ _WarmupErrorBanner (변경 없음)
|
|
│ ├─ Container(error) ← AC-B1/B2
|
|
│ │ constraints: maxHeight: screen/3
|
|
│ │ child: SingleChildScrollView(SelectableText, monospace 12pt)
|
|
│ └─ ListView (변경 없음)
|
|
|
|
ChatSessionController.userTurn ← AC-B1
|
|
└─ catch (e, st) → state.error = "LLM 응답 실패: ${e.runtimeType}\n$e\n\n--- STACK ---\n$st"
|
|
|
|
ui/labels.dart ← AC-C1~C4
|
|
├─ habitTypeLabel(HabitType) → '만들기' / '없애기'
|
|
├─ habitTypeLabelFromDb(String) → ↑ (Drift raw 분기, 기본 fallback = dbValue)
|
|
└─ rewardTierLabel(RewardTier) → '🌱 새싹' / '🥉 3회 도전' / … / '🏆 6주 완주'
|
|
|
|
habit_list_screen / streak_screen / habit_create_screen
|
|
└─ raw enum 노출 지점 모두 labels.dart 의 함수로 교체
|
|
```
|
|
|
|
## 6. 데이터 모델
|
|
신규 모델 없음. 매핑 도메인은 기존 enum (`HabitType`, `FrameLevel`, `RewardTier`) 의 표현 레이어만 분리.
|
|
|
|
| Enum | Raw (DB/wire) | UI 라벨 |
|
|
|---|---|---|
|
|
| `HabitType.build` | `'build'` | `만들기` |
|
|
| `HabitType.breakHabit` | `'break'` | `없애기` |
|
|
| `RewardTier.t0` | `'T0'` | `🌱 새싹` |
|
|
| `RewardTier.t1` | `'T1'` | `🥉 3회 도전` |
|
|
| `RewardTier.t2` | `'T2'` | `🥈 7일 형성` |
|
|
| `RewardTier.t3` | `'T3'` | `🥇 30일 정착` |
|
|
| `RewardTier.t4` | `'T4'` | `🏆 6주 완주` |
|
|
|
|
`FrameLevel` 은 본 hotfix 에서 UI 노출을 **제거** — 사용자에 의미 모호 (L2/L3 차이가 즉시 보이지 않음). 라벨 매핑 미작성.
|
|
|
|
## 7. 함수 명세
|
|
|
|
| 함수 | 책임 | 시그니처 | 복잡? |
|
|
|------|------|----------|-------|
|
|
| `habitTypeLabel` | enum → 한국어 라벨 | `String habitTypeLabel(HabitType)` | 단순 (switch) |
|
|
| `habitTypeLabelFromDb` | Drift raw String → 한국어 (fallback = raw) | `String habitTypeLabelFromDb(String)` | 단순 (switch + default) |
|
|
| `rewardTierLabel` | enum → 이모지+한국어 | `String rewardTierLabel(RewardTier)` | 단순 (switch) |
|
|
|
|
모두 단순 string switch 라 `fn-*.md` 분리 불필요.
|
|
|
|
## 8. 흐름 / 알고리즘
|
|
- A: `Scaffold.body` 가 `SafeArea` 로 감싸지면서 system bottom inset 만큼 padding 자동 적용. `top: false` 인 이유는 AppBar 가 이미 top inset 처리 (이중 padding 방지).
|
|
- B: `Future.try-catch (e, st)` 에서 stack trace 까지 함께 string concat → state → 빨간 컨테이너의 `SelectableText` 로 노출. 사용자가 텍스트 선택 → 복사해 외부에 공유 가능.
|
|
- C: 라벨 매핑은 분기/상태/I/O 없음. switch one-liner.
|
|
|
|
## 9. 테스트 전략
|
|
- 신규 unit 추가 없음 — 라벨 매핑은 상수 매핑이라 unit 가치 낮음.
|
|
- SafeArea + 빨간 배너는 widget 레이어 변경이지만 LLM 단말 시도 자체가 차단 상태 (#312 corpus collection blocker) — manual 검증으로 대체.
|
|
- 167 기존 테스트 회귀 없음으로 단위/통합/도메인 보호.
|
|
|
|
## 10. 후속 (v0.4.3 또는 별개 이슈)
|
|
- `BackendInitException: model may be invalid` 진단/수정 — `maxTokens=2048` 의 GPU buffer 후보. 단말 빌드 비용 때문에 분리.
|
|
- release 빌드에서 stack 숨김 (사용자 친화 메시지로).
|
|
|
|
## 11. 추적성
|
|
- **Redmine**: #342 (07-Release, dev hotfix bundle).
|
|
- **선행**: #311 (v0.4.1 warm-up — 빨간 배너 자체는 v0.4.1 에서 도입, 본 hotfix 가 진단성 강화).
|
|
- **관련**: #312 (corpus collection — LLM 동작 의존, B 진단 완료까지 블로커).
|