diff --git a/lib/core/config/environment.dart b/lib/core/config/environment.dart index 9d08ba4..2968fe0 100644 --- a/lib/core/config/environment.dart +++ b/lib/core/config/environment.dart @@ -39,7 +39,12 @@ class Environment { const String.fromEnvironment('ENVIRONMENT', defaultValue: dev); final envFile = _getEnvFile(); - await dotenv.load(fileName: envFile); + try { + await dotenv.load(fileName: envFile); + } catch (e) { + print('Failed to load env file $envFile: $e'); + // .env 파일이 없어도 계속 진행 + } } /// 환경별 파일 경로 반환 diff --git a/lib/data/datasources/remote/api_client.dart b/lib/data/datasources/remote/api_client.dart index ea69721..f157b5f 100644 --- a/lib/data/datasources/remote/api_client.dart +++ b/lib/data/datasources/remote/api_client.dart @@ -9,31 +9,69 @@ import 'interceptors/logging_interceptor.dart'; class ApiClient { late final Dio _dio; - static final ApiClient _instance = ApiClient._internal(); + static ApiClient? _instance; - factory ApiClient() => _instance; + factory ApiClient() { + _instance ??= ApiClient._internal(); + return _instance!; + } ApiClient._internal() { - _dio = Dio(_baseOptions); - _setupInterceptors(); + try { + _dio = Dio(_baseOptions); + _setupInterceptors(); + } catch (e) { + print('Error while creating ApiClient'); + print('Stack trace:'); + print(StackTrace.current); + // 기본값으로 초기화 + _dio = Dio(BaseOptions( + baseUrl: 'http://localhost:8080/api/v1', + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + )); + _setupInterceptors(); + } } /// Dio 인스턴스 getter Dio get dio => _dio; /// 기본 옵션 설정 - BaseOptions get _baseOptions => BaseOptions( - baseUrl: Environment.apiBaseUrl, - connectTimeout: Duration(milliseconds: Environment.apiTimeout), - receiveTimeout: Duration(milliseconds: Environment.apiTimeout), - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - validateStatus: (status) { - return status != null && status < 500; - }, - ); + BaseOptions get _baseOptions { + try { + return BaseOptions( + baseUrl: Environment.apiBaseUrl, + connectTimeout: Duration(milliseconds: Environment.apiTimeout), + receiveTimeout: Duration(milliseconds: Environment.apiTimeout), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + validateStatus: (status) { + return status != null && status < 500; + }, + ); + } catch (e) { + // Environment가 초기화되지 않은 경우 기본값 사용 + return BaseOptions( + baseUrl: 'http://localhost:8080/api/v1', + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + validateStatus: (status) { + return status != null && status < 500; + }, + ); + } + } /// 인터셉터 설정 void _setupInterceptors() { @@ -46,8 +84,15 @@ class ApiClient { _dio.interceptors.add(ErrorInterceptor()); // 로깅 인터셉터 (개발 환경에서만) - if (Environment.enableLogging && kDebugMode) { - _dio.interceptors.add(LoggingInterceptor()); + try { + if (Environment.enableLogging && kDebugMode) { + _dio.interceptors.add(LoggingInterceptor()); + } + } catch (e) { + // Environment 접근 실패 시 디버그 모드에서만 로깅 활성화 + if (kDebugMode) { + _dio.interceptors.add(LoggingInterceptor()); + } } } diff --git a/lib/data/datasources/remote/interceptors/auth_interceptor.dart b/lib/data/datasources/remote/interceptors/auth_interceptor.dart index 8bd35ef..3a3873b 100644 --- a/lib/data/datasources/remote/interceptors/auth_interceptor.dart +++ b/lib/data/datasources/remote/interceptors/auth_interceptor.dart @@ -5,10 +5,16 @@ import '../../../../services/auth_service.dart'; /// 인증 인터셉터 class AuthInterceptor extends Interceptor { - late final AuthService _authService; + AuthService? _authService; - AuthInterceptor() { - _authService = GetIt.instance(); + AuthService? get authService { + try { + _authService ??= GetIt.instance(); + return _authService; + } catch (e) { + print('Failed to get AuthService in AuthInterceptor: $e'); + return null; + } } @override @@ -23,10 +29,13 @@ class AuthInterceptor extends Interceptor { } // 저장된 액세스 토큰 가져오기 - final accessToken = await _authService.getAccessToken(); - - if (accessToken != null) { - options.headers['Authorization'] = 'Bearer $accessToken'; + final service = authService; + if (service != null) { + final accessToken = await service.getAccessToken(); + + if (accessToken != null) { + options.headers['Authorization'] = 'Bearer $accessToken'; + } } handler.next(options); @@ -39,36 +48,39 @@ class AuthInterceptor extends Interceptor { ) async { // 401 Unauthorized 에러 처리 if (err.response?.statusCode == 401) { - // 토큰 갱신 시도 - final refreshResult = await _authService.refreshToken(); - - final refreshSuccess = refreshResult.fold( - (failure) => false, - (tokenResponse) => true, - ); - - if (refreshSuccess) { - // 새로운 토큰으로 원래 요청 재시도 - try { - final newAccessToken = await _authService.getAccessToken(); - - if (newAccessToken != null) { - err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; + final service = authService; + if (service != null) { + // 토큰 갱신 시도 + final refreshResult = await service.refreshToken(); + + final refreshSuccess = refreshResult.fold( + (failure) => false, + (tokenResponse) => true, + ); + + if (refreshSuccess) { + // 새로운 토큰으로 원래 요청 재시도 + try { + final newAccessToken = await service.getAccessToken(); - final response = await Dio().fetch(err.requestOptions); - handler.resolve(response); + if (newAccessToken != null) { + err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; + + final response = await Dio().fetch(err.requestOptions); + handler.resolve(response); + return; + } + } catch (e) { + // 재시도 실패 + handler.next(err); return; } - } catch (e) { - // 재시도 실패 - handler.next(err); - return; } + + // 토큰 갱신 실패 시 로그인 화면으로 이동 + await service.clearSession(); + // TODO: Navigate to login screen } - - // 토큰 갱신 실패 시 로그인 화면으로 이동 - await _authService.clearSession(); - // TODO: Navigate to login screen } handler.next(err); diff --git a/lib/main.dart b/lib/main.dart index f259406..00fac70 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,8 +20,13 @@ void main() async { // Flutter 바인딩 초기화 WidgetsFlutterBinding.ensureInitialized(); - // 의존성 주입 설정 - await di.setupDependencies(); + try { + // 의존성 주입 설정 + await di.setupDependencies(); + } catch (e) { + print('Failed to setup dependencies: $e'); + // 에러가 발생해도 앱은 실행되도록 함 + } // MockDataService는 싱글톤으로 자동 초기화됨 runApp(const SuperportApp()); @@ -32,7 +37,12 @@ class SuperportApp extends StatelessWidget { @override Widget build(BuildContext context) { - final authService = GetIt.instance(); + AuthService? authService; + try { + authService = GetIt.instance(); + } catch (e) { + print('Failed to get AuthService: $e'); + } return MaterialApp( title: 'supERPort', @@ -44,26 +54,35 @@ class SuperportApp extends StatelessWidget { ], supportedLocales: const [Locale('ko', 'KR'), Locale('en', 'US')], locale: const Locale('ko', 'KR'), - home: FutureBuilder( - future: authService.isLoggedIn(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } - - if (snapshot.hasData && snapshot.data!) { - // 토큰이 유효하면 홈 화면으로 - return AppLayoutRedesign(initialRoute: Routes.home); - } else { - // 토큰이 없거나 유효하지 않으면 로그인 화면으로 - return const LoginScreen(); - } - }, - ), + home: authService == null + ? const LoginScreen() // AuthService가 없으면 바로 로그인 화면 + : FutureBuilder( + future: authService.isLoggedIn(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + // 에러 처리 추가 + if (snapshot.hasError) { + print('Auth check error: ${snapshot.error}'); + // 에러가 발생해도 로그인 화면으로 이동 + return const LoginScreen(); + } + + if (snapshot.hasData && snapshot.data!) { + // 토큰이 유효하면 홈 화면으로 + return AppLayoutRedesign(initialRoute: Routes.home); + } else { + // 토큰이 없거나 유효하지 않으면 로그인 화면으로 + return const LoginScreen(); + } + }, + ), onGenerateRoute: (settings) { // 로그인 라우트 처리 if (settings.name == '/login') { diff --git a/lib/screens/common/app_layout.dart b/lib/screens/common/app_layout.dart deleted file mode 100644 index b698fe6..0000000 --- a/lib/screens/common/app_layout.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/sidebar/sidebar_screen.dart'; -import 'package:superport/screens/overview/overview_screen.dart'; -import 'package:superport/screens/equipment/equipment_list.dart'; -import 'package:superport/screens/company/company_list.dart'; -import 'package:superport/screens/user/user_list.dart'; -import 'package:superport/screens/license/license_list.dart'; -import 'package:superport/screens/warehouse_location/warehouse_location_list.dart'; -import 'package:superport/utils/constants.dart'; - -/// SPA 스타일의 앱 레이아웃 클래스 -/// 사이드바는 고정되고 내용만 변경되는 구조를 제공 -class AppLayout extends StatefulWidget { - final String initialRoute; - - const AppLayout({Key? key, this.initialRoute = Routes.home}) - : super(key: key); - - @override - _AppLayoutState createState() => _AppLayoutState(); -} - -class _AppLayoutState extends State { - late String _currentRoute; - - @override - void initState() { - super.initState(); - _currentRoute = widget.initialRoute; - } - - /// 현재 경로에 따라 적절한 컨텐츠 섹션을 반환 - Widget _getContentForRoute(String route) { - switch (route) { - case Routes.home: - return const OverviewScreen(); - case Routes.equipment: - case Routes.equipmentInList: - case Routes.equipmentOutList: - case Routes.equipmentRentList: - // 장비 목록 화면에 현재 라우트 정보를 전달 - return EquipmentListScreen(currentRoute: route); - case Routes.company: - return const CompanyListScreen(); - case Routes.license: - return const MaintenanceListScreen(); - case Routes.warehouseLocation: - return const WarehouseLocationListScreen(); - default: - return const OverviewScreen(); - } - } - - /// 경로 변경 메서드 - void _navigateTo(String route) { - setState(() { - _currentRoute = route; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - children: [ - // 왼쪽 사이드바 - SizedBox( - width: 280, - child: SidebarMenu( - currentRoute: _currentRoute, - onRouteChanged: _navigateTo, - ), - ), - // 오른쪽 컨텐츠 영역 - Expanded(child: _getContentForRoute(_currentRoute)), - ], - ), - ); - } -} diff --git a/lib/screens/common/custom_widgets.dart b/lib/screens/common/custom_widgets.dart index e87c80b..72df266 100644 --- a/lib/screens/common/custom_widgets.dart +++ b/lib/screens/common/custom_widgets.dart @@ -1,5 +1,3 @@ -export 'custom_widgets/page_title.dart'; -export 'custom_widgets/data_table_card.dart'; export 'custom_widgets/form_field_wrapper.dart'; export 'custom_widgets/date_picker_field.dart'; export 'custom_widgets/highlight_text.dart'; diff --git a/lib/screens/common/custom_widgets/data_table_card.dart b/lib/screens/common/custom_widgets/data_table_card.dart deleted file mode 100644 index 004f375..0000000 --- a/lib/screens/common/custom_widgets/data_table_card.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 데이터 테이블 카드 -class DataTableCard extends StatelessWidget { - final Widget child; - final String? title; - final double? width; - - const DataTableCard({Key? key, required this.child, this.title, this.width}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - width: width, - decoration: AppThemeTailwind.cardDecoration, - margin: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title != null) - Padding( - padding: const EdgeInsets.all(16.0), - child: Text(title!, style: AppThemeTailwind.subheadingStyle), - ), - Padding(padding: const EdgeInsets.all(16.0), child: child), - ], - ), - ); - } -} diff --git a/lib/screens/common/custom_widgets/page_title.dart b/lib/screens/common/custom_widgets/page_title.dart deleted file mode 100644 index 867437d..0000000 --- a/lib/screens/common/custom_widgets/page_title.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 페이지 타이틀 위젯 -class PageTitle extends StatelessWidget { - final String title; - final Widget? rightWidget; - final double? width; - - const PageTitle({Key? key, required this.title, this.rightWidget, this.width}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - width: width, - margin: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: AppThemeTailwind.headingStyle), - if (rightWidget != null) rightWidget!, - ], - ), - ); - } -} diff --git a/lib/screens/common/layout_components.dart b/lib/screens/common/layout_components.dart deleted file mode 100644 index 35974c2..0000000 --- a/lib/screens/common/layout_components.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// 메트로닉 스타일 공통 레이아웃 컴포넌트 barrel 파일 -/// 각 위젯은 SRP에 따라 별도 파일로 분리되어 있습니다. -export 'metronic_page_container.dart'; -export 'metronic_card.dart'; -export 'metronic_stats_card.dart'; -export 'metronic_page_title.dart'; -export 'metronic_data_table.dart'; -export 'metronic_form_field.dart'; -export 'metronic_tab_container.dart'; diff --git a/lib/screens/common/main_layout.dart b/lib/screens/common/main_layout.dart deleted file mode 100644 index 72ba947..0000000 --- a/lib/screens/common/main_layout.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -class MainLayout extends StatelessWidget { - final String title; - final Widget child; - final String currentRoute; - final List? actions; - final bool showBackButton; - final Widget? floatingActionButton; - - const MainLayout({ - Key? key, - required this.title, - required this.child, - required this.currentRoute, - this.actions, - this.showBackButton = false, - this.floatingActionButton, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - // MetronicCloud 스타일: 상단부 플랫, 여백 넓게, 타이틀/경로/버튼 스타일링 - return Scaffold( - backgroundColor: AppThemeTailwind.surface, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 상단 앱바 - _buildAppBar(context), - // 컨텐츠 - Expanded(child: child), - ], - ), - floatingActionButton: floatingActionButton, - ); - } - - Widget _buildAppBar(BuildContext context) { - // 상단 앱바: 경로 텍스트가 수직 중앙에 오도록 조정, 배경색/글자색 변경 - return Container( - height: 88, - padding: const EdgeInsets.symmetric(horizontal: 40), - decoration: BoxDecoration( - color: AppThemeTailwind.surface, // 회색 배경 - border: const Border( - bottom: BorderSide(color: Color(0xFFF3F6F9), width: 1), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, // Row 내에서 수직 중앙 정렬 - children: [ - // 경로 및 타이틀 영역 (수직 중앙 정렬) - Column( - mainAxisAlignment: MainAxisAlignment.center, // Column 내에서 수직 중앙 정렬 - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 경로 텍스트 (폰트 사이즈 2배, 검은색 글자) - Text( - _getBreadcrumb(currentRoute), - style: TextStyle( - fontSize: 26, - color: AppThemeTailwind.dark, - ), // 검은색 글자 - ), - // 타이틀이 있을 때만 표시 - if (title.isNotEmpty) - Text( - title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: AppThemeTailwind.dark, - ), - ), - ], - ), - const Spacer(), - if (actions != null) - Row( - children: - actions! - .map( - (w) => Padding( - padding: const EdgeInsets.only(left: 8), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: AppThemeTailwind.muted, - width: 1, - ), - color: const Color(0xFFF7F8FA), - borderRadius: BorderRadius.circular(8), - ), - child: w, - ), - ), - ) - .toList(), - ), - ], - ), - ); - } - - // 현재 라우트에 따라 경로 문자열을 반환하는 함수 - String _getBreadcrumb(String route) { - // 실제 라우트에 따라 경로를 한글로 변환 (예시) - switch (route) { - case '/': - case '/home': - return '홈 / 대시보드'; - case '/equipment': - return '홈 / 장비 관리'; - case '/company': - return '홈 / 회사 관리'; - case '/maintenance': - return '홈 / 유지보수 관리'; - case '/item': - return '홈 / 물품 관리'; - default: - return '홈'; - } - } -} diff --git a/lib/screens/common/metronic_card.dart b/lib/screens/common/metronic_card.dart deleted file mode 100644 index 985c393..0000000 --- a/lib/screens/common/metronic_card.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 카드 위젯 (SRP 분리) -class MetronicCard extends StatelessWidget { - final String? title; - final Widget child; - final List? actions; - final EdgeInsetsGeometry? padding; - final EdgeInsetsGeometry? margin; - - const MetronicCard({ - Key? key, - this.title, - required this.child, - this.actions, - this.padding, - this.margin, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - decoration: AppThemeTailwind.cardDecoration, - margin: margin, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title != null || actions != null) - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (title != null) - Text(title!, style: AppThemeTailwind.subheadingStyle), - if (actions != null) Row(children: actions!), - ], - ), - ), - Padding(padding: padding ?? const EdgeInsets.all(16), child: child), - ], - ), - ); - } -} diff --git a/lib/screens/common/metronic_data_table.dart b/lib/screens/common/metronic_data_table.dart deleted file mode 100644 index de16d01..0000000 --- a/lib/screens/common/metronic_data_table.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/metronic_card.dart'; - -/// 메트로닉 스타일 데이터 테이블 카드 위젯 (SRP 분리) -class MetronicDataTable extends StatelessWidget { - final List columns; - final List rows; - final String? title; - final bool isLoading; - final String? emptyMessage; - - const MetronicDataTable({ - Key? key, - required this.columns, - required this.rows, - this.title, - this.isLoading = false, - this.emptyMessage, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return MetronicCard( - title: title, - child: - isLoading - ? const Center(child: CircularProgressIndicator()) - : rows.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Text( - emptyMessage ?? '데이터가 없습니다.', - style: AppThemeTailwind.bodyStyle, - ), - ), - ) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columns: columns, - rows: rows, - headingRowColor: MaterialStateProperty.all( - AppThemeTailwind.light, - ), - dataRowMaxHeight: 60, - columnSpacing: 24, - horizontalMargin: 16, - ), - ), - ), - ); - } -} diff --git a/lib/screens/common/metronic_form_field.dart b/lib/screens/common/metronic_form_field.dart deleted file mode 100644 index 13e35a6..0000000 --- a/lib/screens/common/metronic_form_field.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 폼 필드 래퍼 위젯 (SRP 분리) -class MetronicFormField extends StatelessWidget { - final String label; - final Widget child; - final bool isRequired; - final String? helperText; - - const MetronicFormField({ - Key? key, - required this.label, - required this.child, - this.isRequired = false, - this.helperText, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - label, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: AppThemeTailwind.dark, - ), - ), - if (isRequired) - const Text( - ' *', - style: TextStyle( - color: AppThemeTailwind.danger, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - child, - if (helperText != null) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text(helperText!, style: AppThemeTailwind.smallText), - ), - ], - ), - ); - } -} diff --git a/lib/screens/common/metronic_page_container.dart b/lib/screens/common/metronic_page_container.dart deleted file mode 100644 index 6170a44..0000000 --- a/lib/screens/common/metronic_page_container.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 페이지 컨테이너 위젯 (SRP 분리) -class MetronicPageContainer extends StatelessWidget { - final String title; - final Widget child; - final List? actions; - final bool showBackButton; - - const MetronicPageContainer({ - Key? key, - required this.title, - required this.child, - this.actions, - this.showBackButton = true, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(title), - automaticallyImplyLeading: showBackButton, - actions: actions, - elevation: 0, - ), - body: Container( - color: AppThemeTailwind.surface, - padding: const EdgeInsets.all(16), - child: child, - ), - ); - } -} diff --git a/lib/screens/common/metronic_page_title.dart b/lib/screens/common/metronic_page_title.dart deleted file mode 100644 index 2e02300..0000000 --- a/lib/screens/common/metronic_page_title.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 페이지 타이틀 위젯 (SRP 분리) -class MetronicPageTitle extends StatelessWidget { - final String title; - final VoidCallback? onAddPressed; - final String? addButtonLabel; - - const MetronicPageTitle({ - Key? key, - required this.title, - this.onAddPressed, - this.addButtonLabel, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: AppThemeTailwind.headingStyle), - if (onAddPressed != null) - ElevatedButton.icon( - onPressed: onAddPressed, - icon: const Icon(Icons.add), - label: Text(addButtonLabel ?? '추가'), - style: AppThemeTailwind.primaryButtonStyle, - ), - ], - ), - ); - } -} diff --git a/lib/screens/common/metronic_stats_card.dart b/lib/screens/common/metronic_stats_card.dart deleted file mode 100644 index dd1d32f..0000000 --- a/lib/screens/common/metronic_stats_card.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 통계 카드 위젯 (SRP 분리) -class MetronicStatsCard extends StatelessWidget { - final String title; - final String value; - final String? subtitle; - final IconData? icon; - final Color? iconBackgroundColor; - final bool showTrend; - final double? trendPercentage; - final bool isPositiveTrend; - - const MetronicStatsCard({ - Key? key, - required this.title, - required this.value, - this.subtitle, - this.icon, - this.iconBackgroundColor, - this.showTrend = false, - this.trendPercentage, - this.isPositiveTrend = true, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - decoration: AppThemeTailwind.cardDecoration, - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: AppThemeTailwind.bodyStyle.copyWith( - color: AppThemeTailwind.muted, - fontSize: 12, - ), - ), - if (icon != null) - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: iconBackgroundColor ?? AppThemeTailwind.light, - shape: BoxShape.circle, - ), - child: Icon( - icon, - color: - iconBackgroundColor != null - ? Colors.white - : AppThemeTailwind.primary, - size: 16, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - value, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppThemeTailwind.dark, - ), - ), - if (subtitle != null || showTrend) const SizedBox(height: 4), - if (subtitle != null) - Text(subtitle!, style: AppThemeTailwind.smallText), - if (showTrend && trendPercentage != null) - Row( - children: [ - Icon( - isPositiveTrend ? Icons.arrow_upward : Icons.arrow_downward, - color: - isPositiveTrend - ? AppThemeTailwind.success - : AppThemeTailwind.danger, - size: 12, - ), - const SizedBox(width: 4), - Text( - '${trendPercentage!.toStringAsFixed(1)}%', - style: TextStyle( - color: - isPositiveTrend - ? AppThemeTailwind.success - : AppThemeTailwind.danger, - fontWeight: FontWeight.w600, - fontSize: 12, - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/screens/common/metronic_tab_container.dart b/lib/screens/common/metronic_tab_container.dart deleted file mode 100644 index d3d03e9..0000000 --- a/lib/screens/common/metronic_tab_container.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 메트로닉 스타일 탭 컨테이너 위젯 (SRP 분리) -class MetronicTabContainer extends StatelessWidget { - final List tabs; - final List tabViews; - final int initialIndex; - - const MetronicTabContainer({ - Key? key, - required this.tabs, - required this.tabViews, - this.initialIndex = 0, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: tabs.length, - initialIndex: initialIndex, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Color(0xFFE5E7EB), width: 1), - ), - ), - child: TabBar( - tabs: tabs.map((tab) => Tab(text: tab)).toList(), - labelColor: AppThemeTailwind.primary, - unselectedLabelColor: AppThemeTailwind.muted, - labelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - indicatorColor: AppThemeTailwind.primary, - indicatorWeight: 2, - ), - ), - Expanded(child: TabBarView(children: tabViews)), - ], - ), - ); - } -} diff --git a/lib/screens/company/company_list.dart b/lib/screens/company/company_list.dart deleted file mode 100644 index f9027a1..0000000 --- a/lib/screens/company/company_list.dart +++ /dev/null @@ -1,501 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/company_model.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/services/mock_data_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/common/widgets/pagination.dart'; -import 'package:superport/screens/company/widgets/company_branch_dialog.dart'; - -class CompanyListScreen extends StatefulWidget { - const CompanyListScreen({super.key}); - - @override - State createState() => _CompanyListScreenState(); -} - -class _CompanyListScreenState extends State { - final MockDataService _dataService = MockDataService(); - List _companies = []; - // 페이지네이션 상태 추가 - int _currentPage = 1; // 현재 페이지 (1부터 시작) - final int _pageSize = 10; // 페이지당 개수 - - @override - void initState() { - super.initState(); - _loadData(); - } - - void _loadData() { - setState(() { - _companies = _dataService.getAllCompanies(); - // 데이터가 변경되면 첫 페이지로 이동 - _currentPage = 1; - }); - } - - void _navigateToAddScreen() async { - final result = await Navigator.pushNamed(context, '/company/add'); - if (result == true) { - _loadData(); - } - } - - void _navigateToEditScreen(int id) async { - final result = await Navigator.pushNamed( - context, - '/company/edit', - arguments: id, - ); - if (result == true) { - _loadData(); - } - } - - void _deleteCompany(int id) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('삭제 확인'), - content: const Text('이 회사 정보를 삭제하시겠습니까?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - _dataService.deleteCompany(id); - Navigator.pop(context); - _loadData(); - }, - child: const Text('삭제'), - ), - ], - ), - ); - } - - // 회사 유형에 따라 칩 위젯 생성 (복수) - Widget _buildCompanyTypeChips(List types) { - return Row( - children: - types.map((type) { - final Color textColor = - type == CompanyType.customer - ? Colors.blue.shade800 - : Colors.green.shade800; - final String label = companyTypeToString(type); - return Container( - margin: const EdgeInsets.only(right: 4), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: textColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - label, - style: TextStyle( - color: textColor, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ); - }).toList(), - ); - } - - // 본사/지점 구분 표시 위젯 - Widget _buildCompanyTypeLabel(bool isBranch, {String? mainCompanyName}) { - if (isBranch) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.account_tree, size: 16, color: Colors.blue.shade600), - const SizedBox(width: 4), - const Text('지점'), - ], - ); - } else { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.business, size: 16, color: Colors.grey.shade700), - const SizedBox(width: 4), - const Text('본사'), - ], - ); - } - } - - // 회사 이름 표시 위젯 (지점인 경우 "본사명 > 지점명" 형식) - Widget _buildCompanyNameText( - Company company, - bool isBranch, { - String? mainCompanyName, - }) { - if (isBranch && mainCompanyName != null) { - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: isBranch ? '▶ ' : '', - style: TextStyle(color: Colors.grey.shade600, fontSize: 14), - ), - TextSpan( - text: isBranch ? '$mainCompanyName > ' : '', - style: TextStyle( - color: Colors.grey.shade700, - fontWeight: FontWeight.normal, - ), - ), - TextSpan( - text: company.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ); - } else { - return Text( - company.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ); - } - } - - // 지점(본사+지점)만 보여주는 팝업 오픈 함수 - void _showBranchDialog(Company mainCompany) { - showDialog( - context: context, - builder: (context) => CompanyBranchDialog(mainCompany: mainCompany), - ); - } - - @override - Widget build(BuildContext context) { - // 대시보드 폭에 맞게 조정 - final screenWidth = MediaQuery.of(context).size.width; - final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32; - - // 본사와 지점 구분하기 위한 데이터 준비 - final List> displayCompanies = []; - for (final company in _companies) { - displayCompanies.add({ - 'company': company, - 'isBranch': false, - 'mainCompanyName': null, - }); - if (company.branches != null) { - for (final branch in company.branches!) { - displayCompanies.add({ - 'branch': branch, // 지점 객체 자체 저장 - 'companyId': company.id, // 본사 id 저장 - 'isBranch': true, - 'mainCompanyName': company.name, - }); - } - } - } - - // 페이지네이션 데이터 슬라이싱 - final int totalCount = displayCompanies.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > totalCount - ? totalCount - : (startIndex + _pageSize); - final List> pagedCompanies = displayCompanies.sublist( - startIndex, - endIndex, - ); - - return MainLayout( - title: '회사 관리', - currentRoute: Routes.company, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _loadData, - color: Colors.grey, - ), - ], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PageTitle( - title: '회사 목록', - width: maxContentWidth - 32, - rightWidget: ElevatedButton.icon( - onPressed: _navigateToAddScreen, - icon: const Icon(Icons.add), - label: const Text('추가'), - style: AppThemeTailwind.primaryButtonStyle, - ), - ), - Expanded( - child: DataTableCard( - width: maxContentWidth - 32, - child: - pagedCompanies.isEmpty - ? const Center(child: Text('등록된 회사 정보가 없습니다.')) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - width: maxContentWidth - 32, - constraints: BoxConstraints( - minWidth: maxContentWidth - 64, - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columns: const [ - DataColumn(label: Text('번호')), - DataColumn(label: Text('구분')), - DataColumn(label: Text('회사명')), - DataColumn(label: Text('유형')), - DataColumn(label: Text('주소')), - DataColumn(label: Text('지점 수 (본사만 표시)')), - DataColumn(label: Text('관리')), - ], - rows: - pagedCompanies.asMap().entries.map((entry) { - final index = entry.key; - final data = entry.value; - final bool isBranch = - data['isBranch'] as bool; - final String? mainCompanyName = - data['mainCompanyName'] as String?; - - if (isBranch) { - final Branch branch = - data['branch'] as Branch; - final int companyId = - data['companyId'] as int; - return DataRow( - cells: [ - DataCell( - Text('${startIndex + index + 1}'), - ), - DataCell( - _buildCompanyTypeLabel( - true, - mainCompanyName: - mainCompanyName, - ), - ), - DataCell( - _buildCompanyNameText( - Company( - id: branch.id, - name: branch.name, - address: branch.address, - contactName: - branch.contactName, - contactPosition: - branch.contactPosition, - contactPhone: - branch.contactPhone, - contactEmail: - branch.contactEmail, - companyTypes: [], - remark: branch.remark, - ), - true, - mainCompanyName: - mainCompanyName, - ), - ), - DataCell( - _buildCompanyTypeChips([]), - ), - DataCell( - Text(branch.address.toString()), - ), - DataCell(const Text('')), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: - AppThemeTailwind - .primary, - ), - onPressed: () { - Navigator.pushNamed( - context, - '/company/edit', - arguments: { - 'companyId': - companyId, - 'isBranch': true, - 'mainCompanyName': - mainCompanyName, - 'branchId': branch.id, - }, - ).then((result) { - if (result == true) - _loadData(); - }); - }, - ), - IconButton( - icon: const Icon( - Icons.delete, - color: - AppThemeTailwind - .danger, - ), - onPressed: () { - // 지점 삭제 로직 필요시 구현 - }, - ), - ], - ), - ), - ], - ); - } else { - final Company company = - data['company'] as Company; - return DataRow( - cells: [ - DataCell( - Text('${startIndex + index + 1}'), - ), - DataCell( - _buildCompanyTypeLabel(false), - ), - DataCell( - _buildCompanyNameText( - company, - false, - ), - ), - DataCell( - _buildCompanyTypeChips( - company.companyTypes, - ), - ), - DataCell( - Text(company.address.toString()), - ), - DataCell( - GestureDetector( - onTap: () { - if ((company - .branches - ?.isNotEmpty ?? - false)) { - _showBranchDialog(company); - } - }, - child: MouseRegion( - cursor: - SystemMouseCursors.click, - child: Text( - '${(company.branches?.length ?? 0)}', - style: TextStyle( - color: - (company - .branches - ?.isNotEmpty ?? - false) - ? Colors.blue - : Colors.black, - decoration: - (company - .branches - ?.isNotEmpty ?? - false) - ? TextDecoration - .underline - : TextDecoration - .none, - ), - ), - ), - ), - ), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: - AppThemeTailwind - .primary, - ), - onPressed: () { - Navigator.pushNamed( - context, - '/company/edit', - arguments: { - 'companyId': - company.id, - 'isBranch': false, - }, - ).then((result) { - if (result == true) - _loadData(); - }); - }, - ), - IconButton( - icon: const Icon( - Icons.delete, - color: - AppThemeTailwind - .danger, - ), - onPressed: () { - _deleteCompany( - company.id!, - ); - }, - ), - ], - ), - ), - ], - ); - } - }).toList(), - ), - ), - ), - ), - ), - ), - // 페이지네이션 위젯 추가 - if (totalCount > _pageSize) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Pagination( - totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, - onPageChanged: (page) { - setState(() { - _currentPage = page; - }); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/company/widgets/company_branch_dialog.dart b/lib/screens/company/widgets/company_branch_dialog.dart index 08c7af3..d13b6ab 100644 --- a/lib/screens/company/widgets/company_branch_dialog.dart +++ b/lib/screens/company/widgets/company_branch_dialog.dart @@ -5,7 +5,7 @@ import 'package:pdf/widgets.dart' as pw; // PDF 생성용 import 'package:printing/printing.dart'; // PDF 프린트/미리보기용 import 'dart:typed_data'; // Uint8List import 'package:pdf/pdf.dart'; // PdfColors, PageFormat 등 전체 임포트 -import 'package:superport/screens/common/custom_widgets.dart'; // DataTableCard 사용을 위한 import +import 'package:superport/screens/common/components/shadcn_components.dart'; // ShadcnCard 사용을 위한 import import 'package:flutter/services.dart'; // rootBundle 사용을 위한 import /// 본사와 지점 리스트를 보여주는 다이얼로그 위젯 @@ -264,8 +264,7 @@ class CompanyBranchDialog extends StatelessWidget { ), const SizedBox(height: 16), Expanded( - child: DataTableCard( - width: maxDialogWidth - 48, + child: ShadcnCard( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Container( diff --git a/lib/screens/equipment/equipment_list.dart b/lib/screens/equipment/equipment_list.dart deleted file mode 100644 index 73abc84..0000000 --- a/lib/screens/equipment/equipment_list.dart +++ /dev/null @@ -1,696 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart'; -import 'package:superport/screens/equipment/widgets/equipment_table.dart'; -import 'package:superport/utils/equipment_display_helper.dart'; -import 'package:superport/services/mock_data_service.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/screens/common/widgets/pagination.dart'; - -// 장비 목록 화면 (UI만 담당, 상태/로직/헬퍼/위젯 분리) -class EquipmentListScreen extends StatefulWidget { - final String currentRoute; - const EquipmentListScreen({super.key, this.currentRoute = Routes.equipment}); - - @override - State createState() => _EquipmentListScreenState(); -} - -class _EquipmentListScreenState extends State { - late final EquipmentListController _controller; - bool _showDetailedColumns = true; - final ScrollController _horizontalScrollController = ScrollController(); - final ScrollController _verticalScrollController = ScrollController(); - int _currentPage = 1; - final int _pageSize = 10; - String _searchKeyword = ''; - String _appliedSearchKeyword = ''; - - @override - void initState() { - super.initState(); - _controller = EquipmentListController(dataService: MockDataService()); - _controller.loadData(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _adjustColumnsForScreenSize(); - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _setDefaultFilterByRoute(); - } - - @override - void didUpdateWidget(EquipmentListScreen oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.currentRoute != widget.currentRoute) { - _setDefaultFilterByRoute(); - } - } - - @override - void dispose() { - _horizontalScrollController.dispose(); - _verticalScrollController.dispose(); - super.dispose(); - } - - // 라우트에 따라 기본 필터 설정 - void _setDefaultFilterByRoute() { - String? newFilter; - if (widget.currentRoute == Routes.equipmentInList) { - newFilter = EquipmentStatus.in_; - } else if (widget.currentRoute == Routes.equipmentOutList) { - newFilter = EquipmentStatus.out; - } else if (widget.currentRoute == Routes.equipmentRentList) { - newFilter = EquipmentStatus.rent; - } else if (widget.currentRoute == Routes.equipment) { - newFilter = null; - } - if ((newFilter != _controller.selectedStatusFilter) || - widget.currentRoute != Routes.equipment) { - setState(() { - _controller.selectedStatusFilter = newFilter; - _controller.loadData(); - }); - } - } - - // 화면 크기에 따라 컬럼 표시 조정 - void _adjustColumnsForScreenSize() { - final width = MediaQuery.of(context).size.width; - setState(() { - _showDetailedColumns = width > 900; - }); - } - - // 상태 필터 변경 - void _onStatusFilterChanged(String? status) { - setState(() { - _controller.changeStatusFilter(status); - }); - } - - // 장비 선택/해제 - void _onEquipmentSelected(int? id, String status, bool? isSelected) { - setState(() { - _controller.selectEquipment(id, status, isSelected); - }); - } - - // 출고 처리 버튼 핸들러 - void _handleOutEquipment() async { - if (_controller.getSelectedInStockCount() == 0) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('출고할 장비를 선택해주세요.'))); - return; - } - - // 선택된 장비들의 요약 정보를 가져와서 출고 폼으로 전달 - final selectedEquipmentsSummary = - _controller.getSelectedEquipmentsSummary(); - - final result = await Navigator.pushNamed( - context, - Routes.equipmentOutAdd, - arguments: {'selectedEquipments': selectedEquipmentsSummary}, - ); - - if (result == true) { - setState(() { - _controller.loadData(); - }); - } - } - - // 대여 처리 버튼 핸들러 - void _handleRentEquipment() async { - if (_controller.getSelectedInStockCount() == 0) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('대여할 장비를 선택해주세요.'))); - return; - } - - // 선택된 장비들의 요약 정보를 가져와서 대여 폼으로 전달 - final selectedEquipmentsSummary = - _controller.getSelectedEquipmentsSummary(); - - // 현재는 대여 기능이 준비되지 않았으므로 간단히 스낵바 표시 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.', - ), - ), - ); - } - - // 폐기 처리 버튼 핸들러 - void _handleDisposeEquipment() { - if (_controller.getSelectedInStockCount() == 0) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('폐기할 장비를 선택해주세요.'))); - return; - } - - // 선택된 장비들의 요약 정보를 가져옴 - final selectedEquipmentsSummary = - _controller.getSelectedEquipmentsSummary(); - - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('폐기 확인'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?', - ), - const SizedBox(height: 16), - const Text( - '폐기할 장비 목록:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - ...selectedEquipmentsSummary.map((equipmentData) { - final equipment = equipmentData['equipment'] as Equipment; - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - '${equipment.manufacturer} ${equipment.name} (${equipment.quantity}개)', - style: const TextStyle(fontSize: 14), - ), - ); - }).toList(), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - // 여기에 폐기 로직 추가 예정 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('폐기 기능은 준비 중입니다.')), - ); - Navigator.pop(context); - }, - child: const Text('폐기'), - ), - ], - ), - ); - } - - // 카테고리 축약 표기 함수 (예: 컴... > 태... > 안드로...) - String _shortenCategory(String category) { - if (category.length <= 2) return category; - return category.substring(0, 2) + '...'; - } - - // 카테고리 툴팁 위젯 (UI만 담당, 축약 표기 적용) - Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) { - final fullCategory = EquipmentDisplayHelper.formatCategory( - equipment.equipment.category, - equipment.equipment.subCategory, - equipment.equipment.subSubCategory, - ); - // 축약 표기 적용 - final shortCategory = [ - _shortenCategory(equipment.equipment.category), - _shortenCategory(equipment.equipment.subCategory), - _shortenCategory(equipment.equipment.subSubCategory), - ].join(' > '); - return Tooltip(message: fullCategory, child: Text(shortCategory)); - } - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32; - String screenTitle = '장비 목록'; - if (widget.currentRoute == Routes.equipmentInList) { - screenTitle = '입고된 장비'; - } else if (widget.currentRoute == Routes.equipmentOutList) { - screenTitle = '출고된 장비'; - } else if (widget.currentRoute == Routes.equipmentRentList) { - screenTitle = '대여된 장비'; - } - final int totalCount = _controller.equipments.length; - final List filteredEquipments = - _appliedSearchKeyword.isEmpty - ? _controller.equipments - : _controller.equipments.where((e) { - final keyword = _appliedSearchKeyword.toLowerCase(); - // 모든 주요 필드에서 검색 - return [ - e.equipment.manufacturer, - e.equipment.name, - e.equipment.category, - e.equipment.subCategory, - e.equipment.subSubCategory, - e.equipment.serialNumber ?? '', - e.equipment.barcode ?? '', - e.equipment.remark ?? '', - e.equipment.warrantyLicense ?? '', - e.notes ?? '', - ].any((field) => field.toLowerCase().contains(keyword)); - }).toList(); - final int filteredCount = filteredEquipments.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > filteredCount - ? filteredCount - : (startIndex + _pageSize); - final pagedEquipments = filteredEquipments.sublist(startIndex, endIndex); - - // 선택된 장비 개수 - final int selectedCount = _controller.getSelectedEquipmentCount(); - final int selectedInCount = _controller.getSelectedInStockCount(); - final int selectedOutCount = _controller.getSelectedEquipmentCountByStatus( - EquipmentStatus.out, - ); - final int selectedRentCount = _controller.getSelectedEquipmentCountByStatus( - EquipmentStatus.rent, - ); - - return MainLayout( - title: screenTitle, - currentRoute: widget.currentRoute, - actions: [ - IconButton( - icon: Icon( - _showDetailedColumns ? Icons.view_column : Icons.view_compact, - color: Colors.grey, - ), - tooltip: _showDetailedColumns ? '간소화된 보기' : '상세 보기', - onPressed: () { - setState(() { - _showDetailedColumns = !_showDetailedColumns; - }); - }, - ), - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - setState(() { - _controller.loadData(); - _currentPage = 1; - }); - }, - color: Colors.grey, - ), - ], - child: Container( - width: maxContentWidth, - padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - screenTitle, - style: AppThemeTailwind.headingStyle, - ), - ), - if (selectedCount > 0) - Container( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(4), - ), - child: Text( - '$selectedCount개 선택됨', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - if (widget.currentRoute == Routes.equipmentInList) - Row( - children: [ - ElevatedButton.icon( - onPressed: - selectedInCount > 0 ? _handleOutEquipment : null, - icon: const Icon( - Icons.exit_to_app, - color: Colors.white, - ), - label: const Text( - '출고', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - disabledBackgroundColor: Colors.blue.withOpacity(0.5), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final result = await Navigator.pushNamed( - context, - Routes.equipmentInAdd, - ); - if (result == true) { - setState(() { - _controller.loadData(); - _currentPage = 1; - }); - } - }, - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - '입고', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 220, - child: Row( - children: [ - Expanded( - child: TextField( - decoration: const InputDecoration( - hintText: '장비 검색', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), - isDense: true, - contentPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 12, - ), - ), - onChanged: (value) { - setState(() { - _searchKeyword = value; - }); - }, - onSubmitted: (value) { - setState(() { - _appliedSearchKeyword = value; - _currentPage = 1; - }); - }, - ), - ), - const SizedBox(width: 4), - IconButton( - icon: const Icon(Icons.arrow_forward), - tooltip: '검색', - onPressed: () { - setState(() { - _appliedSearchKeyword = _searchKeyword; - _currentPage = 1; - }); - }, - ), - ], - ), - ), - ], - ), - // 출고 목록 화면일 때 버튼들 - if (widget.currentRoute == Routes.equipmentOutList) - Row( - children: [ - ElevatedButton.icon( - onPressed: - selectedOutCount > 0 - ? () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('재입고 기능은 준비 중입니다.'), - ), - ); - } - : null, - icon: const Icon( - Icons.assignment_return, - color: Colors.white, - ), - label: const Text( - '재입고', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - disabledBackgroundColor: Colors.green.withOpacity( - 0.5, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: - selectedOutCount > 0 - ? () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('수리 요청 기능은 준비 중입니다.'), - ), - ); - } - : null, - icon: const Icon(Icons.build, color: Colors.white), - label: const Text( - '수리 요청', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - disabledBackgroundColor: Colors.orange.withOpacity( - 0.5, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - // 대여 목록 화면일 때 버튼들 - if (widget.currentRoute == Routes.equipmentRentList) - Row( - children: [ - ElevatedButton.icon( - onPressed: - selectedRentCount > 0 - ? () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('대여 반납 기능은 준비 중입니다.'), - ), - ); - } - : null, - icon: const Icon( - Icons.keyboard_return, - color: Colors.white, - ), - label: const Text( - '반납', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - disabledBackgroundColor: Colors.green.withOpacity( - 0.5, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: - selectedRentCount > 0 - ? () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('대여 연장 기능은 준비 중입니다.'), - ), - ); - } - : null, - icon: const Icon(Icons.date_range, color: Colors.white), - label: const Text( - '연장', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - disabledBackgroundColor: Colors.blue.withOpacity(0.5), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ], - ), - const SizedBox(height: 8), - Expanded( - child: - pagedEquipments.isEmpty - ? const Center(child: Text('장비 정보가 없습니다.')) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: maxContentWidth, - ), - child: EquipmentTable( - equipments: pagedEquipments, - selectedEquipmentIds: - _controller.selectedEquipmentIds, - showDetailedColumns: _showDetailedColumns, - onEquipmentSelected: _onEquipmentSelected, - getOutEquipmentInfo: - _controller.getOutEquipmentInfo, - buildCategoryWithTooltip: _buildCategoryWithTooltip, - // 수정 버튼 동작: 입고 폼(수정 모드)로 이동 - onEdit: (id, status) async { - if (status == EquipmentStatus.in_) { - final result = await Navigator.pushNamed( - context, - Routes.equipmentInEdit, - arguments: id, - ); - if (result == true) { - setState(() { - _controller.loadData(); - }); - } - } else { - // 출고/대여 등은 별도 폼으로 이동 필요시 구현 - } - }, - // 삭제 버튼 동작: 삭제 다이얼로그 및 삭제 처리 - onDelete: (id, status) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('삭제 확인'), - content: const Text('이 장비 정보를 삭제하시겠습니까?'), - actions: [ - TextButton( - onPressed: - () => Navigator.pop(context), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - setState(() { - // 입고/출고 상태에 따라 삭제 처리 - if (status == - EquipmentStatus.in_) { - MockDataService() - .deleteEquipmentIn(id); - } else if (status == - EquipmentStatus.out) { - MockDataService() - .deleteEquipmentOut(id); - } - _controller.loadData(); - }); - Navigator.pop(context); - }, - child: const Text('삭제'), - ), - ], - ), - ); - }, - getSelectedInStockCount: - _controller.getSelectedInStockCount, - ), - ), - ), - ), - if (totalCount > _pageSize) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Pagination( - totalCount: filteredCount, - currentPage: _currentPage, - pageSize: _pageSize, - onPageChanged: (page) { - setState(() { - _currentPage = page; - }); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/equipment/widgets/equipment_table.dart b/lib/screens/equipment/widgets/equipment_table.dart deleted file mode 100644 index 71b936b..0000000 --- a/lib/screens/equipment/widgets/equipment_table.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/screens/equipment/widgets/equipment_status_chip.dart'; -import 'package:superport/screens/equipment/widgets/equipment_out_info.dart'; -import 'package:superport/utils/equipment_display_helper.dart'; - -// 장비 목록 테이블 위젯 (SRP, 재사용성 강화) -class EquipmentTable extends StatelessWidget { - final List equipments; - final Set selectedEquipmentIds; - final bool showDetailedColumns; - final void Function(int? id, String status, bool? isSelected) - onEquipmentSelected; - final String Function(int equipmentId, String infoType) getOutEquipmentInfo; - final Widget Function(UnifiedEquipment equipment) buildCategoryWithTooltip; - final void Function(int id, String status) onEdit; - final void Function(int id, String status) onDelete; - final int Function() getSelectedInStockCount; - - const EquipmentTable({ - super.key, - required this.equipments, - required this.selectedEquipmentIds, - required this.showDetailedColumns, - required this.onEquipmentSelected, - required this.getOutEquipmentInfo, - required this.buildCategoryWithTooltip, - required this.onEdit, - required this.onDelete, - required this.getSelectedInStockCount, - }); - - // 출고 정보(간소화 모드) 위젯 - Widget _buildCompactOutInfo(int equipmentId) { - final company = getOutEquipmentInfo(equipmentId, 'company'); - final manager = getOutEquipmentInfo(equipmentId, 'manager'); - final license = getOutEquipmentInfo(equipmentId, 'license'); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - EquipmentOutInfoIcon(infoType: 'company', text: company), - const SizedBox(height: 2), - EquipmentOutInfoIcon(infoType: 'manager', text: manager), - const SizedBox(height: 2), - EquipmentOutInfoIcon(infoType: 'license', text: license), - ], - ); - } - - // 카테고리 툴팁 위젯 (UI만 담당, 축약 표기 적용) - Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) { - // 한글 라벨로 표기 - final fullCategory = - '대분류: ${equipment.equipment.category} / 중분류: ${equipment.equipment.subCategory} / 소분류: ${equipment.equipment.subSubCategory}'; - final shortCategory = [ - _shortenCategory(equipment.equipment.category), - _shortenCategory(equipment.equipment.subCategory), - _shortenCategory(equipment.equipment.subSubCategory), - ].join(' > '); - return Tooltip(message: fullCategory, child: Text(shortCategory)); - } - - // 카테고리 축약 표기 함수 (예: 컴...) - String _shortenCategory(String category) { - if (category.length <= 2) return category; - return category.substring(0, 2) + '...'; - } - - @override - Widget build(BuildContext context) { - return DataTable( - headingRowHeight: 48, - dataRowMinHeight: 48, - dataRowMaxHeight: 60, - columnSpacing: 10, - horizontalMargin: 16, - columns: [ - const DataColumn(label: SizedBox(width: 32, child: Text('선택'))), - const DataColumn(label: SizedBox(width: 32, child: Text('번호'))), - if (showDetailedColumns) - const DataColumn(label: SizedBox(width: 60, child: Text('제조사'))), - const DataColumn(label: SizedBox(width: 90, child: Text('장비명'))), - if (showDetailedColumns) - const DataColumn(label: SizedBox(width: 110, child: Text('분류'))), - if (showDetailedColumns) - const DataColumn(label: SizedBox(width: 60, child: Text('장비 유형'))), - if (showDetailedColumns) - const DataColumn(label: SizedBox(width: 70, child: Text('시리얼번호'))), - const DataColumn(label: SizedBox(width: 38, child: Text('수량'))), - const DataColumn(label: SizedBox(width: 80, child: Text('변경 일자'))), - const DataColumn(label: SizedBox(width: 44, child: Text('상태'))), - if (showDetailedColumns) ...[ - const DataColumn(label: SizedBox(width: 90, child: Text('출고 회사'))), - const DataColumn(label: SizedBox(width: 60, child: Text('담당자'))), - const DataColumn(label: SizedBox(width: 60, child: Text('라이센스'))), - ] else - const DataColumn(label: SizedBox(width: 110, child: Text('출고 정보'))), - const DataColumn(label: SizedBox(width: 60, child: Text('관리'))), - ], - rows: - equipments.asMap().entries.map((entry) { - final index = entry.key; - final equipment = entry.value; - final bool isInStock = equipment.status == 'I'; - final bool isOutStock = equipment.status == 'O'; - return DataRow( - color: MaterialStateProperty.resolveWith( - (Set states) => - index % 2 == 0 ? Colors.grey[50] : null, - ), - cells: [ - DataCell( - Checkbox( - value: selectedEquipmentIds.contains( - '${equipment.id}:${equipment.status}', - ), - onChanged: - (isSelected) => onEquipmentSelected( - equipment.id, - equipment.status, - isSelected, - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - DataCell(Text('${index + 1}')), - if (showDetailedColumns) - DataCell( - Text( - EquipmentDisplayHelper.formatManufacturer( - equipment.equipment.manufacturer, - ), - ), - ), - DataCell( - Text( - EquipmentDisplayHelper.formatEquipmentName( - equipment.equipment.name, - ), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - if (showDetailedColumns) - DataCell(buildCategoryWithTooltip(equipment)), - if (showDetailedColumns) - DataCell( - Text( - equipment.status == 'I' && - equipment is UnifiedEquipment && - equipment.type != null - ? equipment.type! - : '-', - ), - ), - if (showDetailedColumns) - DataCell( - Text( - EquipmentDisplayHelper.formatSerialNumber( - equipment.equipment.serialNumber, - ), - ), - ), - DataCell( - Text( - '${equipment.equipment.quantity}', - textAlign: TextAlign.center, - ), - ), - DataCell( - Text(EquipmentDisplayHelper.formatDate(equipment.date)), - ), - DataCell(EquipmentStatusChip(status: equipment.status)), - if (showDetailedColumns) ...[ - DataCell( - Text( - isOutStock - ? getOutEquipmentInfo(equipment.id!, 'company') - : '-', - ), - ), - DataCell( - Text( - isOutStock - ? getOutEquipmentInfo(equipment.id!, 'manager') - : '-', - ), - ), - DataCell( - Text( - isOutStock - ? getOutEquipmentInfo(equipment.id!, 'license') - : '-', - ), - ), - ] else - DataCell( - isOutStock - ? _buildCompactOutInfo(equipment.id!) - : const Text('-'), - ), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.blue, - size: 20, - ), - constraints: const BoxConstraints(), - padding: const EdgeInsets.all(5), - onPressed: - () => onEdit(equipment.id!, equipment.status), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.red, - size: 20, - ), - constraints: const BoxConstraints(), - padding: const EdgeInsets.all(5), - onPressed: - () => onDelete(equipment.id!, equipment.status), - ), - ], - ), - ), - ], - ); - }).toList(), - ); - } -} diff --git a/lib/screens/license/license_list.dart b/lib/screens/license/license_list.dart deleted file mode 100644 index 9a58d25..0000000 --- a/lib/screens/license/license_list.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/license_model.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/services/mock_data_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/license/controllers/license_list_controller.dart'; -import 'package:superport/screens/license/widgets/license_table.dart'; -import 'package:superport/screens/common/widgets/pagination.dart'; - -// 유지보수 목록 화면 (UI만 담당, 상태/로직/테이블 분리) -class MaintenanceListScreen extends StatefulWidget { - const MaintenanceListScreen({super.key}); - - @override - State createState() => _MaintenanceListScreenState(); -} - -// 유지보수 목록 화면의 상태 클래스 -class _MaintenanceListScreenState extends State { - late final LicenseListController _controller; - // 페이지네이션 상태 추가 - int _currentPage = 1; - final int _pageSize = 10; - - @override - void initState() { - super.initState(); - _controller = LicenseListController(dataService: MockDataService()); - _controller.loadData(); - } - - void _reload() { - setState(() { - _controller.loadData(); - }); - } - - void _navigateToAddScreen() async { - final result = await Navigator.pushNamed(context, '/license/add'); - if (result == true) { - _reload(); - } - } - - void _navigateToEditScreen(int id) async { - final result = await Navigator.pushNamed( - context, - '/license/edit', - arguments: id, - ); - if (result == true) { - _reload(); - } - } - - void _deleteLicense(int id) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('삭제 확인'), - content: const Text('이 라이센스 정보를 삭제하시겠습니까?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - setState(() { - _controller.deleteLicense(id); - }); - Navigator.pop(context); - }, - child: const Text('삭제'), - ), - ], - ), - ); - } - - // 회사명 반환 함수 (재사용성 위해 분리) - String _getCompanyName(int companyId) { - return MockDataService().getCompanyById(companyId)?.name ?? '-'; - } - - @override - Widget build(BuildContext context) { - // 대시보드 폭에 맞게 조정 - final screenWidth = MediaQuery.of(context).size.width; - final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32; - - // 페이지네이션 데이터 슬라이싱 - final int totalCount = _controller.licenses.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > totalCount - ? totalCount - : (startIndex + _pageSize); - final pagedLicenses = _controller.licenses.sublist(startIndex, endIndex); - - return MainLayout( - title: '유지보수 관리', - currentRoute: Routes.license, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _reload, - color: Colors.grey, - ), - ], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PageTitle( - title: '유지보수 목록', - width: maxContentWidth - 32, - rightWidget: ElevatedButton.icon( - onPressed: _navigateToAddScreen, - icon: const Icon(Icons.add), - label: const Text('추가'), - style: AppThemeTailwind.primaryButtonStyle, - ), - ), - Expanded( - child: DataTableCard( - width: maxContentWidth - 32, - child: - pagedLicenses.isEmpty - ? const Center(child: Text('등록된 라이센스 정보가 없습니다.')) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - constraints: BoxConstraints( - minWidth: maxContentWidth - 64, - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: LicenseTable( - licenses: pagedLicenses, - getCompanyName: _getCompanyName, - onEdit: _navigateToEditScreen, - onDelete: _deleteLicense, - ), - ), - ), - ), - ), - ), - // 페이지네이션 위젯 추가 - if (totalCount > _pageSize) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Pagination( - totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, - onPageChanged: (page) { - setState(() { - _currentPage = page; - }); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/license/widgets/license_table.dart b/lib/screens/license/widgets/license_table.dart deleted file mode 100644 index 3c3d5dc..0000000 --- a/lib/screens/license/widgets/license_table.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/license_model.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 라이센스 목록 테이블 위젯 (SRP, 재사용성) -class LicenseTable extends StatelessWidget { - final List licenses; - final String Function(int companyId) getCompanyName; - final void Function(int id) onEdit; - final void Function(int id) onDelete; - - const LicenseTable({ - super.key, - required this.licenses, - required this.getCompanyName, - required this.onEdit, - required this.onDelete, - }); - - @override - Widget build(BuildContext context) { - return DataTable( - columns: const [ - DataColumn(label: Text('번호')), - DataColumn(label: Text('유지보수명')), - DataColumn(label: Text('기간')), - DataColumn(label: Text('방문주기')), - DataColumn(label: Text('점검형태')), - DataColumn(label: Text('관리')), - ], - rows: - licenses.map((license) { - // name에서 기간, 방문주기, 점검형태 파싱 (예: '12개월,격월,방문') - final parts = license.name.split(','); - final period = parts.isNotEmpty ? parts[0] : '-'; - final visit = parts.length > 1 ? parts[1] : '-'; - final inspection = parts.length > 2 ? parts[2] : '-'; - return DataRow( - cells: [ - DataCell(Text('${license.id}')), - DataCell(Text(license.name)), - DataCell(Text(period)), - DataCell(Text(visit)), - DataCell(Text(inspection)), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: AppThemeTailwind.primary, - ), - onPressed: () => onEdit(license.id!), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: AppThemeTailwind.danger, - ), - onPressed: () => onDelete(license.id!), - ), - ], - ), - ), - ], - ); - }).toList(), - ); - } -} diff --git a/lib/screens/login/widgets/login_view.dart b/lib/screens/login/widgets/login_view.dart deleted file mode 100644 index 9705c68..0000000 --- a/lib/screens/login/widgets/login_view.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'dart:math' as math; -import 'package:wave/wave.dart'; -import 'package:wave/config.dart'; -import 'package:superport/screens/login/controllers/login_controller.dart'; -import 'package:provider/provider.dart'; - -/// 로그인 화면 진입점 위젯 (controller를 ChangeNotifierProvider로 주입) -class LoginView extends StatelessWidget { - final LoginController controller; - final VoidCallback onLoginSuccess; - const LoginView({ - Key? key, - required this.controller, - required this.onLoginSuccess, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: controller, - child: const _LoginViewBody(), - ); - } -} - -/// 로그인 화면 전체 레이아웃 및 애니메이션 배경 -class _LoginViewBody extends StatelessWidget { - const _LoginViewBody({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - // wave 패키지로 wavy liquid 애니메이션 배경 적용 - Positioned.fill( - child: WaveWidget( - config: CustomConfig( - gradients: [ - [Color(0xFFF7FAFC), Color(0xFFB6E0FE)], - [Color(0xFFB6E0FE), Color(0xFF3182CE)], - [Color(0xFF3182CE), Color(0xFF243B53)], - ], - durations: [4200, 5000, 7000], - heightPercentages: [0.18, 0.25, 0.38], - blur: const MaskFilter.blur(BlurStyle.solid, 8), - gradientBegin: Alignment.topLeft, - gradientEnd: Alignment.bottomRight, - ), - waveAmplitude: 18, - size: Size.infinite, - ), - ), - Center( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 40, - horizontal: 32, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 32, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - AnimatedBoatIcon(), - SizedBox(height: 32), - Text('supERPort', style: AppThemeTailwind.headingStyle), - SizedBox(height: 24), - LoginForm(), - SizedBox(height: 16), - SaveIdCheckbox(), - SizedBox(height: 32), - LoginButton(), - SizedBox(height: 48), - ], - ), - ), - ), - ), - ), - // 카피라이트를 화면 중앙 하단에 고정 - Positioned( - left: 0, - right: 0, - bottom: 32, - child: Center( - child: Opacity( - opacity: 0.7, - child: Text( - 'Copyright 2025 CClabs. All rights reserved.', - style: AppThemeTailwind.smallText.copyWith(fontSize: 13), - ), - ), - ), - ), - ], - ), - ); - } -} - -/// 요트 아이콘 애니메이션 위젯 -class AnimatedBoatIcon extends StatefulWidget { - final Color color; - final double size; - const AnimatedBoatIcon({ - Key? key, - this.color = const Color(0xFF3182CE), - this.size = 80, - }) : super(key: key); - @override - State createState() => _AnimatedBoatIconState(); -} - -class _AnimatedBoatIconState extends State - with TickerProviderStateMixin { - late AnimationController _boatGrowController; - late Animation _boatScaleAnim; - late AnimationController _boatFloatController; - late Animation _boatFloatAnim; - - @override - void initState() { - super.initState(); - _boatGrowController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1100), - ); - _boatScaleAnim = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _boatGrowController, curve: Curves.elasticOut), - ); - _boatFloatController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1800), - ); - _boatFloatAnim = Tween(begin: -0.08, end: 0.08).animate( - CurvedAnimation(parent: _boatFloatController, curve: Curves.easeInOut), - ); - _boatGrowController.forward().then((_) { - _boatFloatController.repeat(reverse: true); - }); - } - - @override - void dispose() { - _boatGrowController.dispose(); - _boatFloatController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: Listenable.merge([_boatGrowController, _boatFloatController]), - builder: (context, child) { - final double scale = _boatScaleAnim.value; - final double angle = - (_boatGrowController.isCompleted) ? _boatFloatAnim.value : 0.0; - return Transform.translate( - offset: Offset( - (_boatGrowController.isCompleted) ? math.sin(angle) * 8 : 0, - 0, - ), - child: Transform.rotate( - angle: angle, - child: Transform.scale(scale: scale, child: child), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: widget.color.withOpacity(0.18), - blurRadius: widget.size * 0.3, - offset: Offset(0, widget.size * 0.1), - ), - ], - ), - child: Icon( - Icons.directions_boat, - size: widget.size, - color: widget.color, - ), - ), - ); - } -} - -/// 로그인 입력 폼 위젯 (ID, PW) -class LoginForm extends StatelessWidget { - const LoginForm({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - final controller = Provider.of(context); - return Column( - children: [ - TextField( - controller: controller.idController, - focusNode: controller.idFocus, - decoration: const InputDecoration( - labelText: 'ID', - border: OutlineInputBorder(), - ), - style: AppThemeTailwind.bodyStyle, - textInputAction: TextInputAction.next, - onSubmitted: (_) { - FocusScope.of(context).requestFocus(controller.pwFocus); - }, - ), - const SizedBox(height: 16), - TextField( - controller: controller.pwController, - focusNode: controller.pwFocus, - decoration: const InputDecoration( - labelText: 'PW', - border: OutlineInputBorder(), - ), - style: AppThemeTailwind.bodyStyle, - obscureText: true, - textInputAction: TextInputAction.done, - onSubmitted: (_) { - // 엔터 시 로그인 버튼에 포커스 이동 또는 로그인 시도 가능 - }, - ), - ], - ); - } -} - -/// 아이디 저장 체크박스 위젯 -class SaveIdCheckbox extends StatelessWidget { - const SaveIdCheckbox({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - final controller = Provider.of(context); - return Row( - children: [ - Checkbox( - value: controller.saveId, - onChanged: (bool? value) { - controller.setSaveId(value ?? false); - }, - ), - Text('아이디 저장', style: AppThemeTailwind.bodyStyle), - ], - ); - } -} - -/// 로그인 버튼 위젯 -class LoginButton extends StatelessWidget { - const LoginButton({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - final controller = Provider.of(context, listen: false); - final onLoginSuccess = - (context.findAncestorWidgetOfExactType() as LoginView) - .onLoginSuccess; - return SizedBox( - width: double.infinity, - child: ElevatedButton( - style: AppThemeTailwind.primaryButtonStyle.copyWith( - elevation: MaterialStateProperty.all(4), - shadowColor: MaterialStateProperty.all( - const Color(0xFF3182CE).withOpacity(0.18), - ), - ), - onPressed: () async { - final bool result = controller.login(); - if (!result) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('로그인에 실패했습니다.'))); - return; - } - // 로그인 성공 시 애니메이션 등은 필요시 별도 처리 - onLoginSuccess(); - }, - child: const Text('로그인'), - ), - ); - } -} diff --git a/lib/screens/login/widgets/login_view_redesign.dart b/lib/screens/login/widgets/login_view_redesign.dart index 806cb2b..8cf0ae3 100644 --- a/lib/screens/login/widgets/login_view_redesign.dart +++ b/lib/screens/login/widgets/login_view_redesign.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/login/controllers/login_controller.dart'; -import 'package:provider/provider.dart'; import 'dart:math' as math; /// shadcn/ui 스타일로 재설계된 로그인 화면 @@ -73,34 +72,32 @@ class _LoginViewRedesignState extends State @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: widget.controller, - child: Consumer( - builder: (context, controller, _) { - return Scaffold( - backgroundColor: ShadcnTheme.background, - body: SafeArea( - child: Center( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.all(ShadcnTheme.spacing6), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildHeader(), - const SizedBox(height: ShadcnTheme.spacing12), - _buildLoginCard(), - const SizedBox(height: ShadcnTheme.spacing8), - _buildFooter(), - ], - ), + return ListenableBuilder( + listenable: widget.controller, + builder: (context, _) { + return Scaffold( + backgroundColor: ShadcnTheme.background, + body: SafeArea( + child: Center( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(ShadcnTheme.spacing6), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildHeader(), + const SizedBox(height: ShadcnTheme.spacing12), + _buildLoginCard(), + const SizedBox(height: ShadcnTheme.spacing8), + _buildFooter(), + ], ), ), ), @@ -108,9 +105,9 @@ class _LoginViewRedesignState extends State ), ), ), - ); - }, - ), + ), + ); + }, ); } @@ -166,13 +163,13 @@ class _LoginViewRedesignState extends State ), ), const SizedBox(height: ShadcnTheme.spacing2), - Text('스마트 포트 관리 시스템', style: ShadcnTheme.bodyMuted), + Text('스마트 ERP 시스템', style: ShadcnTheme.bodyMuted), ], ); } Widget _buildLoginCard() { - final controller = context.watch(); + final controller = widget.controller; return ShadcnCard( padding: const EdgeInsets.all(ShadcnTheme.spacing8), child: Column( @@ -225,7 +222,7 @@ class _LoginViewRedesignState extends State ], ), const SizedBox(height: ShadcnTheme.spacing8), - + // 에러 메시지 표시 if (controller.errorMessage != null) Container( @@ -274,9 +271,7 @@ class _LoginViewRedesignState extends State ShadcnButton( text: '테스트 로그인', onPressed: () { - controller.idController.text = 'admin@example.com'; - controller.pwController.text = 'admin123'; - _handleLogin(); + widget.onLoginSuccess(); }, variant: ShadcnButtonVariant.secondary, size: ShadcnButtonSize.medium, @@ -326,7 +321,7 @@ class _LoginViewRedesignState extends State // 저작권 정보 Text( - 'Copyright 2025 CClabs. All rights reserved.', + 'Copyright 2025 NatureBridgeAI. All rights reserved.', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foreground.withOpacity(0.7), fontWeight: FontWeight.w500, diff --git a/lib/screens/overview/overview_screen.dart b/lib/screens/overview/overview_screen.dart deleted file mode 100644 index 48886f3..0000000 --- a/lib/screens/overview/overview_screen.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/layout_components.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/services/mock_data_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/overview/controllers/overview_controller.dart'; -import 'package:superport/screens/overview/widgets/stats_grid.dart'; -import 'package:superport/screens/overview/widgets/recent_activities_list.dart'; - -// 대시보드(Overview) 화면 (UI만 담당, 상태/로직/위젯 분리) -class OverviewScreen extends StatefulWidget { - const OverviewScreen({Key? key}) : super(key: key); - - @override - _OverviewScreenState createState() => _OverviewScreenState(); -} - -class _OverviewScreenState extends State { - late final OverviewController _controller; - - @override - void initState() { - super.initState(); - _controller = OverviewController(dataService: MockDataService()); - _controller.loadData(); - } - - void _reload() { - setState(() { - _controller.loadData(); - }); - } - - @override - Widget build(BuildContext context) { - // 전체 배경색을 회색(AppThemeTailwind.surface)으로 지정 - return Container( - color: AppThemeTailwind.surface, // 회색 배경 - child: MainLayout( - title: '', // 타이틀 없음 - currentRoute: Routes.home, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _reload, - color: AppThemeTailwind.muted, - ), - IconButton( - icon: const Icon(Icons.notifications_none), - onPressed: () {}, - color: AppThemeTailwind.muted, - ), - IconButton( - icon: const Icon(Icons.logout), - tooltip: '로그아웃', - onPressed: () { - Navigator.of(context).pushReplacementNamed('/login'); - }, - color: AppThemeTailwind.muted, - ), - ], - child: SingleChildScrollView( - padding: EdgeInsets.zero, // 여백 0 - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 상단 경로 표기 완전 삭제 - // 하단부 전체를 감싸는 라운드 흰색 박스 - Container( - margin: const EdgeInsets.all(4), // 외부 여백만 적용 - decoration: BoxDecoration( - color: Colors.white, // 흰색 배경 - borderRadius: BorderRadius.circular(24), // 라운드 처리 - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - padding: const EdgeInsets.all(32), // 내부 여백 유지 - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 통계 카드 그리드 - Container( - margin: const EdgeInsets.only(bottom: 32), - child: StatsGrid( - totalCompanies: _controller.totalCompanies, - totalUsers: _controller.totalUsers, - totalLicenses: _controller.totalLicenses, - totalEquipmentIn: _controller.totalEquipmentIn, - totalEquipmentOut: _controller.totalEquipmentOut, - ), - ), - _buildActivitySection(), - const SizedBox(height: 32), - _buildRecentItemsSection(), - ], - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildActivitySection() { - // MetronicCard로 감싸고, 섹션 헤더 스타일 통일 - return MetronicCard( - title: '시스템 활동', - margin: const EdgeInsets.only(bottom: 32), - child: Column( - children: [ - _buildActivityChart(), - const SizedBox(height: 20), - const Divider(color: Color(0xFFF3F6F9)), - const SizedBox(height: 20), - _buildActivityLegend(), - ], - ), - ); - } - - Widget _buildActivityChart() { - // Metronic 스타일: 카드 내부 차트 영역, 라운드, 밝은 배경, 컬러 강조 - return Container( - height: 200, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: AppThemeTailwind.light, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.bar_chart, - size: 56, - color: AppThemeTailwind.primary, - ), - const SizedBox(height: 18), - Text('월별 장비 입/출고 추이', style: AppThemeTailwind.subheadingStyle), - const SizedBox(height: 10), - Text( - '실제 구현 시 차트 라이브러리 (fl_chart 등) 사용', - style: AppThemeTailwind.smallText, - ), - ], - ), - ); - } - - Widget _buildActivityLegend() { - // Metronic 스타일: 라운드, 컬러, 폰트 통일 - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildLegendItem('장비 입고', AppThemeTailwind.success), - const SizedBox(width: 32), - _buildLegendItem('장비 출고', AppThemeTailwind.warning), - const SizedBox(width: 32), - _buildLegendItem('라이센스 등록', AppThemeTailwind.info), - ], - ); - } - - Widget _buildLegendItem(String text, Color color) { - // Metronic 스타일: 컬러 원, 텍스트, 여백 - return Row( - children: [ - Container( - width: 14, - height: 14, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ), - const SizedBox(width: 10), - Text( - text, - style: AppThemeTailwind.smallText.copyWith( - fontWeight: FontWeight.w600, - color: AppThemeTailwind.dark, - ), - ), - ], - ); - } - - Widget _buildRecentItemsSection() { - // Metronic 스타일: 카드, 섹션 헤더, 리스트 여백/컬러 통일 - return MetronicCard( - title: '최근 활동', - child: Column( - children: [ - const Divider(indent: 0, endIndent: 0, color: Color(0xFFF3F6F9)), - const SizedBox(height: 16), - RecentActivitiesList(recentActivities: _controller.recentActivities), - const SizedBox(height: 8), - ], - ), - ); - } -} diff --git a/lib/screens/overview/widgets/recent_activities_list.dart b/lib/screens/overview/widgets/recent_activities_list.dart deleted file mode 100644 index 9e3ec6e..0000000 --- a/lib/screens/overview/widgets/recent_activities_list.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 최근 활동 리스트 위젯 (SRP, 재사용성) -class RecentActivitiesList extends StatelessWidget { - final List> recentActivities; - const RecentActivitiesList({super.key, required this.recentActivities}); - - @override - Widget build(BuildContext context) { - return Column( - children: - recentActivities.map((activity) { - return Column( - children: [ - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: activity['color'] as Color, - shape: BoxShape.circle, - ), - child: Icon( - activity['icon'] as IconData, - color: Colors.white, - size: 20, - ), - ), - title: Text( - activity['title'] as String, - style: AppThemeTailwind.subheadingStyle, - ), - subtitle: Text( - '${activity['type']} • ${activity['user']}', - style: AppThemeTailwind.smallText, - ), - trailing: Text( - activity['time'] as String, - style: AppThemeTailwind.smallText.copyWith( - color: AppThemeTailwind.muted, - ), - ), - ), - if (activity != recentActivities.last) - const Divider( - height: 1, - indent: 68, - endIndent: 16, - color: (Color(0xFFEEEEF2)), - ), - ], - ); - }).toList(), - ); - } -} diff --git a/lib/screens/overview/widgets/stats_grid.dart b/lib/screens/overview/widgets/stats_grid.dart deleted file mode 100644 index c7988a8..0000000 --- a/lib/screens/overview/widgets/stats_grid.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/layout_components.dart'; - -// 대시보드 통계 카드 그리드 위젯 (SRP, 재사용성) -class StatsGrid extends StatelessWidget { - final int totalCompanies; - final int totalUsers; - final int totalLicenses; - final int totalEquipmentIn; - final int totalEquipmentOut; - - const StatsGrid({ - super.key, - required this.totalCompanies, - required this.totalUsers, - required this.totalLicenses, - required this.totalEquipmentIn, - required this.totalEquipmentOut, - }); - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: 3, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - shrinkWrap: true, - childAspectRatio: 2.5, - physics: const NeverScrollableScrollPhysics(), - children: [ - MetronicStatsCard( - title: '등록된 회사', - value: '$totalCompanies', - icon: Icons.business, - iconBackgroundColor: AppThemeTailwind.info, - showTrend: true, - trendPercentage: 2.5, - isPositiveTrend: true, - ), - MetronicStatsCard( - title: '등록된 사용자', - value: '$totalUsers', - icon: Icons.person, - iconBackgroundColor: AppThemeTailwind.primary, - showTrend: true, - trendPercentage: 3.7, - isPositiveTrend: true, - ), - MetronicStatsCard( - title: '유효 라이센스', - value: '$totalLicenses', - icon: Icons.vpn_key, - iconBackgroundColor: AppThemeTailwind.secondary, - ), - MetronicStatsCard( - title: '총 장비 입고', - value: '$totalEquipmentIn', - icon: Icons.input, - iconBackgroundColor: AppThemeTailwind.success, - showTrend: true, - trendPercentage: 1.8, - isPositiveTrend: true, - ), - MetronicStatsCard( - title: '총 장비 출고', - value: '$totalEquipmentOut', - icon: Icons.output, - iconBackgroundColor: AppThemeTailwind.warning, - ), - MetronicStatsCard( - title: '현재 재고', - value: '${totalEquipmentIn - totalEquipmentOut}', - icon: Icons.inventory_2, - iconBackgroundColor: AppThemeTailwind.danger, - showTrend: true, - trendPercentage: 0.7, - isPositiveTrend: false, - ), - ], - ); - } -} diff --git a/lib/screens/sidebar/sidebar_screen.dart b/lib/screens/sidebar/sidebar_screen.dart deleted file mode 100644 index a57563f..0000000 --- a/lib/screens/sidebar/sidebar_screen.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_header.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_footer.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_item.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_submenu.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_types.dart'; - -// 사이드바 메뉴 메인 위젯 (조립만 담당) -class SidebarMenu extends StatefulWidget { - final String currentRoute; - final Function(String) onRouteChanged; - - const SidebarMenu({ - super.key, - required this.currentRoute, - required this.onRouteChanged, - }); - - @override - State createState() => _SidebarMenuState(); -} - -class _SidebarMenuState extends State { - // 장비 관리 메뉴 확장 상태 - bool _isEquipmentMenuExpanded = false; - // hover 상태 관리 - String? _hoveredRoute; - - @override - void initState() { - super.initState(); - _updateExpandedState(); - } - - @override - void didUpdateWidget(SidebarMenu oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.currentRoute != widget.currentRoute) { - _updateExpandedState(); - } - } - - // 현재 경로에 따라 장비 관리 메뉴 확장 상태 업데이트 - void _updateExpandedState() { - final bool isEquipmentRoute = - widget.currentRoute == Routes.equipment || - widget.currentRoute == Routes.equipmentInList || - widget.currentRoute == Routes.equipmentOutList || - widget.currentRoute == Routes.equipmentRentList; - setState(() { - _isEquipmentMenuExpanded = isEquipmentRoute; - }); - } - - // 장비 관리 메뉴 확장/축소 토글 - void _toggleEquipmentMenu() { - setState(() { - _isEquipmentMenuExpanded = !_isEquipmentMenuExpanded; - }); - } - - @override - Widget build(BuildContext context) { - // SRP 분할: 각 역할별 위젯 조립 - return Container( - width: 260, - color: const Color(0xFFF4F6F8), // 연회색 배경 - child: Column( - children: [ - const SidebarMenuHeader(), - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SidebarMenuItem( - icon: Icons.dashboard, - title: '대시보드', - route: Routes.home, - isActive: widget.currentRoute == Routes.home, - isHovered: _hoveredRoute == Routes.home, - onTap: () => widget.onRouteChanged(Routes.home), - ), - const SizedBox(height: 4), - SidebarMenuWithSubmenu( - icon: Icons.inventory, - title: '장비 관리', - route: Routes.equipment, - subItems: const [ - SidebarSubMenuItem( - title: '입고', - route: Routes.equipmentInList, - ), - SidebarSubMenuItem( - title: '출고', - route: Routes.equipmentOutList, - ), - SidebarSubMenuItem( - title: '대여', - route: Routes.equipmentRentList, - ), - ], - isExpanded: _isEquipmentMenuExpanded, - isMenuActive: widget.currentRoute == Routes.equipment, - isSubMenuActive: [ - Routes.equipmentInList, - Routes.equipmentOutList, - Routes.equipmentRentList, - ].contains(widget.currentRoute), - isHovered: _hoveredRoute == Routes.equipment, - onToggleExpanded: _toggleEquipmentMenu, - currentRoute: widget.currentRoute, - onRouteChanged: widget.onRouteChanged, - ), - const SizedBox(height: 4), - SidebarMenuItem( - icon: Icons.location_on, - title: '입고지 관리', - route: Routes.warehouseLocation, - isActive: widget.currentRoute == Routes.warehouseLocation, - isHovered: _hoveredRoute == Routes.warehouseLocation, - onTap: - () => widget.onRouteChanged(Routes.warehouseLocation), - ), - const SizedBox(height: 4), - SidebarMenuItem( - icon: Icons.business, - title: '회사 관리', - route: Routes.company, - isActive: widget.currentRoute == Routes.company, - isHovered: _hoveredRoute == Routes.company, - onTap: () => widget.onRouteChanged(Routes.company), - ), - const SizedBox(height: 4), - SidebarMenuItem( - icon: Icons.vpn_key, - title: '유지보수 관리', - route: Routes.license, - isActive: widget.currentRoute == Routes.license, - isHovered: _hoveredRoute == Routes.license, - onTap: () => widget.onRouteChanged(Routes.license), - ), - ], - ), - ), - ), - ), - const SidebarMenuFooter(), - ], - ), - ); - } -} diff --git a/lib/screens/sidebar/widgets/sidebar_menu_footer.dart b/lib/screens/sidebar/widgets/sidebar_menu_footer.dart deleted file mode 100644 index f06b439..0000000 --- a/lib/screens/sidebar/widgets/sidebar_menu_footer.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -// 사이드바 푸터 위젯 -class SidebarMenuFooter extends StatelessWidget { - const SidebarMenuFooter({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 48, - alignment: Alignment.center, - child: const Text( - '© 2025 CClabs. All rights reserved.', - style: TextStyle(fontSize: 11, color: Colors.black), // 블랙으로 변경 - ), - ); - } -} diff --git a/lib/screens/sidebar/widgets/sidebar_menu_header.dart b/lib/screens/sidebar/widgets/sidebar_menu_header.dart deleted file mode 100644 index 2c2e9d1..0000000 --- a/lib/screens/sidebar/widgets/sidebar_menu_header.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:wave/wave.dart'; -import 'package:wave/config.dart'; -import 'package:superport/screens/login/widgets/login_view.dart'; // AnimatedBoatIcon import - -// 사이드바 헤더 위젯 -class SidebarMenuHeader extends StatelessWidget { - const SidebarMenuHeader({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 88, - width: double.infinity, - padding: const EdgeInsets.only(left: 0, right: 0), // 아이콘을 더 좌측으로 - child: Stack( - alignment: Alignment.centerLeft, - children: [ - // Wave 배경 - Positioned.fill( - child: Opacity( - opacity: 0.50, // subtle하게 - child: WaveWidget( - config: CustomConfig( - gradients: [ - [Color(0xFFB6E0FE), Color(0xFF3182CE)], - [ - Color.fromARGB(255, 31, 83, 132), - Color.fromARGB(255, 9, 49, 92), - ], - ], - durations: [4800, 6000], - heightPercentages: [0.48, 0.38], - blur: const MaskFilter.blur(BlurStyle.solid, 6), - gradientBegin: Alignment.topLeft, - gradientEnd: Alignment.bottomRight, - ), - waveAmplitude: 8, - size: Size.infinite, - ), - ), - ), - // 아이콘+텍스트 - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(width: 24), // 아이콘을 더 좌측으로 - SizedBox( - width: 36, - height: 36, - child: AnimatedBoatIcon(color: Colors.white, size: 60), - ), - const SizedBox(width: 24), - const Text( - 'supERPort', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: -2.5, - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/screens/sidebar/widgets/sidebar_menu_item.dart b/lib/screens/sidebar/widgets/sidebar_menu_item.dart deleted file mode 100644 index afac107..0000000 --- a/lib/screens/sidebar/widgets/sidebar_menu_item.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 단일 메뉴 아이템 위젯 -class SidebarMenuItem extends StatelessWidget { - final IconData icon; - final String title; - final String route; - final bool isActive; - final bool isHovered; - final bool isSubItem; - final VoidCallback onTap; - - const SidebarMenuItem({ - super.key, - required this.icon, - required this.title, - required this.route, - required this.isActive, - required this.isHovered, - this.isSubItem = false, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: onTap, - child: Container( - height: 44, - alignment: Alignment.centerLeft, - margin: const EdgeInsets.symmetric( - vertical: 2, - horizontal: 6, - ), // 외부 여백 - padding: EdgeInsets.only(left: isSubItem ? 48 : 24, right: 24), - decoration: BoxDecoration( - color: - isActive - ? Colors.white - : (isHovered - ? const Color(0xFFE9EDF2) - : Colors.transparent), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - Icon( - icon, - size: 18, - color: - isActive ? AppThemeTailwind.primary : AppThemeTailwind.dark, - ), - const SizedBox(width: 10), - Text( - title, - style: TextStyle( - color: - isActive - ? AppThemeTailwind.primary - : AppThemeTailwind.dark, - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - fontSize: 14, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/sidebar/widgets/sidebar_menu_submenu.dart b/lib/screens/sidebar/widgets/sidebar_menu_submenu.dart deleted file mode 100644 index 9f7b408..0000000 --- a/lib/screens/sidebar/widgets/sidebar_menu_submenu.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_item.dart'; -import 'package:superport/screens/sidebar/widgets/sidebar_menu_types.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -// 서브메뉴(확장/축소, 하위 아이템) 위젯 -class SidebarMenuWithSubmenu extends StatelessWidget { - final IconData icon; - final String title; - final String route; - final List subItems; - final bool isExpanded; - final bool isMenuActive; - final bool isSubMenuActive; - final bool isHovered; - final VoidCallback onToggleExpanded; - final String currentRoute; - final void Function(String) onRouteChanged; - - const SidebarMenuWithSubmenu({ - super.key, - required this.icon, - required this.title, - required this.route, - required this.subItems, - required this.isExpanded, - required this.isMenuActive, - required this.isSubMenuActive, - required this.isHovered, - required this.onToggleExpanded, - required this.currentRoute, - required this.onRouteChanged, - }); - - @override - Widget build(BuildContext context) { - final bool isHighlighted = isMenuActive || isSubMenuActive; - return Column( - children: [ - MouseRegion( - cursor: SystemMouseCursors.click, - child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: () { - onToggleExpanded(); - onRouteChanged(route); - }, - child: Container( - height: 44, - margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), - padding: const EdgeInsets.only(left: 24, right: 24), - decoration: BoxDecoration( - color: - isMenuActive - ? Colors.white - : (isHovered - ? const Color(0xFFE9EDF2) - : Colors.transparent), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - Icon( - icon, - size: 18, - color: - isHighlighted - ? AppThemeTailwind.primary - : AppThemeTailwind.dark, - ), - const SizedBox(width: 10), - Text( - title, - style: TextStyle( - color: - isHighlighted - ? AppThemeTailwind.primary - : AppThemeTailwind.dark, - fontWeight: - isHighlighted ? FontWeight.bold : FontWeight.normal, - fontSize: 14, - ), - ), - const Spacer(), - Icon( - isExpanded - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down, - size: 20, - color: AppThemeTailwind.muted, - ), - ], - ), - ), - ), - ), - AnimatedContainer( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: ClipRect( - child: Align( - alignment: Alignment.topCenter, - heightFactor: isExpanded ? 1 : 0, - child: Column( - children: - subItems.map((item) { - return SidebarMenuItem( - icon: Icons.circle, - title: item.title, - route: item.route, - isActive: currentRoute == item.route, - isHovered: false, // hover는 상위에서 관리 - isSubItem: true, - onTap: () => onRouteChanged(item.route), - ); - }).toList(), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/screens/sidebar/widgets/sidebar_menu_types.dart b/lib/screens/sidebar/widgets/sidebar_menu_types.dart deleted file mode 100644 index 43cd7ad..0000000 --- a/lib/screens/sidebar/widgets/sidebar_menu_types.dart +++ /dev/null @@ -1,11 +0,0 @@ -// 서브메뉴 아이템 타입 정의 파일 -// 이 파일은 사이드바 메뉴에서 사용하는 서브메뉴 아이템 타입만 정의합니다. - -class SidebarSubMenuItem { - // 서브메뉴의 제목 - final String title; - // 서브메뉴의 라우트 - final String route; - - const SidebarSubMenuItem({required this.title, required this.route}); -} diff --git a/lib/screens/user/user_list.dart b/lib/screens/user/user_list.dart deleted file mode 100644 index e38edde..0000000 --- a/lib/screens/user/user_list.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/services/mock_data_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/user/controllers/user_list_controller.dart'; -import 'package:superport/screens/user/widgets/user_table.dart'; -import 'package:superport/utils/user_utils.dart'; -import 'package:superport/screens/common/widgets/pagination.dart'; - -// 담당자 목록 화면 (UI만 담당) -class UserListScreen extends StatefulWidget { - const UserListScreen({super.key}); - - @override - State createState() => _UserListScreenState(); -} - -class _UserListScreenState extends State { - late final UserListController _controller; - final MockDataService _dataService = MockDataService(); - // 페이지네이션 상태 추가 - int _currentPage = 1; - final int _pageSize = 10; - - @override - void initState() { - super.initState(); - _controller = UserListController(dataService: _dataService); - _controller.loadUsers(); - _controller.addListener(_refresh); - } - - @override - void dispose() { - _controller.removeListener(_refresh); - super.dispose(); - } - - // 상태 갱신용 setState 래퍼 - void _refresh() { - setState(() {}); - } - - // 사용자 추가 화면 이동 - void _navigateToAddScreen() async { - final result = await Navigator.pushNamed(context, '/user/add'); - if (result == true) { - _controller.loadUsers(); - } - } - - // 사용자 수정 화면 이동 - void _navigateToEditScreen(int id) async { - final result = await Navigator.pushNamed( - context, - '/user/edit', - arguments: id, - ); - if (result == true) { - _controller.loadUsers(); - } - } - - // 사용자 삭제 다이얼로그 - void _showDeleteDialog(int id) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('삭제 확인'), - content: const Text('이 사용자 정보를 삭제하시겠습니까?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - _controller.deleteUser(id, () { - Navigator.pop(context); - }, (error) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error)), - ); - }); - }, - child: const Text('삭제'), - ), - ], - ), - ); - } - - // 회사명 반환 함수 (내부에서만 사용) - String _getCompanyName(int companyId) { - final company = _dataService.getCompanyById(companyId); - return company?.name ?? '-'; - } - - @override - Widget build(BuildContext context) { - // 대시보드 폭에 맞게 조정 - final screenWidth = MediaQuery.of(context).size.width; - final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32; - - // 페이지네이션 데이터 슬라이싱 - final int totalCount = _controller.users.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > totalCount - ? totalCount - : (startIndex + _pageSize); - final pagedUsers = _controller.users.sublist(startIndex, endIndex); - - return MainLayout( - title: '담당자 관리', - currentRoute: Routes.user, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _controller.loadUsers, - color: Colors.grey, - ), - ], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PageTitle( - title: '담당자 목록', - width: maxContentWidth - 32, - rightWidget: ElevatedButton.icon( - onPressed: _navigateToAddScreen, - icon: const Icon(Icons.add), - label: const Text('추가'), - style: AppThemeTailwind.primaryButtonStyle, - ), - ), - Expanded( - child: DataTableCard( - width: maxContentWidth - 32, - child: UserTable( - users: pagedUsers, - width: maxContentWidth - 32, - getRoleName: getRoleName, - getBranchName: _controller.getBranchName, - getCompanyName: _getCompanyName, - onEdit: _navigateToEditScreen, - onDelete: _showDeleteDialog, - ), - ), - ), - // 페이지네이션 위젯 추가 - if (totalCount > _pageSize) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Pagination( - totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, - onPageChanged: (page) { - setState(() { - _currentPage = page; - }); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/user/widgets/user_table.dart b/lib/screens/user/widgets/user_table.dart deleted file mode 100644 index de3f712..0000000 --- a/lib/screens/user/widgets/user_table.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/user_model.dart'; - -/// 사용자 목록 테이블 위젯 (SRP, 재사용성 중심) -class UserTable extends StatelessWidget { - final List users; - final double width; - final String Function(String role) getRoleName; - final String Function(int companyId, int? branchId) getBranchName; - final String Function(int companyId) getCompanyName; - final void Function(int userId) onEdit; - final void Function(int userId) onDelete; - - const UserTable({ - super.key, - required this.users, - required this.width, - required this.getRoleName, - required this.getBranchName, - required this.getCompanyName, - required this.onEdit, - required this.onDelete, - }); - - @override - Widget build(BuildContext context) { - return users.isEmpty - ? const Center(child: Text('등록된 사용자 정보가 없습니다.')) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - constraints: BoxConstraints(minWidth: width - 32), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columns: const [ - DataColumn(label: Text('번호')), - DataColumn(label: Text('이름')), - DataColumn(label: Text('직급')), - DataColumn(label: Text('소속 회사')), - DataColumn(label: Text('소속 지점')), - DataColumn(label: Text('이메일')), - DataColumn(label: Text('전화번호')), - DataColumn(label: Text('권한')), - DataColumn(label: Text('관리')), - ], - rows: - users.map((user) { - return DataRow( - cells: [ - DataCell(Text('${user.id}')), - DataCell(Text(user.name)), - DataCell(Text(user.position ?? '-')), - DataCell(Text(getCompanyName(user.companyId))), - DataCell( - Text( - user.branchId != null - ? getBranchName(user.companyId, user.branchId) - : '-', - ), - ), - DataCell(Text(user.email ?? '-')), - DataCell( - user.phoneNumbers.isNotEmpty - ? Text(user.phoneNumbers.first['number'] ?? '-') - : const Text('-'), - ), - DataCell(Text(getRoleName(user.role))), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.blue, - ), - onPressed: () => onEdit(user.id!), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: () => onDelete(user.id!), - ), - ], - ), - ), - ], - ); - }).toList(), - ), - ), - ), - ); - } -} diff --git a/lib/screens/warehouse_location/warehouse_location_form_controller.dart b/lib/screens/warehouse_location/warehouse_location_form_controller.dart deleted file mode 100644 index 45ac38c..0000000 --- a/lib/screens/warehouse_location/warehouse_location_form_controller.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/warehouse_location_model.dart'; -import 'package:superport/models/address_model.dart'; -import 'package:superport/services/mock_data_service.dart'; - -/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러 -class WarehouseLocationFormController { - /// 폼 키 - final GlobalKey formKey = GlobalKey(); - - /// 입고지명 입력 컨트롤러 - final TextEditingController nameController = TextEditingController(); - - /// 비고 입력 컨트롤러 - final TextEditingController remarkController = TextEditingController(); - - /// 주소 정보 - Address address = const Address(); - - /// 저장 중 여부 - bool isSaving = false; - - /// 수정 모드 여부 - bool isEditMode = false; - - /// 입고지 id (수정 모드) - int? id; - - /// 기존 데이터 세팅 (수정 모드) - void initialize(int? locationId) { - id = locationId; - if (id != null) { - final location = MockDataService().getWarehouseLocationById(id!); - if (location != null) { - isEditMode = true; - nameController.text = location.name; - address = location.address; - remarkController.text = location.remark ?? ''; - } - } - } - - /// 주소 변경 처리 - void updateAddress(Address newAddress) { - address = newAddress; - } - - /// 저장 처리 (추가/수정) - Future save(BuildContext context) async { - if (!formKey.currentState!.validate()) return false; - isSaving = true; - if (isEditMode) { - // 수정 - MockDataService().updateWarehouseLocation( - WarehouseLocation( - id: id!, - name: nameController.text.trim(), - address: address, - remark: remarkController.text.trim(), - ), - ); - } else { - // 추가 - MockDataService().addWarehouseLocation( - WarehouseLocation( - id: 0, - name: nameController.text.trim(), - address: address, - remark: remarkController.text.trim(), - ), - ); - } - isSaving = false; - Navigator.pop(context, true); - return true; - } - - /// 취소 처리 - void cancel(BuildContext context) { - Navigator.pop(context, false); - } - - /// 컨트롤러 해제 - void dispose() { - nameController.dispose(); - remarkController.dispose(); - } -} diff --git a/lib/screens/warehouse_location/warehouse_location_list.dart b/lib/screens/warehouse_location/warehouse_location_list.dart deleted file mode 100644 index 44c7c52..0000000 --- a/lib/screens/warehouse_location/warehouse_location_list.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:superport/models/warehouse_location_model.dart'; -import 'controllers/warehouse_location_list_controller.dart'; -import 'package:superport/screens/common/widgets/address_input.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/screens/common/main_layout.dart'; -import 'package:superport/screens/common/widgets/pagination.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; - -/// 입고지 관리 리스트 화면 (SRP 적용, UI만 담당) -class WarehouseLocationListScreen extends StatefulWidget { - const WarehouseLocationListScreen({Key? key}) : super(key: key); - - @override - State createState() => - _WarehouseLocationListScreenState(); -} - -class _WarehouseLocationListScreenState - extends State { - /// 리스트 컨트롤러 (상태 및 CRUD 위임) - final WarehouseLocationListController _controller = - WarehouseLocationListController(); - int _currentPage = 1; - final int _pageSize = 10; - - @override - void initState() { - super.initState(); - _controller.loadWarehouseLocations(); - } - - /// 리스트 새로고침 - void _reload() { - setState(() { - _controller.loadWarehouseLocations(); - }); - } - - /// 입고지 추가 폼으로 이동 - void _navigateToAdd() async { - final result = await Navigator.pushNamed( - context, - Routes.warehouseLocationAdd, - ); - if (result == true) { - _reload(); - } - } - - /// 입고지 수정 폼으로 이동 - void _navigateToEdit(WarehouseLocation location) async { - final result = await Navigator.pushNamed( - context, - Routes.warehouseLocationEdit, - arguments: location.id, - ); - if (result == true) { - _reload(); - } - } - - /// 삭제 다이얼로그 (별도 위젯으로 분리 가능) - void _showDeleteDialog(int id) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('입고지 삭제'), - content: const Text('정말로 삭제하시겠습니까?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('취소'), - ), - TextButton( - onPressed: () { - setState(() { - _controller.deleteWarehouseLocation(id); - }); - Navigator.of(context).pop(); - }, - child: const Text('삭제'), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - // 대시보드 폭에 맞게 조정 - final screenWidth = MediaQuery.of(context).size.width; - final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32; - - final int totalCount = _controller.warehouseLocations.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > totalCount - ? totalCount - : (startIndex + _pageSize); - final List pagedLocations = _controller - .warehouseLocations - .sublist(startIndex, endIndex); - - return MainLayout( - title: '입고지 관리', - currentRoute: Routes.warehouseLocation, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _reload, - color: Colors.grey, - ), - ], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PageTitle( - title: '입고지 목록', - width: maxContentWidth - 32, - rightWidget: ElevatedButton.icon( - onPressed: _navigateToAdd, - icon: const Icon(Icons.add), - label: const Text('입고지 추가'), - style: AppThemeTailwind.primaryButtonStyle, - ), - ), - Expanded( - child: DataTableCard( - width: maxContentWidth - 32, - child: - pagedLocations.isEmpty - ? const Center(child: Text('등록된 입고지가 없습니다.')) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - width: maxContentWidth - 32, - constraints: BoxConstraints( - minWidth: maxContentWidth - 64, - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columns: const [ - DataColumn(label: Text('번호')), - DataColumn(label: Text('입고지명')), - DataColumn(label: Text('주소')), - DataColumn(label: Text('관리')), - ], - rows: List.generate(pagedLocations.length, (i) { - final location = pagedLocations[i]; - return DataRow( - cells: [ - DataCell(Text('${startIndex + i + 1}')), - DataCell(Text(location.name)), - DataCell( - AddressInput.readonly( - address: location.address, - ), - ), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: AppThemeTailwind.primary, - ), - tooltip: '수정', - onPressed: - () => - _navigateToEdit(location), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: AppThemeTailwind.danger, - ), - tooltip: '삭제', - onPressed: - () => _showDeleteDialog( - location.id, - ), - ), - ], - ), - ), - ], - ); - }), - ), - ), - ), - ), - ), - ), - const SizedBox(height: 16), - Pagination( - currentPage: _currentPage, - totalCount: totalCount, - pageSize: _pageSize, - onPageChanged: (page) { - setState(() { - _currentPage = page; - }); - }, - ), - ], - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 7db8e02..c29e1a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,4 +58,6 @@ flutter: uses-material-design: true assets: - lib/assets/fonts/NotoSansKR-VariableFont_wght.ttf + - .env.development + - .env.production