환경 초기화 및 벤더 리포지토리 스켈레톤 도입
This commit is contained in:
65
lib/core/config/environment.dart
Normal file
65
lib/core/config/environment.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
/// 환경 설정 로더
|
||||
///
|
||||
/// - .env.development / .env.production 파일을 로드하여 런타임 설정을 주입한다.
|
||||
/// - `--dart-define=ENV=production` 형태로 빌드/실행 시 환경을 지정한다.
|
||||
/// - 주요 키: `API_BASE_URL`, `FEATURE_*` 플래그들.
|
||||
class Environment {
|
||||
Environment._();
|
||||
|
||||
/// 현재 환경명 (development | production)
|
||||
static late final String envName;
|
||||
|
||||
/// API 서버 베이스 URL
|
||||
static late final String baseUrl;
|
||||
|
||||
/// 프로덕션 여부
|
||||
static late final bool isProduction;
|
||||
|
||||
/// 환경 초기화
|
||||
///
|
||||
/// - 기본 환경은 development이며, `ENV` dart-define 으로 변경 가능
|
||||
/// - 해당 환경의 .env 파일을 로드하고 핵심 값을 추출한다.
|
||||
static Future<void> initialize() async {
|
||||
const envFromDefine = String.fromEnvironment('ENV', defaultValue: 'development');
|
||||
envName = envFromDefine.toLowerCase();
|
||||
isProduction = envName == 'production';
|
||||
|
||||
final fileName = '.env.$envName';
|
||||
try {
|
||||
await dotenv.load(fileName: fileName);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
// 개발 편의를 위해 파일 미존재 시 경고만 출력하고 진행
|
||||
// 실제 배포에서는 파일 존재가 필수다.
|
||||
// ignore: avoid_print
|
||||
print('[Environment] $fileName 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
baseUrl = dotenv.maybeGet('API_BASE_URL') ?? 'http://localhost:8080';
|
||||
}
|
||||
|
||||
/// 기능 플래그 조회 (기본 false)
|
||||
static bool flag(String key, {bool defaultValue = false}) {
|
||||
final v = dotenv.maybeGet(key);
|
||||
if (v == null) return defaultValue;
|
||||
switch (v.trim().toLowerCase()) {
|
||||
case '1':
|
||||
case 'y':
|
||||
case 'yes':
|
||||
case 'true':
|
||||
return true;
|
||||
case '0':
|
||||
case 'n':
|
||||
case 'no':
|
||||
case 'false':
|
||||
return false;
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
171
lib/core/constants/app_sections.dart
Normal file
171
lib/core/constants/app_sections.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
class AppPageDescriptor {
|
||||
const AppPageDescriptor({
|
||||
required this.path,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final String summary;
|
||||
}
|
||||
|
||||
class AppSectionDescriptor {
|
||||
const AppSectionDescriptor({required this.label, required this.pages});
|
||||
|
||||
final String label;
|
||||
final List<AppPageDescriptor> pages;
|
||||
}
|
||||
|
||||
const loginRoutePath = '/login';
|
||||
const dashboardRoutePath = '/dashboard';
|
||||
|
||||
const appSections = <AppSectionDescriptor>[
|
||||
AppSectionDescriptor(
|
||||
label: '대시보드',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: dashboardRoutePath,
|
||||
label: '대시보드',
|
||||
icon: LucideIcons.layoutDashboard,
|
||||
summary: '오늘 입고/출고, 결재 대기, 최근 트랜잭션을 한 화면에서 확인합니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSectionDescriptor(
|
||||
label: '입·출고',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: '/inventory/inbound',
|
||||
label: '입고',
|
||||
icon: LucideIcons.packagePlus,
|
||||
summary: '입고 처리 기본정보와 라인 품목을 등록하고 검토합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/inventory/outbound',
|
||||
label: '출고',
|
||||
icon: LucideIcons.packageMinus,
|
||||
summary: '출고 품목, 고객사 연결, 상태 변경을 관리합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/inventory/rental',
|
||||
label: '대여',
|
||||
icon: LucideIcons.handshake,
|
||||
summary: '대여/반납 구분과 반납예정일을 포함한 대여 흐름입니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSectionDescriptor(
|
||||
label: '마스터',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: '/masters/vendors',
|
||||
label: '제조사 관리',
|
||||
icon: LucideIcons.factory,
|
||||
summary: '벤더코드, 명칭, 사용여부 등을 유지합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/products',
|
||||
label: '장비 모델 관리',
|
||||
icon: LucideIcons.box,
|
||||
summary: '제품코드, 제조사, 단위 정보를 관리합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/warehouses',
|
||||
label: '입고지 관리',
|
||||
icon: LucideIcons.warehouse,
|
||||
summary: '창고 주소와 사용여부를 설정합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/customers',
|
||||
label: '회사 관리',
|
||||
icon: LucideIcons.building,
|
||||
summary: '고객사 연락처와 주소 정보를 관리합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/users',
|
||||
label: '사용자 관리',
|
||||
icon: LucideIcons.users,
|
||||
summary: '사번, 그룹, 사용여부를 관리합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/groups',
|
||||
label: '그룹 관리',
|
||||
icon: LucideIcons.layers,
|
||||
summary: '권한 그룹과 설명, 기본여부를 정의합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/menus',
|
||||
label: '메뉴 관리',
|
||||
icon: LucideIcons.listTree,
|
||||
summary: '메뉴 계층과 경로, 노출 순서를 구성합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/masters/group-permissions',
|
||||
label: '그룹 메뉴 권한',
|
||||
icon: LucideIcons.shieldCheck,
|
||||
summary: '그룹별 메뉴 CRUD 권한을 설정합니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSectionDescriptor(
|
||||
label: '결재',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: '/approvals/requests',
|
||||
label: '결재 관리',
|
||||
icon: LucideIcons.fileCheck,
|
||||
summary: '결재 번호, 상태, 상신자를 관리합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/approvals/steps',
|
||||
label: '결재 단계',
|
||||
icon: LucideIcons.workflow,
|
||||
summary: '단계 순서와 승인자 할당을 설정합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/approvals/history',
|
||||
label: '결재 이력',
|
||||
icon: LucideIcons.history,
|
||||
summary: '결재 단계별 변경 이력을 조회합니다.',
|
||||
),
|
||||
AppPageDescriptor(
|
||||
path: '/approvals/templates',
|
||||
label: '결재 템플릿',
|
||||
icon: LucideIcons.fileSpreadsheet,
|
||||
summary: '반복되는 결재 흐름을 템플릿으로 관리합니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSectionDescriptor(
|
||||
label: '도구',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: '/utilities/postal-search',
|
||||
label: '우편번호 검색',
|
||||
icon: LucideIcons.search,
|
||||
summary: '모달 기반 우편번호 검색 도구입니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSectionDescriptor(
|
||||
label: '보고',
|
||||
pages: [
|
||||
AppPageDescriptor(
|
||||
path: '/reports',
|
||||
label: '보고서',
|
||||
icon: LucideIcons.fileDown,
|
||||
summary: '조건 필터와 PDF/XLSX 다운로드 기능입니다.',
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
List<AppPageDescriptor> get allAppPages => [
|
||||
for (final section in appSections) ...section.pages,
|
||||
];
|
||||
60
lib/core/network/api_client.dart
Normal file
60
lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// 공통 API 클라이언트 (Dio 래퍼)
|
||||
/// - 모든 HTTP 호출은 이 클래스를 통해 이루어진다.
|
||||
/// - BaseURL/타임아웃/인증/로깅/에러 처리 등을 중앙집중화한다.
|
||||
class ApiClient {
|
||||
final Dio _dio;
|
||||
|
||||
/// 내부에서 사용하는 Dio 인스턴스
|
||||
/// 외부에서 Dio 직접 사용을 최소화하고, 가능하면 아래 헬퍼 메서드를 사용한다.
|
||||
Dio get dio => _dio;
|
||||
|
||||
ApiClient({required Dio dio}) : _dio = dio;
|
||||
|
||||
/// GET 요청 헬퍼
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? query,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.get<T>(path, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
/// POST 요청 헬퍼
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? query,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.post<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
/// PATCH 요청 헬퍼
|
||||
Future<Response<T>> patch<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? query,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.patch<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
/// DELETE 요청 헬퍼
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? query,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.delete<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
29
lib/core/network/interceptors/auth_interceptor.dart
Normal file
29
lib/core/network/interceptors/auth_interceptor.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// 인증 인터셉터(스켈레톤)
|
||||
/// - 요청 전에 Authorization 헤더 주입
|
||||
/// - 401 수신 시 토큰 갱신 및 원요청 1회 재시도 (구현 예정)
|
||||
class AuthInterceptor extends Interceptor {
|
||||
/// TODO: 토큰 저장/조회 서비스 주입 (예: AuthRepository)
|
||||
AuthInterceptor();
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
// TODO: 저장된 토큰을 읽어 Authorization 헤더에 주입한다.
|
||||
// final token = await _authRepository.getToken();
|
||||
// if (token != null && token.isNotEmpty) {
|
||||
// options.headers['Authorization'] = 'Bearer $token';
|
||||
// }
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// TODO: 401 처리 로직(토큰 갱신 → 원요청 재시도) 구현
|
||||
// if (err.response?.statusCode == 401) { ... }
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
135
lib/core/routing/app_router.dart
Normal file
135
lib/core/routing/app_router.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/approvals/history/presentation/pages/approval_history_page.dart';
|
||||
import '../../features/approvals/request/presentation/pages/approval_request_page.dart';
|
||||
import '../../features/approvals/step/presentation/pages/approval_step_page.dart';
|
||||
import '../../features/approvals/template/presentation/pages/approval_template_page.dart';
|
||||
import '../../features/dashboard/presentation/pages/dashboard_page.dart';
|
||||
import '../../features/inventory/inbound/presentation/pages/inbound_page.dart';
|
||||
import '../../features/inventory/outbound/presentation/pages/outbound_page.dart';
|
||||
import '../../features/inventory/rental/presentation/pages/rental_page.dart';
|
||||
import '../../features/login/presentation/pages/login_page.dart';
|
||||
import '../../features/masters/customer/presentation/pages/customer_page.dart';
|
||||
import '../../features/masters/group/presentation/pages/group_page.dart';
|
||||
import '../../features/masters/group_permission/presentation/pages/group_permission_page.dart';
|
||||
import '../../features/masters/menu/presentation/pages/menu_page.dart';
|
||||
import '../../features/masters/product/presentation/pages/product_page.dart';
|
||||
import '../../features/masters/user/presentation/pages/user_page.dart';
|
||||
import '../../features/masters/vendor/presentation/pages/vendor_page.dart';
|
||||
import '../../features/masters/warehouse/presentation/pages/warehouse_page.dart';
|
||||
import '../../features/reporting/presentation/pages/reporting_page.dart';
|
||||
import '../../features/util/postal_search/presentation/pages/postal_search_page.dart';
|
||||
import '../../widgets/app_shell.dart';
|
||||
import '../constants/app_sections.dart';
|
||||
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
|
||||
final appRouter = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: loginRoutePath,
|
||||
routes: [
|
||||
GoRoute(path: '/', redirect: (_, __) => loginRoutePath),
|
||||
GoRoute(
|
||||
path: loginRoutePath,
|
||||
name: 'login',
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
AppShell(currentLocation: state.uri.toString(), child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: dashboardRoutePath,
|
||||
name: 'dashboard',
|
||||
builder: (context, state) => const DashboardPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/inventory/inbound',
|
||||
name: 'inventory-inbound',
|
||||
builder: (context, state) => const InboundPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/inventory/outbound',
|
||||
name: 'inventory-outbound',
|
||||
builder: (context, state) => const OutboundPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/inventory/rental',
|
||||
name: 'inventory-rental',
|
||||
builder: (context, state) => const RentalPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/vendors',
|
||||
name: 'masters-vendors',
|
||||
builder: (context, state) => const VendorPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/products',
|
||||
name: 'masters-products',
|
||||
builder: (context, state) => const ProductPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/warehouses',
|
||||
name: 'masters-warehouses',
|
||||
builder: (context, state) => const WarehousePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/customers',
|
||||
name: 'masters-customers',
|
||||
builder: (context, state) => const CustomerPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/users',
|
||||
name: 'masters-users',
|
||||
builder: (context, state) => const UserPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/groups',
|
||||
name: 'masters-groups',
|
||||
builder: (context, state) => const GroupPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/menus',
|
||||
name: 'masters-menus',
|
||||
builder: (context, state) => const MenuPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/masters/group-permissions',
|
||||
name: 'masters-group-permissions',
|
||||
builder: (context, state) => const GroupPermissionPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approvals/requests',
|
||||
name: 'approvals-requests',
|
||||
builder: (context, state) => const ApprovalRequestPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approvals/steps',
|
||||
name: 'approvals-steps',
|
||||
builder: (context, state) => const ApprovalStepPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approvals/history',
|
||||
name: 'approvals-history',
|
||||
builder: (context, state) => const ApprovalHistoryPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approvals/templates',
|
||||
name: 'approvals-templates',
|
||||
builder: (context, state) => const ApprovalTemplatePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/utilities/postal-search',
|
||||
name: 'utilities-postal-search',
|
||||
builder: (context, state) => const PostalSearchPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'reports',
|
||||
builder: (context, state) => const ReportingPage(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
Reference in New Issue
Block a user