주석화 진행상황 정리하고 핵심 모듈에 한글 주석 추가
This commit is contained in:
@@ -18,6 +18,7 @@ class Environment {
|
||||
/// 프로덕션 여부
|
||||
static late final bool isProduction;
|
||||
|
||||
/// 환경 변수에서 파싱한 리소스별 권한 집합.
|
||||
static final Map<String, Set<String>> _permissions = {};
|
||||
|
||||
/// 환경 초기화
|
||||
@@ -74,6 +75,7 @@ class Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// `.env` 파일에서 `PERMISSION__*` 키를 파싱해 권한 맵을 구성한다.
|
||||
static void _loadPermissions() {
|
||||
_permissions.clear();
|
||||
for (final entry in dotenv.env.entries) {
|
||||
@@ -82,6 +84,7 @@ class Environment {
|
||||
continue;
|
||||
}
|
||||
final resource = entry.key.substring(prefix.length).toLowerCase();
|
||||
// 콤마 구분 문자열을 소문자/trim 처리해 비교를 일관되게 맞춘다.
|
||||
final values = entry.value
|
||||
.split(',')
|
||||
.map((token) => token.trim().toLowerCase())
|
||||
@@ -91,12 +94,14 @@ class Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// 환경에 설정된 권한이 있는 경우 해당 액션 허용 여부를 반환한다.
|
||||
static bool hasPermission(String resource, String action) {
|
||||
final actions = _permissions[resource.toLowerCase()];
|
||||
if (actions == null || actions.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
if (actions.contains('all')) {
|
||||
// all 키워드는 모든 액션 허용을 의미한다.
|
||||
return true;
|
||||
}
|
||||
return actions.contains(action.toLowerCase());
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../services/token_storage.dart';
|
||||
|
||||
/// 갱신 토큰을 기반으로 새로운 토큰 쌍을 받아오는 비동기 콜백 시그니처.
|
||||
typedef RefreshTokenCallback = Future<TokenPair?> Function();
|
||||
|
||||
/// 액세스/리프레시 토큰 값을 함께 보관하기 위한 불변 모델.
|
||||
class TokenPair {
|
||||
/// [accessToken], [refreshToken]을 모두 전달받아 초기화한다.
|
||||
const TokenPair({required this.accessToken, required this.refreshToken});
|
||||
|
||||
/// 인증 헤더에 사용되는 액세스 토큰 문자열.
|
||||
final String accessToken;
|
||||
|
||||
/// 토큰 재발급 요청에 전달되는 리프레시 토큰 문자열.
|
||||
final String refreshToken;
|
||||
}
|
||||
|
||||
@@ -28,11 +32,17 @@ class AuthInterceptor extends Interceptor {
|
||||
|
||||
final TokenStorage _tokenStorage;
|
||||
final Dio _dio;
|
||||
|
||||
/// 서버에 토큰 재발급을 요청하는 콜백. null이면 갱신을 시도하지 않는다.
|
||||
final RefreshTokenCallback? onRefresh;
|
||||
|
||||
/// 동시에 들어온 요청을 순차적으로 처리하기 위한 대기열.
|
||||
final List<Completer<void>> _refreshQueue = [];
|
||||
|
||||
/// 현재 토큰 갱신 중인지 여부.
|
||||
bool _isRefreshing = false;
|
||||
|
||||
/// 요청 직전에 저장된 액세스 토큰을 Authorization 헤더에 주입한다.
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
@@ -45,6 +55,7 @@ class AuthInterceptor extends Interceptor {
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
/// 401 응답을 감지하면 토큰을 갱신하고 동일 요청을 한 번 재시도한다.
|
||||
@override
|
||||
Future<void> onError(
|
||||
DioException err,
|
||||
@@ -69,12 +80,14 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 토큰 갱신을 시도해야 하는 상황인지 판별한다.
|
||||
bool _shouldAttemptRefresh(DioException err) {
|
||||
return onRefresh != null &&
|
||||
err.response?.statusCode == 401 &&
|
||||
err.requestOptions.extra['__retry'] != true;
|
||||
}
|
||||
|
||||
/// 토큰을 갱신하고 대기 중인 요청을 깨운다.
|
||||
Future<void> _refreshToken() async {
|
||||
if (_isRefreshing) {
|
||||
final completer = Completer<void>();
|
||||
@@ -107,6 +120,7 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 최신 토큰을 헤더에 주입해 한 번만 동일 요청을 재시도한다.
|
||||
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
|
||||
final token = await _tokenStorage.readAccessToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
@@ -119,6 +133,7 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 토큰 재발급 실패 시 재시도 루프를 중단하기 위해 사용하는 내부 예외.
|
||||
class _RefreshFailedException implements Exception {
|
||||
const _RefreshFailedException();
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ class PermissionManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 리소스별 임시 권한 집합을 보관한다.
|
||||
final Map<String, Set<PermissionAction>> _overrides = {};
|
||||
|
||||
/// 지정한 리소스/행동이 허용되는지 여부를 반환한다.
|
||||
bool can(String resource, PermissionAction action) {
|
||||
final override = _overrides[resource];
|
||||
if (override != null) {
|
||||
// View 권한은 최소 접근을 허용하기 위해 별도로 처리한다.
|
||||
if (override.contains(PermissionAction.view) &&
|
||||
action == PermissionAction.view) {
|
||||
return true;
|
||||
@@ -45,6 +47,7 @@ class PermissionScope extends InheritedNotifier<PermissionManager> {
|
||||
required super.child,
|
||||
}) : super(notifier: manager);
|
||||
|
||||
/// 현재 빌드 컨텍스트에서 [PermissionManager]를 조회한다.
|
||||
static PermissionManager of(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<PermissionScope>();
|
||||
assert(
|
||||
|
||||
@@ -27,6 +27,10 @@ import '../constants/app_sections.dart';
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
|
||||
/// 애플리케이션 전체 라우팅 구성을 담당하는 GoRouter 인스턴스.
|
||||
///
|
||||
/// - 로그인 전용 라우트는 루트(primary) 네비게이터에서 처리한다.
|
||||
/// - 로그인 이후 화면은 [ShellRoute] 하위에서 `AppShell` 레이아웃을 공유한다.
|
||||
/// - 각 기능 모듈은 고유 name/path를 가지며 `GoRouter` 딥링크와 연결된다.
|
||||
final appRouter = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: loginRoutePath,
|
||||
|
||||
@@ -4,14 +4,19 @@ import 'token_storage_stub.dart'
|
||||
|
||||
/// 액세스/리프레시 토큰을 안전하게 보관하는 스토리지 인터페이스.
|
||||
abstract class TokenStorage {
|
||||
/// 액세스 토큰을 저장한다. null을 전달하면 기존 값을 제거한다.
|
||||
Future<void> writeAccessToken(String? token);
|
||||
|
||||
/// 저장된 액세스 토큰을 읽어온다. 없으면 null을 반환한다.
|
||||
Future<String?> readAccessToken();
|
||||
|
||||
/// 리프레시 토큰을 저장한다. null이면 값을 삭제한다.
|
||||
Future<void> writeRefreshToken(String? token);
|
||||
|
||||
/// 저장된 리프레시 토큰을 읽어온다. 없으면 null을 반환한다.
|
||||
Future<String?> readRefreshToken();
|
||||
|
||||
/// 저장 중인 모든 토큰 정보를 초기화한다.
|
||||
Future<void> clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import 'token_storage.dart';
|
||||
|
||||
/// 안전한 스토리지에 저장할 액세스 토큰 키.
|
||||
const _kAccessTokenKey = 'access_token';
|
||||
/// 안전한 스토리지에 저장할 리프레시 토큰 키.
|
||||
const _kRefreshTokenKey = 'refresh_token';
|
||||
|
||||
/// 모바일/데스크톱에서 [FlutterSecureStorage]를 사용하는 토큰 스토리지를 생성한다.
|
||||
TokenStorage buildTokenStorage() {
|
||||
const storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
@@ -16,6 +19,7 @@ TokenStorage buildTokenStorage() {
|
||||
return _SecureTokenStorage(storage);
|
||||
}
|
||||
|
||||
/// [FlutterSecureStorage] 기반 토큰 스토리지 구현체.
|
||||
class _SecureTokenStorage implements TokenStorage {
|
||||
const _SecureTokenStorage(this._storage);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'token_storage.dart';
|
||||
|
||||
/// 현재 플랫폼에서 지원되는 토큰 스토리지가 없을 때 사용하는 기본 구현을 생성한다.
|
||||
TokenStorage buildTokenStorage() => _UnsupportedTokenStorage();
|
||||
|
||||
/// 토큰 저장 기능이 제공되지 않는 환경에서 호출될 때 예외를 던지는 스텁.
|
||||
class _UnsupportedTokenStorage implements TokenStorage {
|
||||
Never _unsupported() {
|
||||
throw UnsupportedError('TokenStorage is not supported on this platform.');
|
||||
|
||||
@@ -3,11 +3,15 @@ import 'dart:html' as html;
|
||||
|
||||
import 'token_storage.dart';
|
||||
|
||||
/// 웹 로컬스토리지에 저장할 액세스 토큰 키.
|
||||
const _kAccessTokenKey = 'access_token';
|
||||
/// 웹 로컬스토리지에 저장할 리프레시 토큰 키.
|
||||
const _kRefreshTokenKey = 'refresh_token';
|
||||
|
||||
/// 웹 환경에서 로컬스토리지를 활용하는 토큰 스토리지를 생성한다.
|
||||
TokenStorage buildTokenStorage() => _WebTokenStorage(html.window.localStorage);
|
||||
|
||||
/// 브라우저 로컬스토리지에 토큰을 저장하는 구현체.
|
||||
class _WebTokenStorage implements TokenStorage {
|
||||
const _WebTokenStorage(this._storage);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
class SuperportShadTheme {
|
||||
const SuperportShadTheme._();
|
||||
|
||||
/// 브랜드 색상을 기준으로 정의한 주 색상 값.
|
||||
static const Color primaryColor = Color(0xFF1B4F87);
|
||||
static const Color successColor = Color(0xFF2E8B57);
|
||||
static const Color warningColor = Color(0xFFFFC107);
|
||||
@@ -16,6 +17,7 @@ class SuperportShadTheme {
|
||||
return ShadThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ShadColorScheme(
|
||||
// 라이트 모드 기본 배경/텍스트 대비값 설정.
|
||||
background: Color(0xFFFFFFFF),
|
||||
foreground: Color(0xFF09090B),
|
||||
card: Color(0xFFFFFFFF),
|
||||
@@ -94,6 +96,7 @@ class SuperportShadTheme {
|
||||
return ShadThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ShadColorScheme(
|
||||
// 다크 모드 대비를 위해 어두운 배경/밝은 텍스트 조합을 사용한다.
|
||||
background: Color(0xFF09090B),
|
||||
foreground: Color(0xFFFAFAFA),
|
||||
card: Color(0xFF09090B),
|
||||
@@ -168,6 +171,7 @@ class SuperportShadTheme {
|
||||
}
|
||||
|
||||
/// 상태 텍스트 배경을 위한 데코레이션을 반환한다.
|
||||
/// 상태 문자열을 기반으로 배경/테두리 색을 선택해 상태 배지를 표현한다.
|
||||
static BoxDecoration statusDecoration(String status) {
|
||||
Color backgroundColor;
|
||||
Color borderColor;
|
||||
|
||||
Reference in New Issue
Block a user