diff --git a/CHANGELOG.md b/CHANGELOG.md index f89f7b5..d59b4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ - **습관 추가 드롭다운** — `만들기 (build)` → `만들기` (영어 식별자 병기 제거). - 신규 `app/lib/ui/labels.dart` — domain enum 의 한국어 라벨 매핑 단일 지점. domain layer 에 `koreanLabel` 두지 않음 (관심사 분리). +### UX round 2 — 빈 상태 + 날짜 + 라벨 명확화 (Redmine #342 추가) +- **ChatScreen 빈 상태 안내** — 첫 진입 시 빈 메시지 리스트 대신 아이콘 + 한 줄 설명 + 예시 prompt 4개 (`아침 햇빛 받기 습관 추가해줘`, `오늘 운동 했어`, `내 스트릭 보여줘`, `수면 프로토콜 알려줘`). tap → 입력창 자동 채움 (자동 send X, 사용자 수정 여지). +- **CheckIn 날짜 한국식** — `2026-06-15` raw → `6월 15일 (월)`. DB 저장은 `_ymd` 유지. +- **HabitCreate 표현 방식** — `프레임 레벨` (의미 모호) → `표현 방식` + helperText `행동 위주 vs 정체성 위주`. 아이템 라벨 `L2 · 조건부 긍정` / `L3 · 정체성` → `조건부 행동 (예: 아침에 햇빛 받기)` / `정체성 (예: 나는 일찍 자는 사람)` 식 예시 포함. + ### Dev - **LLM 실패 빨간 배너에 full message + stack trace** — 단말 진단을 위해 release 빌드에서도 노출. `LLM 응답 실패: \n\n--- STACK ---\n` 형식. SelectableText + monospace + 최대 화면 1/3 높이 + scroll. 사용자 친화 메시지로 좁히는 작업은 #342 종료 후 follow-up. diff --git a/app/lib/ui/screens/chat_screen.dart b/app/lib/ui/screens/chat_screen.dart index 597d335..3b832c0 100644 --- a/app/lib/ui/screens/chat_screen.dart +++ b/app/lib/ui/screens/chat_screen.dart @@ -132,24 +132,31 @@ class _ChatScreenState extends ConsumerState { ), ), Expanded( - child: ListView.builder( - controller: _scrollCtrl, - padding: const EdgeInsets.all(12), - itemCount: state.messages.length + - (state.streamingText != null && - state.streamingText!.isNotEmpty - ? 1 - : 0), - itemBuilder: (context, i) { - if (i < state.messages.length) { - return _MessageBubble(message: state.messages[i]); - } - return _MessageBubble( - message: ModelChatMessage(state.streamingText ?? ''), - streaming: true, - ); - }, - ), + child: state.messages.isEmpty && state.streamingText == null + ? _EmptyChatHint(onPickPrompt: (p) { + _textCtrl.text = p; + _textCtrl.selection = TextSelection.fromPosition( + TextPosition(offset: p.length), + ); + }) + : ListView.builder( + controller: _scrollCtrl, + padding: const EdgeInsets.all(12), + itemCount: state.messages.length + + (state.streamingText != null && + state.streamingText!.isNotEmpty + ? 1 + : 0), + itemBuilder: (context, i) { + if (i < state.messages.length) { + return _MessageBubble(message: state.messages[i]); + } + return _MessageBubble( + message: ModelChatMessage(state.streamingText ?? ''), + streaming: true, + ); + }, + ), ), const Divider(height: 1), Padding( @@ -248,6 +255,73 @@ class _WarmupErrorBanner extends ConsumerWidget { } } +/// 첫 진입 시 빈 대화창 안내. 사용자가 무엇을 물어볼 수 있는지 +/// 예시 prompt 4개 + tool 카테고리 짧은 안내. tap 하면 입력창에 채워지고 +/// 사용자가 보내기만 누르면 됨 (자동 send X — 사용자가 문구 수정 여지). +class _EmptyChatHint extends StatelessWidget { + final ValueChanged onPickPrompt; + const _EmptyChatHint({required this.onPickPrompt}); + + static const _examples = [ + '아침 햇빛 받기 습관 추가해줘', + '오늘 운동 했어', + '내 스트릭 보여줘', + '수면 프로토콜 알려줘', + ]; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.smart_toy_outlined, + size: 48, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + 'AI 코치', + style: theme.textTheme.titleLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '습관 추가·기록 · 카탈로그 검색 · 스트릭 조회를 도와드려요.', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Text( + '예시', + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + ..._examples.map((p) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: OutlinedButton( + onPressed: () => onPickPrompt(p), + style: OutlinedButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + ), + child: Text(p), + ), + )), + ], + ), + ); + } +} + /// Human-friendly Korean labels for the 6 tools registered in /// `ToolRegistry.defaults()`. Falls back to the raw tool name for any /// future tool that hasn't been mapped yet — better to show the raw id diff --git a/app/lib/ui/screens/check_in_screen.dart b/app/lib/ui/screens/check_in_screen.dart index 964719e..d878327 100644 --- a/app/lib/ui/screens/check_in_screen.dart +++ b/app/lib/ui/screens/check_in_screen.dart @@ -50,7 +50,7 @@ class _CheckInScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('오늘 (${_ymd(nowKst())})', + Text('오늘 · ${_koreanDate(nowKst())}', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), const SizedBox(height: 32), @@ -80,3 +80,9 @@ String _ymd(DateTime d) => '${d.year.toString().padLeft(4, '0')}-' '${d.month.toString().padLeft(2, '0')}-' '${d.day.toString().padLeft(2, '0')}'; + +/// 사용자 노출용 한국식 날짜 — '6월 15일 (월)'. DB 저장은 _ymd 가 담당. +String _koreanDate(DateTime d) { + const weekdays = ['월', '화', '수', '목', '금', '토', '일']; + return '${d.month}월 ${d.day}일 (${weekdays[d.weekday - 1]})'; +} diff --git a/app/lib/ui/screens/habit_create_screen.dart b/app/lib/ui/screens/habit_create_screen.dart index bbaaea5..e64f7e5 100644 --- a/app/lib/ui/screens/habit_create_screen.dart +++ b/app/lib/ui/screens/habit_create_screen.dart @@ -104,11 +104,16 @@ class _HabitCreateScreenState extends ConsumerState { DropdownButtonFormField( initialValue: _level, items: const [ - DropdownMenuItem(value: FrameLevel.l2, child: Text('L2 · 조건부 긍정')), - DropdownMenuItem(value: FrameLevel.l3, child: Text('L3 · 정체성')), + DropdownMenuItem( + value: FrameLevel.l2, child: Text('조건부 행동 (예: 아침에 햇빛 받기)')), + DropdownMenuItem( + value: FrameLevel.l3, child: Text('정체성 (예: 나는 일찍 자는 사람)')), ], onChanged: (v) => setState(() => _level = v ?? FrameLevel.l2), - decoration: const InputDecoration(labelText: '프레임 레벨'), + decoration: const InputDecoration( + labelText: '표현 방식', + helperText: '문구를 어떻게 적을지 — 행동 위주 vs 정체성 위주', + ), ), const SizedBox(height: 16), TextFormField( diff --git a/docs/design/342-v042-hotfix/README.md b/docs/design/342-v042-hotfix/README.md index 1360bf4..1e6a52b 100644 --- a/docs/design/342-v042-hotfix/README.md +++ b/docs/design/342-v042-hotfix/README.md @@ -18,10 +18,10 @@ v0.4.1 실 단말 (Android, 사용자 본인) 첫 테스트에서 발견된 사 - 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. - - 8 화면 UX 전체 — 남은 P1/P2 (chat 빈 상태, check_in 날짜 포맷, 프레임 레벨 라벨 명확화) 는 v0.4.3 또는 후속 이슈로. ## 3. 인수조건 (Acceptance Criteria) - [x] **AC-A1** Pixel/Galaxy gesture nav 단말에서 입력창 + send 버튼이 nav bar 와 겹치지 않음. @@ -32,6 +32,9 @@ v0.4.1 실 단말 (Android, 사용자 본인) 첫 테스트에서 발견된 사 - [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. 컨텍스트 & 제약 @@ -105,7 +108,6 @@ habit_list_screen / streak_screen / habit_create_screen ## 10. 후속 (v0.4.3 또는 별개 이슈) - `BackendInitException: model may be invalid` 진단/수정 — `maxTokens=2048` 의 GPU buffer 후보. 단말 빌드 비용 때문에 분리. -- 남은 UX P1/P2 — chat 빈 상태 안내, check_in 한국식 날짜, 프레임 레벨 라벨 명확화. - release 빌드에서 stack 숨김 (사용자 친화 메시지로). ## 11. 추적성