Files
life-helper/docs/design/342-v042-hotfix
joungmin e81f3e44a4 [Designer] #342 UX round 1 — raw enum → 한국어 라벨 + 스트릭 hero
dev v0.4.2 위 hotfix. v0.4.1 단말 테스트에서 발견된 raw 식별자
노출 P0 3 + P1 2.

- ui/labels.dart 신규 — habitTypeLabel(FromDb) / rewardTierLabel.
  domain enum 의 한국어 라벨 단일 지점 (domain layer 분리).
- habit_list 부제: 'build · L3 · …' → '만들기 · …'.
  FrameLevel 노출 제거 (시스템 규약).
- streak: 'T0' / 'T1' raw → '🌱 새싹' / '🥉 3회 도전' …,
  영문 'Never miss twice' → '이틀 연속 빠졌어요. 한 단계 강등됐습니다',
  현재 스트릭을 displayLarge hero 로 위계 강조.
- habit_create 드롭다운: '만들기 (build)' → '만들기'.
- 설계서 docs/design/342-v042-hotfix/README.md — A/B/C 11 AC.
- CHANGELOG v0.4.2 에 UX round 1 섹션 추가.

167 tests passed, analyze clean. APK 재빌드 보류 (사용자 결정).

Refs #342
2026-06-15 15:23:05 +09:00
..

설계서: 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건.
  • 제외 (out of scope):
    • LLM BackendInitException: model may be invalid 실 원인 (GPU KV cache RESOURCE_EXHAUSTED 후보) — 단말 빌드/설치 비용 때문에 별도 사이클로 분리. follow-up 이슈 발행 예정.
    • release 빌드에서 stack 숨김 (사용자 친화 메시지로 좁히기) — #342 종료 후 follow-up.
    • 8 화면 UX 전체 — 남은 P1/P2 (chat 빈 상태, check_in 날짜 포맷, 프레임 레벨 라벨 명확화) 는 v0.4.3 또는 후속 이슈로.

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 스트릭 화면의 현재 스트릭이 displayLarge hero + 티어 라벨로 시각 위계 강조.
  • 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.bodySafeArea 로 감싸지면서 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 후보. 단말 빌드 비용 때문에 분리.
  • 남은 UX P1/P2 — chat 빈 상태 안내, check_in 한국식 날짜, 프레임 레벨 라벨 명확화.
  • 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 진단 완료까지 블로커).