[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
This commit is contained in:
44
app/lib/ui/labels.dart
Normal file
44
app/lib/ui/labels.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import '../domain/models/habit.dart';
|
||||
import '../domain/streak/compute_streak.dart';
|
||||
|
||||
/// UI 한국어 라벨 매핑. domain enum 의 `dbValue` 는 DB 직렬화용이므로
|
||||
/// 사용자에게 그대로 노출하면 'build', 'L3', 'T0' 같은 raw 식별자가
|
||||
/// 그대로 보인다. 본 헬퍼는 그걸 한국어 표현으로 바꾼다.
|
||||
|
||||
String habitTypeLabel(HabitType t) {
|
||||
switch (t) {
|
||||
case HabitType.build:
|
||||
return '만들기';
|
||||
case HabitType.breakHabit:
|
||||
return '없애기';
|
||||
}
|
||||
}
|
||||
|
||||
/// Drift row (raw db String) 에서 직접 매핑. 'build' / 'break' 외의 값은
|
||||
/// 그대로 노출해 invariant 위반을 가시화.
|
||||
String habitTypeLabelFromDb(String dbValue) {
|
||||
switch (dbValue) {
|
||||
case 'build':
|
||||
return '만들기';
|
||||
case 'break':
|
||||
return '없애기';
|
||||
default:
|
||||
return dbValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// 5-Tier Reward Ladder (T0 새싹 → T4 6주 완주). milestone 누적 보상.
|
||||
String rewardTierLabel(RewardTier t) {
|
||||
switch (t) {
|
||||
case RewardTier.t0:
|
||||
return '🌱 새싹';
|
||||
case RewardTier.t1:
|
||||
return '🥉 3회 도전';
|
||||
case RewardTier.t2:
|
||||
return '🥈 7일 형성';
|
||||
case RewardTier.t3:
|
||||
return '🥇 30일 정착';
|
||||
case RewardTier.t4:
|
||||
return '🏆 6주 완주';
|
||||
}
|
||||
}
|
||||
@@ -94,9 +94,8 @@ class _HabitCreateScreenState extends ConsumerState<HabitCreateScreen> {
|
||||
DropdownButtonFormField<HabitType>(
|
||||
initialValue: _type,
|
||||
items: const [
|
||||
DropdownMenuItem(value: HabitType.build, child: Text('만들기 (build)')),
|
||||
DropdownMenuItem(
|
||||
value: HabitType.breakHabit, child: Text('없애기 (break)')),
|
||||
DropdownMenuItem(value: HabitType.build, child: Text('만들기')),
|
||||
DropdownMenuItem(value: HabitType.breakHabit, child: Text('없애기')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _type = v ?? HabitType.build),
|
||||
decoration: const InputDecoration(labelText: '타입'),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../state/ai_providers.dart';
|
||||
import '../../state/providers.dart';
|
||||
import '../labels.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'check_in_screen.dart';
|
||||
import 'habit_create_screen.dart';
|
||||
@@ -85,8 +86,10 @@ class HabitListScreen extends ConsumerWidget {
|
||||
final h = habits[i];
|
||||
return ListTile(
|
||||
title: Text(h.title),
|
||||
// FrameLevel (L2/L3) 은 시스템 규약이라 사용자에게 노출
|
||||
// 가치 낮음 — type chip + framedText 만 표시.
|
||||
subtitle: Text(
|
||||
'${h.type} · ${h.frameLevel} · ${h.frameFramedText}',
|
||||
'${habitTypeLabelFromDb(h.type)} · ${h.frameFramedText}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../core/time.dart';
|
||||
import '../../domain/models/tracker_entry.dart';
|
||||
import '../../domain/streak/compute_streak.dart';
|
||||
import '../../state/providers.dart';
|
||||
import '../labels.dart';
|
||||
|
||||
class StreakScreen extends ConsumerWidget {
|
||||
final String habitId;
|
||||
@@ -51,25 +52,43 @@ class StreakScreen extends ConsumerWidget {
|
||||
asOf: nowKst(),
|
||||
habitStartedAt: habit.startedAt as String,
|
||||
);
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(habit.title as String,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 24),
|
||||
_Row('현재 스트릭', '${state.currentStreak}일'),
|
||||
// Hero — 핵심 동기 지표. 큰 숫자 + 티어 emoji 라벨로 위계 강조.
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${state.currentStreak}',
|
||||
style: theme.textTheme.displayLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Text('일 연속 (현재 스트릭)',
|
||||
style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(rewardTierLabel(state.currentTier),
|
||||
style: theme.textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 40),
|
||||
_Row('최장 스트릭', '${state.longestStreak}일'),
|
||||
_Row('최근 30일 / 완료', '${state.doneCountInWindow30}회'),
|
||||
_Row('Phase 42일 / 완료', '${state.doneCountInPhase42}회'),
|
||||
const Divider(height: 32),
|
||||
_Row('현재 티어', state.currentTier.dbValue),
|
||||
if (state.neverMissTwiceBroken)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
'⚠ Never miss twice 발동 — 티어 강등',
|
||||
'⚠ 이틀 연속 빠졌어요. 한 단계 강등됐습니다.',
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user