feat(ui): 반응형 레이아웃 및 저장 시스템 개선

## 반응형 레이아웃
- app.dart: 화면 크기별 레이아웃 분기 로직 추가 (+173 라인)
- game_play_screen.dart: 반응형 UI 구조 개선
- layouts/, pages/ 디렉토리 추가 (새 레이아웃 시스템)
- carousel_nav_bar.dart: 캐러셀 네비게이션 바 추가
- enhanced_animation_panel.dart: 향상된 애니메이션 패널

## 저장 시스템
- save_manager.dart: 저장 관리 기능 확장
- save_repository.dart: 저장소 인터페이스 개선
- save_service.dart: 저장 서비스 로직 추가

## UI 개선
- notification_service.dart: 알림 시스템 기능 확장
- notification_overlay.dart: 오버레이 UI 개선
- equipment_stats_panel.dart: 장비 스탯 패널 개선
- cinematic_view.dart: 시네마틱 뷰 개선
- new_character_screen.dart: 캐릭터 생성 화면 개선

## 다국어
- game_text_l10n.dart: 텍스트 추가 (+182 라인)

## 테스트
- 관련 테스트 파일 업데이트
This commit is contained in:
JiWoong Sul
2025-12-23 17:52:43 +09:00
parent 1da6fa7a2b
commit e6af7dd91a
28 changed files with 2734 additions and 73 deletions

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
/// 캐로셀 페이지 인덱스
enum CarouselPage {
skills, // 0: 스킬
inventory, // 1: 인벤토리
equipment, // 2: 장비
character, // 3: 캐릭터시트 (기본)
combatLog, // 4: 전투로그
quest, // 5: 퀘스트
story, // 6: 스토리
}
/// 캐로셀 네비게이션 바
///
/// 7개의 페이지 버튼을 표시하고 현재 페이지를 하이라이트.
/// 버튼 탭 시 해당 페이지로 이동.
class CarouselNavBar extends StatelessWidget {
const CarouselNavBar({
super.key,
required this.currentPage,
required this.onPageSelected,
});
final int currentPage;
final ValueChanged<int> onPageSelected;
@override
Widget build(BuildContext context) {
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
),
child: Row(
children: CarouselPage.values.map((page) {
final isSelected = page.index == currentPage;
return Expanded(
child: _NavButton(
page: page,
isSelected: isSelected,
onTap: () => onPageSelected(page.index),
),
);
}).toList(),
),
);
}
}
/// 개별 네비게이션 버튼
class _NavButton extends StatelessWidget {
const _NavButton({
required this.page,
required this.isSelected,
required this.onTap,
});
final CarouselPage page;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final (icon, label) = _getIconAndLabel(page);
final theme = Theme.of(context);
final color = isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: isSelected
? BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.circular(8),
)
: null,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20, color: color),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
/// 페이지별 아이콘과 라벨
(IconData, String) _getIconAndLabel(CarouselPage page) {
return switch (page) {
CarouselPage.skills => (Icons.auto_fix_high, l10n.navSkills),
CarouselPage.inventory => (Icons.inventory_2, l10n.navInventory),
CarouselPage.equipment => (Icons.shield, l10n.navEquipment),
CarouselPage.character => (Icons.person, l10n.navCharacter),
CarouselPage.combatLog => (Icons.list_alt, l10n.navCombatLog),
CarouselPage.story => (Icons.auto_stories, l10n.navStory),
CarouselPage.quest => (Icons.flag, l10n.navQuest),
};
}
}