diff --git a/lib/main.dart b/lib/main.dart index fca53ed..eaa720f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,8 +6,6 @@ import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/company/company_form.dart'; import 'package:superport/screens/equipment/equipment_in_form.dart'; import 'package:superport/screens/equipment/equipment_out_form.dart'; -import 'package:superport/screens/equipment/equipment_history_screen.dart'; -import 'package:superport/screens/equipment/test_history_screen.dart'; import 'package:superport/screens/license/license_form.dart'; // MaintenanceFormScreen으로 사용 import 'package:superport/screens/user/user_form.dart'; import 'package:superport/screens/warehouse_location/warehouse_location_form.dart'; @@ -162,22 +160,6 @@ class SuperportApp extends StatelessWidget { return MaterialPageRoute( builder: (context) => EquipmentOutFormScreen(equipmentOutId: id), ); - - // 장비 이력 조회 - case Routes.equipmentHistory: - final args = settings.arguments as Map; - return MaterialPageRoute( - builder: (context) => EquipmentHistoryScreen( - equipmentId: args['equipmentId'] as int, - equipmentName: args['equipmentName'] as String, - ), - ); - - // 테스트 이력 화면 - case Routes.testHistory: - return MaterialPageRoute( - builder: (context) => const TestHistoryScreen(), - ); // 회사 관련 라우트 case Routes.companyAdd: diff --git a/lib/screens/equipment/equipment_history_screen.dart b/lib/screens/equipment/equipment_history_screen.dart deleted file mode 100644 index 02ad1fc..0000000 --- a/lib/screens/equipment/equipment_history_screen.dart +++ /dev/null @@ -1,351 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/data/models/equipment/equipment_history_dto.dart'; -import 'package:superport/services/equipment_service.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/core/errors/failures.dart'; -import 'package:intl/intl.dart'; - -class EquipmentHistoryScreen extends StatefulWidget { - final int equipmentId; - final String equipmentName; - - const EquipmentHistoryScreen({ - Key? key, - required this.equipmentId, - required this.equipmentName, - }) : super(key: key); - - @override - State createState() => _EquipmentHistoryScreenState(); -} - -class _EquipmentHistoryScreenState extends State { - final EquipmentService _equipmentService = GetIt.instance(); - List _histories = []; - bool _isLoading = false; // 초기값을 false로 변경 - bool _isInitialLoad = true; // 초기 로딩 상태 추가 - String? _error; - int _currentPage = 1; - final int _perPage = 20; - bool _hasMore = true; - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - print('[INIT] EquipmentHistoryScreen initialized for equipment ${widget.equipmentId}'); - _loadHistory(isInitialLoad: true); - _scrollController.addListener(_onScroll); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - void _onScroll() { - if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { - _loadMoreHistory(); - } - } - - Future _loadHistory({bool isRefresh = false, bool isInitialLoad = false}) async { - print('[_loadHistory] Called - isRefresh: $isRefresh, isInitialLoad: $isInitialLoad, _isLoading: $_isLoading'); - - if (isRefresh) { - _currentPage = 1; - _hasMore = true; - _histories.clear(); - } - - // 초기 로딩이 아닌 경우에만 중복 호출 방지 - if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) { - print('[_loadHistory] Skipping - hasMore: $_hasMore, isLoading: $_isLoading'); - return; - } - - if (!mounted) return; - - setState(() { - _isLoading = true; - _error = null; - if (isInitialLoad) _isInitialLoad = false; - }); - - try { - print('[DEBUG] ==== STARTING HISTORY LOAD ===='); - print('[DEBUG] Equipment ID: ${widget.equipmentId}'); - print('[DEBUG] Equipment Name: ${widget.equipmentName}'); - print('[DEBUG] Page: $_currentPage, PerPage: $_perPage'); - print('[DEBUG] Current time: ${DateTime.now()}'); - - // 타임아웃 설정 - final histories = await _equipmentService.getEquipmentHistory( - widget.equipmentId, - page: _currentPage, - perPage: _perPage, - ).timeout( - const Duration(seconds: 10), - onTimeout: () { - print('[ERROR] API call timeout after 10 seconds'); - throw Exception('API 호출 시간 초과 (10초)'); - }, - ); - - print('[DEBUG] API call completed successfully'); - print('[DEBUG] Received ${histories.length} history records'); - if (histories.isNotEmpty) { - print('[DEBUG] First history record: ${histories.first.toJson()}'); - } else { - print('[DEBUG] No history records found'); - } - - if (!mounted) { - print('[WARNING] Widget not mounted, skipping setState'); - return; - } - - setState(() { - if (isRefresh) { - _histories = histories; - } else { - _histories.addAll(histories); - } - _hasMore = histories.length == _perPage; - if (_hasMore) _currentPage++; - _isLoading = false; - print('[DEBUG] State updated - Loading: false, Histories count: ${_histories.length}'); - }); - } on Failure catch (e) { - print('[ERROR] Failure loading history: ${e.message}'); - if (!mounted) return; - setState(() { - _error = e.message; - _isLoading = false; - }); - } catch (e, stackTrace) { - print('[ERROR] Unexpected error loading history: $e'); - print('[ERROR] Stack trace: $stackTrace'); - if (!mounted) return; - setState(() { - _error = '이력을 불러오는 중 오류가 발생했습니다: $e'; - _isLoading = false; - }); - } finally { - print('[DEBUG] ==== HISTORY LOAD COMPLETED ===='); - } - } - - Future _loadMoreHistory() async { - await _loadHistory(); - } - - String _formatDate(DateTime? date) { - if (date == null) return '-'; - return DateFormat('yyyy-MM-dd HH:mm').format(date); - } - - String _getTransactionTypeText(String? type) { - switch (type) { - case 'I': - return '입고'; - case 'O': - return '출고'; - case 'R': - return '대여'; - case 'T': - return '반납'; - case 'D': - return '폐기'; - default: - return type ?? '-'; - } - } - - Color _getTransactionTypeColor(String? type) { - switch (type) { - case 'I': - return Colors.green; - case 'O': - return Colors.blue; - case 'R': - return Colors.orange; - case 'T': - return Colors.teal; - case 'D': - return Colors.red; - default: - return Colors.grey; - } - } - - Widget _buildHistoryItem(EquipmentHistoryDto history) { - final typeColor = _getTransactionTypeColor(history.transactionType); - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - leading: CircleAvatar( - backgroundColor: typeColor.withOpacity(0.2), - child: Text( - _getTransactionTypeText(history.transactionType), - style: TextStyle( - color: typeColor, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - history.remarks ?? '비고 없음', - overflow: TextOverflow.ellipsis, - ), - ), - Text( - '수량: ${history.quantity}', - style: const TextStyle(fontSize: 14), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text(_formatDate(history.transactionDate)), - if (history.userName != null) - Text('담당자: ${history.userName}'), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - print('[BUILD] EquipmentHistoryScreen - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}'); - - return Scaffold( - appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('장비 이력'), - Text( - widget.equipmentName, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), - ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => _loadHistory(isRefresh: true), - tooltip: '새로고침', - ), - ], - ), - body: Builder( - builder: (context) { - print('[UI] Building body - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}'); - - // 초기 로딩 또는 일반 로딩 중 - if ((_isInitialLoad || _isLoading) && _histories.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text('장비 ID ${widget.equipmentId}의 이력을 불러오는 중...'), - const SizedBox(height: 8), - TextButton( - onPressed: () { - print('[USER] Cancel loading clicked'); - setState(() { - _isLoading = false; - _error = '사용자가 로딩을 취소했습니다'; - }); - }, - child: const Text('로딩 취소'), - ), - ], - ), - ); - } - - if (_error != null && _histories.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: Colors.red), - const SizedBox(height: 16), - Text('오류 발생', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text( - _error!, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.red), - ), - ), - const SizedBox(height: 16), - ElevatedButton.icon( - icon: const Icon(Icons.refresh), - label: const Text('다시 시도'), - onPressed: () => _loadHistory(isRefresh: true), - ), - ], - ), - ); - } - - return RefreshIndicator( - onRefresh: () => _loadHistory(isRefresh: true), - child: _histories.isEmpty - ? ListView( - children: [ - const SizedBox(height: 200), - const Icon(Icons.history, size: 48, color: Colors.grey), - const SizedBox(height: 16), - const Center( - child: Text( - '이력이 없습니다.', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ), - const SizedBox(height: 8), - Center( - child: TextButton( - onPressed: () => _loadHistory(isRefresh: true), - child: const Text('새로고침'), - ), - ), - ], - ) - : ListView.builder( - controller: _scrollController, - itemCount: _histories.length + (_hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index == _histories.length) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), - ); - } - return _buildHistoryItem(_histories[index]); - }, - ), - ); - }, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/equipment/equipment_list_redesign.dart b/lib/screens/equipment/equipment_list_redesign.dart index 6c35e0e..251cb42 100644 --- a/lib/screens/equipment/equipment_list_redesign.dart +++ b/lib/screens/equipment/equipment_list_redesign.dart @@ -12,6 +12,7 @@ import 'package:superport/services/mock_data_service.dart'; import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/utils/equipment_display_helper.dart'; +import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart'; /// shadcn/ui 스타일로 재설계된 장비 관리 화면 class EquipmentListRedesign extends StatefulWidget { @@ -362,13 +363,11 @@ class _EquipmentListRedesignState extends State { return; } - final result = await Navigator.pushNamed( - context, - Routes.equipmentHistory, - arguments: { - 'equipmentId': equipment.equipment.id, - 'equipmentName': '${equipment.equipment.manufacturer} ${equipment.equipment.name}', - }, + // 팝업 다이얼로그로 이력 표시 + final result = await EquipmentHistoryDialog.show( + context: context, + equipmentId: equipment.equipment.id!, + equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.name}', ); if (result == true) { diff --git a/lib/screens/equipment/test_history_screen.dart b/lib/screens/equipment/test_history_screen.dart deleted file mode 100644 index 2202b2b..0000000 --- a/lib/screens/equipment/test_history_screen.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/services/equipment_service.dart'; -import 'package:superport/data/models/equipment/equipment_history_dto.dart'; - -class TestHistoryScreen extends StatefulWidget { - const TestHistoryScreen({Key? key}) : super(key: key); - - @override - State createState() => _TestHistoryScreenState(); -} - -class _TestHistoryScreenState extends State { - final EquipmentService _equipmentService = GetIt.instance(); - final TextEditingController _idController = TextEditingController(text: '1'); - List? _histories; - bool _isLoading = false; - String? _error; - - Future _testAddHistory() async { - setState(() { - _isLoading = true; - _error = null; - }); - - try { - final equipmentId = int.tryParse(_idController.text) ?? 1; - - // 테스트 이력 추가 - await _equipmentService.addEquipmentHistory( - equipmentId, - 'I', // transaction type - 10, // quantity - 'Test history added at ${DateTime.now()}', // remarks - ); - - // 이력 다시 조회 - await _testGetHistory(); - - } catch (e) { - setState(() { - _error = 'Error adding history: $e'; - _isLoading = false; - }); - } - } - - Future _testGetHistory() async { - setState(() { - _isLoading = true; - _error = null; - _histories = null; - }); - - try { - final equipmentId = int.tryParse(_idController.text) ?? 1; - - print('[TEST] Fetching history for equipment ID: $equipmentId'); - - final histories = await _equipmentService.getEquipmentHistory( - equipmentId, - page: 1, - perPage: 20, - ); - - print('[TEST] Received ${histories.length} history records'); - - setState(() { - _histories = histories; - _isLoading = false; - }); - } catch (e) { - print('[TEST ERROR] $e'); - setState(() { - _error = 'Error: $e'; - _isLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Equipment History Test'), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: TextField( - controller: _idController, - decoration: const InputDecoration( - labelText: 'Equipment ID', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _isLoading ? null : _testGetHistory, - child: const Text('Get History'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _isLoading ? null : _testAddHistory, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - ), - child: const Text('Add Test History'), - ), - ], - ), - const SizedBox(height: 16), - if (_isLoading) - const Center(child: CircularProgressIndicator()) - else if (_error != null) - Card( - color: Colors.red[50], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - _error!, - style: const TextStyle(color: Colors.red), - ), - ), - ) - else if (_histories != null) - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Found ${_histories!.length} history records', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const Divider(), - Expanded( - child: _histories!.isEmpty - ? const Center(child: Text('No history found')) - : ListView.builder( - itemCount: _histories!.length, - itemBuilder: (context, index) { - final history = _histories![index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - leading: CircleAvatar( - child: Text(history.transactionType), - ), - title: Text('Quantity: ${history.quantity}'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Remarks: ${history.remarks ?? "N/A"}'), - Text('Date: ${history.transactionDate}'), - Text('User: ${history.userName ?? "Unknown"}'), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/equipment/widgets/equipment_history_dialog.dart b/lib/screens/equipment/widgets/equipment_history_dialog.dart new file mode 100644 index 0000000..73c5a01 --- /dev/null +++ b/lib/screens/equipment/widgets/equipment_history_dialog.dart @@ -0,0 +1,647 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:superport/data/models/equipment/equipment_history_dto.dart'; +import 'package:superport/services/equipment_service.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:intl/intl.dart'; + +/// 장비 이력을 표시하는 팝업 다이얼로그 +class EquipmentHistoryDialog extends StatefulWidget { + final int equipmentId; + final String equipmentName; + + const EquipmentHistoryDialog({ + Key? key, + required this.equipmentId, + required this.equipmentName, + }) : super(key: key); + + @override + State createState() => _EquipmentHistoryDialogState(); + + /// 다이얼로그를 표시하는 정적 메서드 + static Future show({ + required BuildContext context, + required int equipmentId, + required String equipmentName, + }) { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return EquipmentHistoryDialog( + equipmentId: equipmentId, + equipmentName: equipmentName, + ); + }, + ); + } +} + +class _EquipmentHistoryDialogState extends State { + final EquipmentService _equipmentService = GetIt.instance(); + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + + List _histories = []; + List _filteredHistories = []; + bool _isLoading = false; + bool _isInitialLoad = true; + String? _error; + int _currentPage = 1; + final int _perPage = 20; + bool _hasMore = true; + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + _loadHistory(isInitialLoad: true); + _scrollController.addListener(_onScroll); + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + _loadMoreHistory(); + } + } + + void _onSearchChanged() { + setState(() { + _searchQuery = _searchController.text.toLowerCase(); + _filterHistories(); + }); + } + + void _filterHistories() { + if (_searchQuery.isEmpty) { + _filteredHistories = List.from(_histories); + } else { + _filteredHistories = _histories.where((history) { + final remarks = (history.remarks ?? '').toLowerCase(); + final userName = (history.userName ?? '').toLowerCase(); + final type = _getTransactionTypeText(history.transactionType).toLowerCase(); + + return remarks.contains(_searchQuery) || + userName.contains(_searchQuery) || + type.contains(_searchQuery); + }).toList(); + } + } + + Future _loadHistory({ + bool isRefresh = false, + bool isInitialLoad = false + }) async { + if (isRefresh) { + _currentPage = 1; + _hasMore = true; + _histories.clear(); + _filteredHistories.clear(); + } + + if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) { + return; + } + + if (!mounted) return; + + setState(() { + _isLoading = true; + _error = null; + if (isInitialLoad) _isInitialLoad = false; + }); + + try { + final histories = await _equipmentService.getEquipmentHistory( + widget.equipmentId, + page: _currentPage, + perPage: _perPage, + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + throw Exception('API 호출 시간 초과 (10초)'); + }, + ); + + if (!mounted) return; + + setState(() { + if (isRefresh) { + _histories = histories; + } else { + _histories.addAll(histories); + } + _filterHistories(); + _hasMore = histories.length == _perPage; + if (_hasMore) _currentPage++; + _isLoading = false; + }); + } on Failure catch (e) { + if (!mounted) return; + setState(() { + _error = e.message; + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = '이력을 불러오는 중 오류가 발생했습니다: $e'; + _isLoading = false; + }); + } + } + + Future _loadMoreHistory() async { + await _loadHistory(); + } + + String _formatDate(DateTime? date) { + if (date == null) return '-'; + return DateFormat('yyyy-MM-dd HH:mm').format(date); + } + + String _getTransactionTypeText(String? type) { + switch (type) { + case 'I': + return '입고'; + case 'O': + return '출고'; + case 'R': + return '대여'; + case 'T': + return '반납'; + case 'D': + return '폐기'; + default: + return type ?? '-'; + } + } + + Color _getTransactionTypeColor(String? type) { + switch (type) { + case 'I': + return Colors.green; + case 'O': + return Colors.blue; + case 'R': + return Colors.orange; + case 'T': + return Colors.teal; + case 'D': + return Colors.red; + default: + return Colors.grey; + } + } + + Widget _buildHistoryItem(EquipmentHistoryDto history) { + final typeColor = _getTransactionTypeColor(history.transactionType); + final typeText = _getTransactionTypeText(history.transactionType); + + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () {}, // 클릭 효과를 위한 빈 핸들러 + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 타입 아이콘 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: typeColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + typeText, + style: TextStyle( + color: typeColor, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ), + ), + const SizedBox(width: 12), + // 내용 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + history.remarks ?? '비고 없음', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '수량: ${history.quantity}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + _formatDate(history.transactionDate), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + if (history.userName != null) ...[ + const SizedBox(width: 12), + Icon( + Icons.person_outline, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + history.userName!, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final bool isDesktop = screenSize.width > 768; + + // 반응형 크기 설정 + final dialogWidth = isDesktop + ? screenSize.width * 0.7 + : screenSize.width * 0.95; + final dialogHeight = isDesktop + ? screenSize.height * 0.75 + : screenSize.height * 0.85; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.symmetric( + horizontal: isDesktop ? 40 : 10, + vertical: isDesktop ? 40 : 20, + ), + child: RawKeyboardListener( + focusNode: FocusNode(), + autofocus: true, + onKey: (RawKeyEvent event) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.of(context).pop(); + } + }, + child: Container( + width: dialogWidth, + height: dialogHeight, + constraints: BoxConstraints( + maxWidth: 900, + maxHeight: 700, + minWidth: 320, + minHeight: 400, + ), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + // 헤더 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Icon( + Icons.history, + color: Colors.grey.shade700, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '장비 이력', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.equipmentName, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // 검색 필드 + SizedBox( + width: 200, + height: 36, + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: '검색...', + hintStyle: TextStyle(fontSize: 13), + prefixIcon: const Icon(Icons.search, size: 18), + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(width: 8), + // 새로고침 버튼 + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _isLoading ? null : () => _loadHistory(isRefresh: true), + tooltip: '새로고침', + ), + // 닫기 버튼 + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + tooltip: '닫기', + ), + ], + ), + ), + // 본문 + Expanded( + child: Builder( + builder: (context) { + // 초기 로딩 + if ((_isInitialLoad || _isLoading) && _histories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text('이력을 불러오는 중...'), + ], + ), + ); + } + + // 에러 상태 + if (_error != null && _histories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: Colors.red.shade400, + ), + const SizedBox(height: 16), + Text( + '오류 발생', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + _error!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.red.shade600, + fontSize: 13, + ), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(Icons.refresh, size: 16), + label: const Text('다시 시도'), + onPressed: () => _loadHistory(isRefresh: true), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ], + ), + ); + } + + // 데이터 없음 + if (_filteredHistories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox_outlined, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + _searchQuery.isNotEmpty + ? '검색 결과가 없습니다.' + : '이력이 없습니다.', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + if (_searchQuery.isNotEmpty) ...[ + const SizedBox(height: 8), + TextButton( + onPressed: () { + _searchController.clear(); + }, + child: const Text('검색 초기화'), + ), + ], + ], + ), + ); + } + + // 이력 목록 + return Padding( + padding: const EdgeInsets.all(16), + child: ListView.builder( + controller: _scrollController, + itemCount: _filteredHistories.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0), + itemBuilder: (context, index) { + if (index == _filteredHistories.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + return _buildHistoryItem(_filteredHistories[index]); + }, + ), + ); + }, + ), + ), + // 푸터 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '총 ${_histories.length}개 이력', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + if (_isLoading && _histories.isNotEmpty) + Row( + children: [ + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + const SizedBox(width: 8), + Text( + '더 불러오는 중...', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 7043141..bcebfc9 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -11,8 +11,6 @@ class Routes { static const String equipmentInEdit = '/equipment-in/edit'; // 장비 입고 편집 static const String equipmentOut = '/equipment-out'; // 출고 목록(미사용) static const String equipmentOutAdd = '/equipment-out/add'; // 장비 출고 폼 - static const String equipmentHistory = '/equipment/history'; // 장비 이력 조회 - static const String testHistory = '/test/history'; // 테스트 이력 화면 static const String equipmentOutEdit = '/equipment-out/edit'; // 장비 출고 편집 static const String equipmentInList = '/equipment/in'; // 입고 장비 목록 static const String equipmentOutList = '/equipment/out'; // 출고 장비 목록