초기 커밋

This commit is contained in:
2026-03-01 07:55:59 +09:00
commit b0262d6bab
67 changed files with 4660 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../features/admin/presentation/screens/admin_home_screen.dart';
import '../../features/admin/presentation/screens/system_settings_screen.dart';
import '../../features/admin/presentation/screens/user_management_screen.dart';
import '../../features/auth/presentation/screens/login_screen.dart';
import '../../features/auth/presentation/screens/register_screen.dart';
import '../../features/dashboard/presentation/screens/dashboard_screen.dart';
import '../../features/user/presentation/screens/user_home_screen.dart';
import '../../features/user/presentation/screens/user_profile_screen.dart';
import '../../shared/providers/auth_provider.dart';
import '../../shared/widgets/app_scaffold.dart';
import 'auth_guard.dart';
import 'route_names.dart';
part 'app_router.g.dart';
@Riverpod(keepAlive: true)
GoRouter appRouter(Ref ref) {
final authGuard = AuthGuard(ref);
final authState = ref.watch(authStateProvider);
return GoRouter(
initialLocation: RoutePaths.login,
debugLogDiagnostics: true,
refreshListenable: _GoRouterRefreshStream(ref, authState),
redirect: (context, state) {
final location = state.uri.toString();
return authGuard.redirect(context, location);
},
routes: [
// Auth Routes (비인증)
GoRoute(
name: RouteNames.login,
path: RoutePaths.login,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
name: RouteNames.register,
path: RoutePaths.register,
builder: (context, state) => const RegisterScreen(),
),
// User Shell Route
ShellRoute(
builder: (context, state, child) => AppScaffold(
currentPath: state.uri.toString(),
child: child,
),
routes: [
GoRoute(
name: RouteNames.userHome,
path: RoutePaths.userHome,
builder: (context, state) => const UserHomeScreen(),
),
GoRoute(
name: RouteNames.userProfile,
path: RoutePaths.userProfile,
builder: (context, state) => const UserProfileScreen(),
),
GoRoute(
name: RouteNames.userDashboard,
path: RoutePaths.userDashboard,
builder: (context, state) => const DashboardScreen(),
),
],
),
// Admin Shell Route
ShellRoute(
builder: (context, state, child) => AppScaffold(
currentPath: state.uri.toString(),
isAdmin: true,
child: child,
),
routes: [
GoRoute(
name: RouteNames.adminHome,
path: RoutePaths.adminHome,
builder: (context, state) => const AdminHomeScreen(),
),
GoRoute(
name: RouteNames.adminUsers,
path: RoutePaths.adminUsers,
builder: (context, state) => const UserManagementScreen(),
),
GoRoute(
name: RouteNames.adminDashboard,
path: RoutePaths.adminDashboard,
builder: (context, state) => const DashboardScreen(),
),
GoRoute(
name: RouteNames.adminSettings,
path: RoutePaths.adminSettings,
builder: (context, state) => const SystemSettingsScreen(),
),
],
),
],
);
}
/// GoRouter에서 Riverpod 상태 변경 시 리프레시하기 위한 Listenable 래퍼
class _GoRouterRefreshStream extends ChangeNotifier {
_GoRouterRefreshStream(this.ref, dynamic _) {
// authState 변경 시 GoRouter refresh 트리거
notifyListeners();
}
final Ref ref;
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../shared/models/user_role.dart';
import '../../shared/providers/auth_provider.dart';
import 'route_names.dart';
/// 인증/역할 기반 라우트 가드
class AuthGuard {
const AuthGuard(this.ref);
final Ref ref;
/// GoRouter redirect 콜백
String? redirect(BuildContext context, String location) {
final authState = ref.read(authStateProvider);
return authState.when(
data: (user) {
final isLoggedIn = user != null;
final isAuthRoute =
location.startsWith(RoutePaths.login) ||
location.startsWith(RoutePaths.register);
// 비로그인 사용자가 인증 페이지 외 접근 시 → 로그인으로
if (!isLoggedIn && !isAuthRoute) {
return RoutePaths.login;
}
// 로그인 사용자가 인증 페이지 접근 시 → 역할에 따라 리다이렉트
if (isLoggedIn && isAuthRoute) {
return _redirectByRole(user!.role);
}
// 역할 기반 접근 제어
if (isLoggedIn) {
return _checkRoleAccess(location, user!.role);
}
return null;
},
loading: () => null,
error: (_, __) => RoutePaths.login,
);
}
/// 역할에 따른 기본 페이지 리다이렉트
String _redirectByRole(UserRole role) {
return switch (role) {
UserRole.admin => RoutePaths.adminHome,
UserRole.user => RoutePaths.userHome,
};
}
/// 역할 기반 접근 제어
String? _checkRoleAccess(String location, UserRole role) {
// 일반 사용자가 관리자 페이지 접근 시도
if (location.startsWith(RoutePaths.admin) && role != UserRole.admin) {
return RoutePaths.userHome;
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
/// 라우트 이름 상수
abstract final class RouteNames {
// Auth
static const String login = 'login';
static const String register = 'register';
// User
static const String userShell = 'user-shell';
static const String userHome = 'user-home';
static const String userProfile = 'user-profile';
static const String userDashboard = 'user-dashboard';
// Admin
static const String adminShell = 'admin-shell';
static const String adminHome = 'admin-home';
static const String adminUsers = 'admin-users';
static const String adminDashboard = 'admin-dashboard';
static const String adminSettings = 'admin-settings';
}
/// 라우트 경로 상수
abstract final class RoutePaths {
// Auth
static const String login = '/login';
static const String register = '/register';
// User
static const String user = '/user';
static const String userHome = '/user/home';
static const String userProfile = '/user/profile';
static const String userDashboard = '/user/dashboard';
// Admin
static const String admin = '/admin';
static const String adminHome = '/admin/home';
static const String adminUsers = '/admin/users';
static const String adminDashboard = '/admin/dashboard';
static const String adminSettings = '/admin/settings';
}