diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index 07f1d7b..f35342a 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -4,52 +4,52 @@ class AppConstants { static const int defaultPageSize = 20; static const int maxPageSize = 100; static const Duration cacheTimeout = Duration(minutes: 5); - + // API 타임아웃 static const Duration apiConnectTimeout = Duration(seconds: 60); static const Duration apiReceiveTimeout = Duration(seconds: 60); static const Duration healthCheckTimeout = Duration(seconds: 10); static const Duration loginTimeout = Duration(seconds: 10); - + // 디바운스 시간 static const Duration searchDebounce = Duration(milliseconds: 500); static const Duration licenseSearchDebounce = Duration(milliseconds: 300); - + // 애니메이션 시간 static const Duration autocompleteAnimation = Duration(milliseconds: 200); static const Duration formAnimation = Duration(milliseconds: 300); static const Duration loginAnimation = Duration(milliseconds: 1000); static const Duration loginSubAnimation = Duration(milliseconds: 800); - + // 라이선스 만료 기간 static const int licenseExpiryWarningDays = 30; static const int licenseExpiryCautionDays = 60; static const int licenseExpiryInfoDays = 90; - + // 헬스체크 주기 - static const Duration healthCheckInterval = Duration(seconds: 30); - + static const Duration healthCheckInterval = Duration(seconds: 300); + // 토큰 키 static const String accessTokenKey = 'access_token'; static const String refreshTokenKey = 'refresh_token'; static const String tokenTypeKey = 'token_type'; static const String expiresInKey = 'expires_in'; - + // 사용자 권한 매핑 static const Map flutterToBackendRole = { - 'S': 'admin', // Super user - 'M': 'manager', // Manager - 'U': 'staff', // User - 'V': 'viewer', // Viewer + 'S': 'admin', // Super user + 'M': 'manager', // Manager + 'U': 'staff', // User + 'V': 'viewer', // Viewer }; - + static const Map backendToFlutterRole = { 'admin': 'S', 'manager': 'M', 'staff': 'U', 'viewer': 'V', }; - + // 장비 상태 static const Map equipmentStatus = { 'available': '사용가능', @@ -58,7 +58,7 @@ class AppConstants { 'disposed': '폐기', 'rented': '대여중', }; - + // 정렬 옵션 static const Map sortOptions = { 'created_at': '생성일', @@ -66,34 +66,39 @@ class AppConstants { 'name': '이름', 'status': '상태', }; - + // 날짜 형식 static const String dateFormat = 'yyyy-MM-dd'; static const String dateTimeFormat = 'yyyy-MM-dd HH:mm:ss'; - + // 파일 업로드 static const int maxFileSize = 10 * 1024 * 1024; // 10MB static const List allowedFileExtensions = [ - 'jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx' + 'jpg', + 'jpeg', + 'png', + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', ]; - + // 에러 메시지 static const String networkError = '네트워크 연결을 확인해주세요.'; static const String timeoutError = '요청 시간이 초과되었습니다.'; static const String unauthorizedError = '인증이 필요합니다.'; static const String serverError = '서버 오류가 발생했습니다.'; static const String unknownError = '알 수 없는 오류가 발생했습니다.'; - + // 정규식 패턴 static final RegExp emailRegex = RegExp( r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+', ); - - static final RegExp phoneRegex = RegExp( - r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$', - ); - + + static final RegExp phoneRegex = RegExp(r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$'); + static final RegExp businessNumberRegex = RegExp( r'^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$', ); -} \ No newline at end of file +} diff --git a/lib/screens/equipment/controllers/equipment_list_controller.dart b/lib/screens/equipment/controllers/equipment_list_controller.dart index 1b45364..07b9c71 100644 --- a/lib/screens/equipment/controllers/equipment_list_controller.dart +++ b/lib/screens/equipment/controllers/equipment_list_controller.dart @@ -108,9 +108,11 @@ class EquipmentListController extends BaseListController { manufacturer: dto.manufacturer ?? 'Unknown', equipmentNumber: dto.equipmentNumber ?? 'Unknown', // name → equipmentNumber (required) modelName: dto.modelName ?? dto.equipmentNumber ?? 'Unknown', // 새로운 필수 필드 (required) - category1: 'Equipment', // category → category1 (required) - category2: 'General', // subCategory → category2 (required) - category3: 'Standard', // subSubCategory → category3 (required) + // 🔧 [BUG FIX] 하드코딩 제거 - 백엔드 API에서 카테고리 정보 미제공 시 기본값 사용 + // TODO: 백엔드 API에서 category1/2/3 필드 추가 필요 + category1: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값 + category2: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값 + category3: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값 serialNumber: dto.serialNumber, quantity: 1, // 기본 수량 ); @@ -154,7 +156,8 @@ class EquipmentListController extends BaseListController { @override bool filterItem(UnifiedEquipment item, String query) { final q = query.toLowerCase(); - return (item.equipment.name.toLowerCase().contains(q)) || + return (item.equipment.equipmentNumber.toLowerCase().contains(q)) || // name → equipmentNumber + (item.equipment.modelName?.toLowerCase().contains(q) ?? false) || // 모델명 추가 (item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) || (item.equipment.manufacturer.toLowerCase().contains(q)) || (item.notes?.toLowerCase().contains(q) ?? false) || @@ -311,7 +314,7 @@ class EquipmentListController extends BaseListController { reason: reason ?? '폐기 처리', ); } catch (e) { - failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.name}'); + failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}'); // name → equipmentNumber } } diff --git a/lib/screens/equipment/equipment_list.dart b/lib/screens/equipment/equipment_list.dart index 05334e0..1145d5c 100644 --- a/lib/screens/equipment/equipment_list.dart +++ b/lib/screens/equipment/equipment_list.dart @@ -195,10 +195,11 @@ class _EquipmentListState extends State { final keyword = _appliedSearchKeyword.toLowerCase(); return [ e.equipment.manufacturer, - e.equipment.name, - e.equipment.category, - e.equipment.subCategory, - e.equipment.subSubCategory, + e.equipment.equipmentNumber, // name → equipmentNumber (메인 필드) + e.equipment.modelName ?? '', // 모델명 추가 + e.equipment.category1, // category → category1 (메인 필드) + e.equipment.category2, // subCategory → category2 (메인 필드) + e.equipment.category3, // subSubCategory → category3 (메인 필드) e.equipment.serialNumber ?? '', e.equipment.barcode ?? '', e.equipment.remark ?? '', @@ -288,7 +289,7 @@ class _EquipmentListState extends State { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( - '${equipment.manufacturer} ${equipment.name}', + '${equipment.manufacturer} ${equipment.equipmentNumber}', // name → equipmentNumber style: const TextStyle(fontSize: 14), ), ); @@ -434,7 +435,7 @@ class _EquipmentListState extends State { final result = await EquipmentHistoryDialog.show( context: context, equipmentId: equipment.equipment.id!, - equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.name}', + equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}', // name → equipmentNumber ); if (result == true) { @@ -731,28 +732,20 @@ class _EquipmentListState extends State { double _getMinimumTableWidth(List pagedEquipments) { double totalWidth = 0; - // 기본 컬럼들 (최소 너비) + // 기본 컬럼들 (리스트 API에서 제공하는 데이터만) totalWidth += 40; // 체크박스 totalWidth += 50; // 번호 totalWidth += 120; // 제조사 - totalWidth += 120; // 장비명 - totalWidth += 100; // 카테고리 + totalWidth += 120; // 장비번호 + totalWidth += 120; // 모델명 totalWidth += 50; // 수량 totalWidth += 70; // 상태 totalWidth += 80; // 입출고일 - totalWidth += 120; // 입고지 - totalWidth += 120; // 구매처 - totalWidth += 100; // 구매일 - totalWidth += 100; // 구매가격 totalWidth += 90; // 관리 // 상세 컬럼들 (조건부) if (_showDetailedColumns) { totalWidth += 120; // 시리얼번호 - totalWidth += 120; // 바코드 - totalWidth += 120; // 현재 위치 - totalWidth += 100; // 창고 위치 (중복 - 입고지와 다름) - totalWidth += 100; // 점검일 } // padding 추가 (좌우 각 16px) @@ -838,14 +831,13 @@ class _EquipmentListState extends State { _buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 50), // 제조사 _buildHeaderCell('제조사', flex: 3, useExpanded: useExpanded, minWidth: 120), - // 장비명 - _buildHeaderCell('장비명', flex: 3, useExpanded: useExpanded, minWidth: 120), - // 카테고리 - _buildHeaderCell('카테고리', flex: 2, useExpanded: useExpanded, minWidth: 100), + // 장비번호 + _buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 120), + // 모델명 + _buildHeaderCell('모델명', flex: 3, useExpanded: useExpanded, minWidth: 120), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ _buildHeaderCell('시리얼번호', flex: 3, useExpanded: useExpanded, minWidth: 120), - _buildHeaderCell('바코드', flex: 3, useExpanded: useExpanded, minWidth: 120), ], // 수량 _buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 50), @@ -853,20 +845,6 @@ class _EquipmentListState extends State { _buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70), // 입출고일 _buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 80), - // 입고지 - _buildHeaderCell('입고지', flex: 3, useExpanded: useExpanded, minWidth: 120), - // 구매처 - _buildHeaderCell('구매처', flex: 3, useExpanded: useExpanded, minWidth: 120), - // 구매일 - _buildHeaderCell('구매일', flex: 2, useExpanded: useExpanded, minWidth: 100), - // 구매가격 - _buildHeaderCell('구매가격', flex: 2, useExpanded: useExpanded, minWidth: 100), - // 상세 정보 (조건부) - if (_showDetailedColumns) ...[ - _buildHeaderCell('현재 위치', flex: 3, useExpanded: useExpanded, minWidth: 120), - _buildHeaderCell('창고 위치', flex: 2, useExpanded: useExpanded, minWidth: 100), - _buildHeaderCell('점검일', flex: 2, useExpanded: useExpanded, minWidth: 100), - ], // 관리 _buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90), ], @@ -924,22 +902,25 @@ class _EquipmentListState extends State { useExpanded: useExpanded, minWidth: 120, ), - // 장비명 + // 장비번호 _buildDataCell( _buildTextWithTooltip( - equipment.equipment.name, - equipment.equipment.name, + equipment.equipment.equipmentNumber, // name → equipmentNumber (메인 필드) + equipment.equipment.equipmentNumber, ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), - // 카테고리 + // 모델명 _buildDataCell( - _buildCategoryWithTooltip(equipment), - flex: 2, + _buildTextWithTooltip( + equipment.equipment.modelName ?? '-', // 모델명 표시 + equipment.equipment.modelName ?? '-', + ), + flex: 3, useExpanded: useExpanded, - minWidth: 100, + minWidth: 120, ), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ @@ -952,15 +933,6 @@ class _EquipmentListState extends State { useExpanded: useExpanded, minWidth: 120, ), - _buildDataCell( - _buildTextWithTooltip( - equipment.equipment.barcode ?? '-', - equipment.equipment.barcode ?? '-', - ), - flex: 3, - useExpanded: useExpanded, - minWidth: 120, - ), ], // 수량 _buildDataCell( @@ -986,80 +958,6 @@ class _EquipmentListState extends State { useExpanded: useExpanded, minWidth: 80, ), - // 입고지 - _buildDataCell( - Text( - equipment.warehouseLocation ?? '-', - style: ShadcnTheme.bodySmall, - ), - flex: 3, - useExpanded: useExpanded, - minWidth: 120, - ), - // 구매처 (회사명) - _buildDataCell( - Text( - equipment.currentCompany ?? '-', - style: ShadcnTheme.bodySmall, - ), - flex: 3, - useExpanded: useExpanded, - minWidth: 120, - ), - // 구매일 - _buildDataCell( - Text( - equipment.equipment.purchaseDate != null - ? '${equipment.equipment.purchaseDate!.year}/${equipment.equipment.purchaseDate!.month.toString().padLeft(2, '0')}/${equipment.equipment.purchaseDate!.day.toString().padLeft(2, '0')}' - : '-', - style: ShadcnTheme.bodySmall, - ), - flex: 2, - useExpanded: useExpanded, - minWidth: 100, - ), - // 구매가격 - _buildDataCell( - Text( - equipment.equipment.purchasePrice != null - ? '₩${equipment.equipment.purchasePrice!.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}' - : '-', - style: ShadcnTheme.bodySmall, - ), - flex: 2, - useExpanded: useExpanded, - minWidth: 100, - ), - // 상세 정보 (조건부) - if (_showDetailedColumns) ...[ - // 현재 위치 (회사 + 지점) - _buildDataCell( - _buildTextWithTooltip( - _buildCurrentLocationText(equipment), - _buildCurrentLocationText(equipment), - ), - flex: 3, - useExpanded: useExpanded, - minWidth: 120, - ), - // 창고 위치 - _buildDataCell( - Text( - equipment.warehouseLocation ?? '-', - style: ShadcnTheme.bodySmall, - ), - flex: 2, - useExpanded: useExpanded, - minWidth: 100, - ), - // 점검일 (최근/다음) - _buildDataCell( - _buildInspectionDateWidget(equipment), - flex: 2, - useExpanded: useExpanded, - minWidth: 100, - ), - ], // 관리 _buildDataCell( _buildActionButtons(equipment.equipment.id ?? 0), @@ -1250,7 +1148,7 @@ class _EquipmentListState extends State { final result = await EquipmentHistoryDialog.show( context: context, equipmentId: equipmentId, - equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.name}', + equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}', // name → equipmentNumber ); if (result == true) { @@ -1306,81 +1204,7 @@ class _EquipmentListState extends State { return _getFilteredEquipments(); } - /// 카테고리 축약 표기 함수 - String _shortenCategory(String category) { - if (category.length <= 2) return category; - return '${category.substring(0, 2)}...'; - } - - /// 영어 카테고리를 한국어로 변환 - String _translateCategory(String category) { - const Map categoryMap = { - // 대분류 - 'Network': '네트워크', - 'Server': '서버', - 'Storage': '스토리지', - 'Security': '보안', - 'Computer': '컴퓨터', - 'Mobile': '모바일', - 'Printer': '프린터', - 'Monitor': '모니터', - 'Peripheral': '주변기기', - // 중분류 - 'Router': '라우터', - 'Switch': '스위치', - 'Firewall': '방화벽', - 'Laptop': '노트북', - 'Desktop': '데스크톱', - 'Tablet': '태블릿', - 'Smartphone': '스마트폰', - 'Scanner': '스캐너', - 'Keyboard': '키보드', - 'Mouse': '마우스', - // 소분류 예시 - 'Wireless': '무선', - 'Wired': '유선', - 'Gaming': '게이밍', - 'Office': '사무용', - }; - - return categoryMap[category] ?? category; - } - - /// 카테고리 툴팁 위젯 (한국어 변환 적용) - Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) { - // 영어→한국어 변환 적용 - final translatedCategory = _translateCategory(equipment.equipment.category); - final translatedSubCategory = _translateCategory(equipment.equipment.subCategory); - final translatedSubSubCategory = _translateCategory(equipment.equipment.subSubCategory); - - final fullCategory = EquipmentDisplayHelper.formatCategory( - translatedCategory, - translatedSubCategory, - translatedSubSubCategory, - ); - - // 축약 표기 적용 - 비어있지 않은 카테고리만 표시 - final List parts = []; - if (translatedCategory.isNotEmpty) { - parts.add(_shortenCategory(translatedCategory)); - } - if (translatedSubCategory.isNotEmpty) { - parts.add(_shortenCategory(translatedSubCategory)); - } - if (translatedSubSubCategory.isNotEmpty) { - parts.add(_shortenCategory(translatedSubSubCategory)); - } - final shortCategory = parts.join(' > '); - - return Tooltip( - message: fullCategory, - child: Text( - shortCategory, - style: ShadcnTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ); - } + // 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 API에서 제공하지 않음) /// 캐시된 데이터를 사용한 상태 드롭다운 아이템 생성 List> _buildStatusDropdownItems() { @@ -1416,49 +1240,5 @@ class _EquipmentListState extends State { return items; } - /// 현재 위치 텍스트 생성 (회사명 + 지점명) - String _buildCurrentLocationText(UnifiedEquipment equipment) { - final currentCompany = equipment.currentCompany ?? '-'; - final currentBranch = equipment.currentBranch ?? ''; - - if (currentBranch.isNotEmpty) { - return '$currentCompany ($currentBranch)'; - } else { - return currentCompany; - } - } - - /// 점검일 위젯 생성 (최근 점검일/다음 점검일) - Widget _buildInspectionDateWidget(UnifiedEquipment equipment) { - final lastInspection = equipment.lastInspectionDate; - final nextInspection = equipment.nextInspectionDate; - - String displayText = '-'; - Color? textColor; - - if (nextInspection != null) { - final now = DateTime.now(); - final difference = nextInspection.difference(now).inDays; - - if (difference < 0) { - displayText = '점검 필요'; - textColor = Colors.red; - } else if (difference <= 30) { - displayText = '${difference}일 후'; - textColor = Colors.orange; - } else { - displayText = '${nextInspection.month}/${nextInspection.day}'; - textColor = Colors.green; - } - } else if (lastInspection != null) { - displayText = '${lastInspection.month}/${lastInspection.day}'; - } - - return Text( - displayText, - style: ShadcnTheme.bodySmall.copyWith( - color: textColor ?? ShadcnTheme.bodySmall.color, - ), - ); - } + // 사용하지 않는 현재위치, 점검일 관련 함수들 제거됨 (리스트 API에서 제공하지 않음) }