남은 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
8.1 KiB
8.1 KiB
설계서: 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.
- A — ChatScreen 입력창 가림: edge-to-edge 모드의 시스템 nav bar 가 send 버튼/입력창을 덮어 사용 불가.
- B — LLM 실패 원인 불명: warm-up 은 Ready 까지 가지만 send 실패 시 빨간 배너에
LLM 응답 실패: <Type>만 떠 원인 진단 불가. - C — UX round 1 (raw enum 노출): 습관 카드/스트릭/추가 화면이 Drift row 의
'build'/RewardTier.dbValue('T0')/'Never miss twice'같은 식별자를 그대로 사용자에 노출.
2. 범위 (Scope)
- 포함:
- A.
ChatScreenScaffold.body →SafeArea(top: false, …). - B.
userTurncatch 가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 "프레임 레벨" → "표현 방식" + 예시 부연.
- A.
- 제외 (out of scope):
- LLM
BackendInitException: model may be invalid실 원인 (GPU KV cache RESOURCE_EXHAUSTED 후보) — 단말 빌드/설치 비용 때문에 별도 사이클로 분리. follow-up 이슈 발행 예정. - release 빌드에서 stack 숨김 (사용자 친화 메시지로 좁히기) — #342 종료 후 follow-up.
- LLM
3. 인수조건 (Acceptance Criteria)
- AC-A1 Pixel/Galaxy gesture nav 단말에서 입력창 + send 버튼이 nav bar 와 겹치지 않음.
- AC-B1 send 실패 시 빨간 배너에
LLM 응답 실패: <Type>\n<message>\n--- STACK ---\n<stack>표시. SelectableText 라 복사 가능. - AC-B2 빨간 배너 높이는 화면의 1/3 을 넘지 않고, 내부 스크롤로 전체 노출.
- AC-C1 습관 카드 부제가
build · L3 · …→만들기 · …(frameLevel 식별자 제거). - AC-C2 스트릭 화면 현재 티어가
T0/T1(raw) →🌱 새싹/🥉 3회 도전…. - AC-C3 스트릭 화면 강등 경고가
Never miss twice 발동 — 티어 강등→이틀 연속 빠졌어요. 한 단계 강등됐습니다.(영문 잠언 제거). - AC-C4 습관 추가 드롭다운이
만들기 (build)→만들기(식별자 병기 제거). - AC-C5 스트릭 화면의 현재 스트릭이
displayLargehero + 티어 라벨로 시각 위계 강조. - AC-D1 ChatScreen 첫 진입 시 빈 메시지 리스트 대신 안내 (아이콘 + 한 줄 설명 + 예시 prompt 4개). tap → 입력창 자동 채움 (자동 send X — 사용자 수정 여지).
- AC-D2 CheckIn 화면 날짜
2026-06-15raw →6월 15일 (월)한국식. DB 저장은_ymd유지. - AC-D3 HabitCreate 의
프레임 레벨→표현 방식(+ helperText행동 위주 vs 정체성 위주). 아이템 라벨L2 · 조건부 긍정→조건부 행동 (예: 아침에 햇빛 받기)식 예시 포함. - AC-D 167 기존 테스트 회귀 없음,
flutter analyzeclean.
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 진단 완료까지 블로커).