퀘스트 일시 정지 · 이어하기 · 진행 상태 서버 저장 · 목록 보상받기

This commit is contained in:
Sn-Kinos
2026-10-06 03:20:46 +09:00
parent ecf5998725
commit abe5f1ae50
28 changed files with 1023 additions and 118 deletions
+56 -38
View File
@@ -14,6 +14,8 @@ import '../../features/home/coach_mark.dart';
import '../../features/home/home_screen.dart';
import '../../features/inside/inside_screen.dart';
import '../../features/my/my_screen.dart';
import '../../features/quest/quest_data.dart' show QuestRepository;
import '../../features/quest/widgets/paused_quest_capsule.dart';
import '../../features/station/station_screen.dart';
/// 하단 탭 하나.
@@ -149,7 +151,8 @@ GoRouter buildRouter(AuthRepository auth, {Repositories? repositories}) {
builder: (_, _) => SignupCompleteScreen(onStart: auth.finishSignup),
),
StatefulShellRoute.indexedStack(
builder: (_, _, shell) => AppShell(shell: shell, flags: repos.flags),
builder: (_, _, shell) =>
AppShell(shell: shell, flags: repos.flags, quests: repos.quest),
branches: [
StatefulShellBranch(
observers: [HomeRefreshObserver()],
@@ -238,11 +241,19 @@ GoRouter buildRouter(AuthRepository auth, {Repositories? repositories}) {
/// 사라져 딥링크·기존 경로가 죽고, 플래그를 켤 때 재배포가 필요해진다. 여기서는
/// 하단 바에 그리는 목적지만 줄이고, 눌린 위치를 원래 브랜치 번호로 되돌린다.
class AppShell extends StatelessWidget {
const AppShell({super.key, required this.shell, required this.flags});
const AppShell({
super.key,
required this.shell,
required this.flags,
this.quests,
});
final StatefulNavigationShell shell;
final FeatureFlags flags;
/// 일시 정지 Quest 캡슐(원장 C-83)을 읽는 저장소. null 이면 캡슐이 없다.
final QuestRepository? quests;
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: flags,
@@ -254,43 +265,50 @@ class AppShell extends StatelessWidget {
return Scaffold(
body: shell,
// Figma 탭 줄은 좌우 16 을 비우고 칸을 나눈다(350:215853).
bottomNavigationBar: ColoredBox(
color: AppColors.cosmicVoid,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NavigationBar(
// 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다.
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (i) {
// 다른 탭에서 HOME 으로 돌아오면 다시 읽는다(HOME 은 탭 전환으로 다시 만들어지지 않는다).
if (branches[i] == 0 && shell.currentIndex != 0)
homeRefreshSignal.value++;
final again = branches[i] == shell.currentIndex;
shell.goBranch(
branches[i],
// initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다.
initialLocation: again,
);
// 설명 프레임 `2097:16427` 「HOME 화면에서 클릭 시, Y스크롤값 초기화」(INSIDE · GALAXY ·
// MY 동일) — 열려 있는 탭을 다시 누르면 그 탭 루트 화면을 맨 위로 올린다.
if (again) {
scrollTabToTop(
shell.route.branches[branches[i]].navigatorKey,
);
}
},
destinations: [
for (final tab in tabs)
NavigationDestination(
// Coach Mark ② 가 MY 위치를 가리킨다(coach_mark.dart).
icon: tab.path == appTabs.last.path
? KeyedSubtree(key: coachMyTabKey, child: tab.icon)
: tab.icon,
label: tab.label,
),
],
// 그 바로 위에 일시 정지 Quest 캡슐 한 줄(원장 C-83) — 없으면 높이 0.
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (quests case final repo?) PausedQuestCapsule(repository: repo),
ColoredBox(
color: AppColors.cosmicVoid,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NavigationBar(
// 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다.
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (i) {
// 다른 탭에서 HOME 으로 돌아오면 다시 읽는다(HOME 은 탭 전환으로 다시 만들어지지 않는다).
if (branches[i] == 0 && shell.currentIndex != 0)
homeRefreshSignal.value++;
final again = branches[i] == shell.currentIndex;
shell.goBranch(
branches[i],
// initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다.
initialLocation: again,
);
// 설명 프레임 `2097:16427` 「HOME 화면에서 클릭 시, Y스크롤값 초기화」(INSIDE · GALAXY ·
// MY 동일) — 열려 있는 탭을 다시 누르면 그 탭 루트 화면을 맨 위로 올린다.
if (again) {
scrollTabToTop(
shell.route.branches[branches[i]].navigatorKey,
);
}
},
destinations: [
for (final tab in tabs)
NavigationDestination(
// Coach Mark ② 가 MY 위치를 가리킨다(coach_mark.dart).
icon: tab.path == appTabs.last.path
? KeyedSubtree(key: coachMyTabKey, child: tab.icon)
: tab.icon,
label: tab.label,
),
],
),
),
),
),
],
),
);
},