110 lines
3.8 KiB
Dart
110 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../features/auth/auth_repository.dart';
|
|
import '../../features/auth/login_screen.dart';
|
|
import '../../features/galaxy/galaxy_screen.dart';
|
|
import '../../features/home/home_screen.dart';
|
|
import '../../features/inside/inside_screen.dart';
|
|
import '../../features/my/my_screen.dart';
|
|
import '../../features/station/station_screen.dart';
|
|
|
|
/// 하단 탭 하나.
|
|
class AppTab {
|
|
const AppTab({required this.label, required this.path, required this.icon});
|
|
|
|
/// **계약 고정 문자열.** 한글화·치환·순서 변경 금지.
|
|
final String label;
|
|
final String path;
|
|
final IconData icon;
|
|
}
|
|
|
|
/// 계약 고정 5탭.
|
|
///
|
|
/// 근거 3개가 서로 일치한다:
|
|
/// - 견적서 v2.1 「5탭 네비게이션」
|
|
/// - `design/craft-allowlist.yaml` → `APP_TABS: [HOME, INSIDE, GALAXY, STATION, MY]`
|
|
/// - 시안 `시안/screens/S-*.html` 의 `<nav class="tabbar">` (5개 루트 화면 전부 동일)
|
|
///
|
|
/// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다.
|
|
/// 아이콘은 아직 확정 자산이 없어 Material 기본 아이콘을 쓴다(README 「미투입 항목」).
|
|
const List<AppTab> appTabs = [
|
|
AppTab(label: 'HOME', path: '/home', icon: Icons.checklist_rtl),
|
|
AppTab(label: 'INSIDE', path: '/inside', icon: Icons.forum_outlined),
|
|
AppTab(label: 'GALAXY', path: '/galaxy', icon: Icons.public),
|
|
AppTab(label: 'STATION', path: '/station', icon: Icons.storefront_outlined),
|
|
AppTab(label: 'MY', path: '/my', icon: Icons.person_outline),
|
|
];
|
|
|
|
const String loginPath = '/login';
|
|
|
|
/// 5탭 셸 라우트 + 온보딩(로그인) 라우트.
|
|
///
|
|
/// 온보딩 나머지 화면(S-A-INTRO · S-A-TERMS · S-A-PROFILE · S-A-ARTIST ·
|
|
/// S-A-PLANET-NEW · S-A-PERMS)은 3주차 범위라 아직 라우트가 없다.
|
|
GoRouter buildRouter(AuthRepository auth) => GoRouter(
|
|
initialLocation: appTabs.first.path,
|
|
refreshListenable: auth,
|
|
redirect: (context, state) {
|
|
final atLogin = state.matchedLocation == loginPath;
|
|
if (!auth.isSignedIn) return atLogin ? null : loginPath;
|
|
if (atLogin) return appTabs.first.path;
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(path: loginPath, builder: (_, _) => LoginScreen(auth: auth)),
|
|
StatefulShellRoute.indexedStack(
|
|
builder: (_, _, shell) => AppShell(shell: shell),
|
|
branches: [
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(path: appTabs[0].path, builder: (_, _) => const HomeScreen()),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(path: appTabs[1].path, builder: (_, _) => const InsideScreen()),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(path: appTabs[2].path, builder: (_, _) => const GalaxyScreen()),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(path: appTabs[3].path, builder: (_, _) => const StationScreen()),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(path: appTabs[4].path, builder: (_, _) => const MyScreen()),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
/// 5탭 셸. 탭별 상태는 [StatefulNavigationShell] 이 유지한다.
|
|
class AppShell extends StatelessWidget {
|
|
const AppShell({super.key, required this.shell});
|
|
|
|
final StatefulNavigationShell shell;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
body: shell,
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: shell.currentIndex,
|
|
// initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다.
|
|
onDestinationSelected: (i) =>
|
|
shell.goBranch(i, initialLocation: i == shell.currentIndex),
|
|
destinations: [
|
|
for (final tab in appTabs)
|
|
NavigationDestination(icon: Icon(tab.icon), label: tab.label),
|
|
],
|
|
),
|
|
);
|
|
}
|