- 설정 화면에 SMS 권한 카드 추가: 상태 표시(허용/미허용/영구 거부), 권한 요청/설정 이동 지원\n- 기존 알림 권한 카드 스타일과 일관성 유지 feat(permissions): 최초 실행 시 SMS 권한 온보딩 화면 추가 및 Splash에서 라우팅 (Android) - 권한 필요 이유/수집 범위 현지화 문구 추가\n- 거부/영구거부 케이스 처리 및 설정 이동 chore(codex): AGENTS.md/체크 스크립트/CI/프롬프트 템플릿 추가 - AGENTS.md, scripts/check.sh, scripts/fix.sh, .github/workflows/flutter_ci.yml, .claude/agents/codex.md, 문서 템플릿 추가 refactor(logging): 경로별 print 제거 후 경량 로거(Log) 도입 - SMS 스캐너/컨트롤러, URL 매처, 데이터 리포지토리, 내비게이션, 메모리/성능 유틸 등 핵심 경로 치환 feat(exchange): 환율 API URL을 --dart-define로 오버라이드 가능 + 폴백 로깅 강화 test: URL 매처/환율 스모크 테스트 추가 chore(android): RECEIVE_SMS 권한 제거 (READ_SMS만 유지) fix(lints): dart fix + 수동 정리로 경고 대폭 감소, 비동기 context(mounted) 보강 fix(deprecations):\n- flutter_local_notifications의 androidAllowWhileIdle → androidScheduleMode 전환\n- WillPopScope → PopScope 교체 i18n: SMS 권한 온보딩/설정 문구 현지화 키 추가
205 lines
6.3 KiB
Dart
205 lines
6.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../screens/category_management_screen.dart';
|
|
import '../screens/app_lock_screen.dart';
|
|
import '../models/subscription_model.dart';
|
|
import '../providers/navigation_provider.dart';
|
|
import '../routes/app_routes.dart';
|
|
import '../utils/logger.dart';
|
|
import 'animated_page_transitions.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
/// 앱 전체의 네비게이션을 관리하는 클래스
|
|
class AppNavigator {
|
|
// NavigationProvider를 사용하여 상태를 관리하므로 더 이상 싱글톤 패턴이 필요하지 않음
|
|
|
|
/// 홈으로 네비게이션
|
|
static Future<void> toHome(BuildContext context) async {
|
|
HapticFeedback.lightImpact();
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
navigationProvider.clearHistoryAndGoHome();
|
|
|
|
await Navigator.of(context).pushNamedAndRemoveUntil(
|
|
AppRoutes.main,
|
|
(route) => false,
|
|
);
|
|
}
|
|
|
|
/// 분석 화면으로 네비게이션
|
|
static Future<void> toAnalysis(BuildContext context) async {
|
|
HapticFeedback.lightImpact();
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
navigationProvider.updateCurrentIndex(1);
|
|
|
|
await Navigator.of(context).pushNamed(AppRoutes.analysis);
|
|
}
|
|
|
|
/// 구독 추가 화면으로 네비게이션
|
|
static Future<void> toAddSubscription(BuildContext context) async {
|
|
HapticFeedback.mediumImpact();
|
|
|
|
await Navigator.of(context).pushNamed(AppRoutes.addSubscription);
|
|
}
|
|
|
|
/// 구독 상세 화면으로 네비게이션
|
|
static Future<void> toDetail(
|
|
BuildContext context, SubscriptionModel subscription) async {
|
|
Log.d('AppNavigator.toDetail 호출됨: ${subscription.serviceName}');
|
|
HapticFeedback.lightImpact();
|
|
|
|
try {
|
|
await Navigator.of(context).pushNamed(
|
|
AppRoutes.subscriptionDetail,
|
|
arguments: subscription,
|
|
);
|
|
Log.d('DetailScreen 네비게이션 성공');
|
|
} catch (e) {
|
|
Log.e('DetailScreen 네비게이션 오류', e);
|
|
}
|
|
}
|
|
|
|
/// SMS 스캔 화면으로 네비게이션
|
|
static Future<void> toSmsScan(BuildContext context) async {
|
|
HapticFeedback.lightImpact();
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
navigationProvider.updateCurrentIndex(3);
|
|
|
|
await Navigator.of(context).pushNamed(AppRoutes.smsScanner);
|
|
}
|
|
|
|
/// 설정 화면으로 네비게이션
|
|
static Future<void> toSettings(BuildContext context) async {
|
|
HapticFeedback.lightImpact();
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
navigationProvider.updateCurrentIndex(4);
|
|
|
|
await Navigator.of(context).pushNamed(AppRoutes.settings);
|
|
}
|
|
|
|
/// 카테고리 관리 화면으로 네비게이션
|
|
static Future<void> toCategoryManagement(BuildContext context) async {
|
|
HapticFeedback.lightImpact();
|
|
|
|
await Navigator.of(context).push(
|
|
SlidePageRoute(
|
|
page: const CategoryManagementScreen(),
|
|
direction: AxisDirection.up,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 앱 잠금 화면으로 네비게이션
|
|
static Future<void> toAppLock(BuildContext context) async {
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => const AppLockScreen(),
|
|
fullscreenDialog: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 뒤로가기 처리
|
|
static Future<bool> handleBackButton(BuildContext context) async {
|
|
final navigator = Navigator.of(context);
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
|
|
// 네비게이션 스택이 있으면 팝
|
|
if (navigator.canPop()) {
|
|
HapticFeedback.lightImpact();
|
|
|
|
// NavigationProvider의 히스토리를 사용하여 이전 인덱스로 복원
|
|
if (navigationProvider.canPop()) {
|
|
navigationProvider.pop();
|
|
}
|
|
|
|
navigator.pop();
|
|
return false;
|
|
}
|
|
|
|
// 앱 종료 확인
|
|
final shouldExit = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(AppLocalizations.of(context).exitApp),
|
|
content: Text(AppLocalizations.of(context).exitAppConfirm),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: Text(AppLocalizations.of(context).cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: Text(AppLocalizations.of(context).exit),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return shouldExit ?? false;
|
|
}
|
|
|
|
/// 플로팅 네비게이션 바 탭 처리
|
|
static void handleFloatingNavTap(BuildContext context, int index) {
|
|
final navigationProvider = context.read<NavigationProvider>();
|
|
final currentIndex = navigationProvider.currentIndex;
|
|
|
|
// 같은 탭을 다시 탭하면 아무 동작 안 함
|
|
if (currentIndex == index) {
|
|
return;
|
|
}
|
|
|
|
// 현재 화면이 메인이 아니면 먼저 메인으로 돌아가기
|
|
if (Navigator.of(context).canPop()) {
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
}
|
|
|
|
// 선택된 인덱스에 따라 네비게이션
|
|
switch (index) {
|
|
case 0: // 홈
|
|
navigationProvider.updateCurrentIndex(0);
|
|
break;
|
|
case 1: // 분석
|
|
toAnalysis(context);
|
|
break;
|
|
case 2: // 추가
|
|
toAddSubscription(context);
|
|
break;
|
|
case 3: // SMS
|
|
toSmsScan(context);
|
|
break;
|
|
case 4: // 설정
|
|
toSettings(context);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 네비게이션 관찰자 (디버깅용)
|
|
class AppNavigationObserver extends NavigatorObserver {
|
|
@override
|
|
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
super.didPush(route, previousRoute);
|
|
debugPrint('Navigation: Push ${route.settings.name}');
|
|
}
|
|
|
|
@override
|
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
super.didPop(route, previousRoute);
|
|
debugPrint('Navigation: Pop ${route.settings.name}');
|
|
}
|
|
|
|
@override
|
|
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
super.didRemove(route, previousRoute);
|
|
debugPrint('Navigation: Remove ${route.settings.name}');
|
|
}
|
|
|
|
@override
|
|
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
|
|
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
|
|
debugPrint(
|
|
'Navigation: Replace ${oldRoute?.settings.name} with ${newRoute?.settings.name}');
|
|
}
|
|
}
|