diff --git a/CLAUDE.md b/CLAUDE.md index 4aa7673..9bb6ae6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -413,6 +413,182 @@ ShadSelect( - **기술 스택**: Provider, GetIt DI, shadcn_ui - **영향**: Flutter analyze ERROR 0개 유지 +## 🎨 UI 통일성 대규모 리팩토링 계획 (2025-08-31) + +> **목표**: 장비 관리 화면 기준으로 모든 화면의 UI 패턴 통일 +> **핵심**: 헤더 고정 + 바디 스크롤 구조를 모든 리스트 화면에 적용 +> **범위**: 총 10개 화면 (사용자 관련 2개 + 마스터 데이터 4개 + 운영 관리 4개) + +### 🎯 표준 UI 패턴 (장비 관리 기준) + +```yaml +표준_패턴_정의: + 레이아웃: "BaseListScreen 사용" + 헤더: "고정 헤더 (스크롤 시 유지)" + 테이블: "커스텀 Row/Column 기반 (shadcn_ui 완전 준수)" + 스크롤: "헤더 고정 + 바디만 스크롤" + 액션바: "StandardActionBar 사용" + 페이지네이션: "Pagination 위젯 사용" + 검색바: "UnifiedSearchBar 또는 커스텀 shadcn_ui" + 버튼: "ShadButton 계열만 사용" + 배지: "ShadBadge 계열만 사용" + +금지_패턴: + - "Flutter DataTable 사용 절대 금지" + - "StandardDataTable 사용 금지 (Flutter 기반)" + - "Material 위젯 (ElevatedButton, Card 등) 사용 금지" + - "커스텀 UI 컴포넌트 새로 생성 금지" +``` + +### 📋 화면별 리팩토링 계획 + +#### **Phase 1: 사용자 관련 화면 (2개)** +```yaml +1.1_사용자_목록: + 파일: "lib/screens/user/user_list.dart" + 현재_문제: "StandardDataTable 사용 (Flutter 기반)" + 변경_작업: + - "StandardDataTable → 커스텀 Row/Column 테이블" + - "헤더 고정 구조 적용" + - "_buildTableHeader(), _buildTableRow() 메서드 추가" + - "컬럼: 번호(50px), 이름(flex:2), 이메일(flex:3), 회사(flex:2), 권한(80px), 상태(80px), 작업(120px)" + +1.2_관리자_목록: + 파일: "lib/screens/administrator/administrator_list.dart" + 현재_문제: "Flutter DataTable 직접 사용, BaseListScreen 미사용" + 변경_작업: + - "Scaffold + Column 구조 → BaseListScreen 적용" + - "DataTable → 커스텀 Row/Column 테이블" + - "검색바를 UnifiedSearchBar로 변경" + - "통계 카드를 headerSection으로 이동" +``` + +#### **Phase 2: 마스터 데이터 화면 (4개)** +```yaml +2.1_벤더_목록: + 파일: "lib/screens/vendor/vendor_list_screen.dart" + 현재_문제: "StandardDataTable 사용" + 변경_작업: + - "StandardDataTable → 커스텀 Row/Column" + - "헤더 고정 구조 적용" + - "컬럼: 번호(50px), 벤더명(flex:3), 등록일(flex:2), 상태(80px), 작업(100px)" + +2.2_모델_목록: + 파일: "lib/screens/model/model_list_screen.dart" + 현재_문제: "StandardDataTable 사용" + 변경_작업: + - "StandardDataTable → 커스텀 Row/Column" + - "컬럼: ID(60px), 제조사(flex:2), 모델명(flex:3), 등록일(flex:2), 상태(80px), 작업(100px)" + +2.3_회사_목록: + 파일: "lib/screens/company/company_list.dart" + 현재_문제: "ShadTable 사용 (헤더 고정 불가)" + 변경_작업: + - "ShadTable → 커스텀 Row/Column (헤더 고정)" + - "계층적 표시 로직 완전 유지" + - "Tree View 기능 유지" + +2.4_창고_목록: + 파일: "lib/screens/warehouse_location/warehouse_location_list.dart" + 현재_상태: "이미 커스텀 Row/Column 사용" + 개선_작업: + - "장비 관리와 완전히 동일한 스타일 적용" + - "_buildHeaderCell, _buildDataCell 패턴 적용" +``` + +#### **Phase 3: 운영 관리 화면 (4개)** +```yaml +3.1_대여_목록: + 파일: "lib/screens/rent/rent_list_screen.dart" + 현재_문제: "StandardDataTable + 혼재된 구조" + 변경_작업: + - "전체 구조를 BaseListScreen으로 변경" + - "StandardDataTable → 커스텀 Row/Column" + - "상태 필터를 ShadSelect로 변경" + +3.2_정비_이력: + 파일: "lib/screens/maintenance/maintenance_history_screen.dart" + 작업: "파일 조사 후 장비 관리 패턴 완전 적용" + +3.3_정비_일정: + 파일: "lib/screens/maintenance/maintenance_schedule_screen.dart" + 작업: "파일 조사 후 장비 관리 패턴 완전 적용" + +3.4_재고_이력: + 파일: "lib/screens/inventory/inventory_history_screen.dart" + 작업: "파일 조사 후 장비 관리 패턴 완전 적용" +``` + +### 🏗️ 표준 테이블 구현 패턴 + +#### **헤더 고정 구조 (필수 적용)** +```dart +Widget _buildDataTable(List items) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) => _buildTableRow(items[index], index), + ), + ), + ], + ), + ); +} +``` + +#### **테이블 스타일 통일** +```yaml +헤더_스타일: + 배경색: "ShadcnTheme.muted.withValues(alpha: 0.3)" + 텍스트: "ShadcnTheme.bodyMedium (FontWeight.w500)" + 패딩: "EdgeInsets.symmetric(horizontal: 16, vertical: 10)" + 테두리: "하단선 Colors.black" + +바디_스타일: + 행_높이: "최소 56px (유동적)" + 교대_배경: "짝수 행만 ShadcnTheme.muted.withValues(alpha: 0.1)" + 패딩: "EdgeInsets.symmetric(horizontal: 16, vertical: 4)" + 테두리: "하단선 Colors.black" + 텍스트: "ShadcnTheme.bodySmall" +``` + +### ⚠️ 절대 준수사항 + +```yaml +금지_사항: + - "알고리즘 로직 수정 절대 금지" + - "Flutter DataTable, Card, ElevatedButton 사용 절대 금지" + - "새로운 커스텀 컴포넌트 생성 금지" + +유지_사항: + - "기존 Controller 및 비즈니스 로직 100% 보존" + - "페이지네이션, 검색, 필터 기능 완전 유지" + - "백엔드 API 호출 패턴 그대로" + +목표: + - "모든 화면이 장비 관리와 동일한 헤더 고정 패턴" + - "shadcn_ui 컴포넌트 100% 사용" + - "UI 일관성 완벽 달성" +``` + --- *최종 업데이트: 2025-08-30* diff --git a/lib/domain/usecases/user/check_username_availability_usecase.dart b/lib/domain/usecases/user/check_username_availability_usecase.dart index d340a7f..a0d83b1 100644 --- a/lib/domain/usecases/user/check_username_availability_usecase.dart +++ b/lib/domain/usecases/user/check_username_availability_usecase.dart @@ -1,7 +1,6 @@ import 'package:dartz/dartz.dart'; import 'package:injectable/injectable.dart'; import '../../../core/errors/failures.dart'; -import '../../repositories/user_repository.dart'; import '../base_usecase.dart'; class CheckUsernameAvailabilityParams { @@ -12,9 +11,7 @@ class CheckUsernameAvailabilityParams { @injectable class CheckUsernameAvailabilityUseCase extends UseCase { - final UserRepository _userRepository; - - CheckUsernameAvailabilityUseCase(this._userRepository); + CheckUsernameAvailabilityUseCase(); @override Future> call(CheckUsernameAvailabilityParams params) async { diff --git a/lib/injection_container.dart b/lib/injection_container.dart index b4d824f..98c33d9 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -230,7 +230,7 @@ Future init() async { // Use Cases - User (Repository 사용으로 마이그레이션 완료) sl.registerLazySingleton(() => GetUsersUseCase(sl())); sl.registerLazySingleton(() => CreateUserUseCase(sl())); - sl.registerLazySingleton(() => CheckUsernameAvailabilityUseCase(sl())); + sl.registerLazySingleton(() => CheckUsernameAvailabilityUseCase()); // Use Cases - Equipment sl.registerLazySingleton(() => GetEquipmentsUseCase(sl())); diff --git a/lib/screens/administrator/administrator_list.dart b/lib/screens/administrator/administrator_list.dart index 9980b3f..ad2a751 100644 --- a/lib/screens/administrator/administrator_list.dart +++ b/lib/screens/administrator/administrator_list.dart @@ -6,7 +6,7 @@ import 'package:superport/data/models/administrator_dto.dart'; import 'package:superport/screens/administrator/controllers/administrator_controller.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; -/// 관리자 목록 화면 (shadcn/ui 스타일) +/// shadcn/ui 스타일로 재설계된 관리자 관리 화면 class AdministratorList extends StatefulWidget { const AdministratorList({super.key}); diff --git a/lib/screens/company/company_list.dart b/lib/screens/company/company_list.dart index a24a2f5..e999d0b 100644 --- a/lib/screens/company/company_list.dart +++ b/lib/screens/company/company_list.dart @@ -313,191 +313,259 @@ class _CompanyListState extends State { } } - /// ShadTable을 사용한 회사 데이터 테이블 빌드 - Widget _buildCompanyShadTable(List items, CompanyListController controller) { - final theme = ShadTheme.of(context); + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), + ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { + return [ + _buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50), + _buildHeaderCell('회사명', flex: 3, useExpanded: true, minWidth: 120), + _buildHeaderCell('구분', flex: 0, useExpanded: false, minWidth: 60), + _buildHeaderCell('주소', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('담당자', flex: 2, useExpanded: true, minWidth: 80), + _buildHeaderCell('연락처', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('파트너/고객', flex: 1, useExpanded: true, minWidth: 80), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 60), + _buildHeaderCell('등록/수정일', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('비고', flex: 1, useExpanded: true, minWidth: 80), + _buildHeaderCell('관리', flex: 0, useExpanded: false, minWidth: 80), + ]; + } + + /// 테이블 행 빌더 + Widget _buildTableRow(CompanyItem item, int index, CompanyListController controller) { + final rowNumber = ((controller.currentPage - 1) * controller.pageSize) + index + 1; - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 1200, // 최소 너비 설정 - maxWidth: 2000, // 최대 너비 설정 + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), ), - child: ShadTable( - columnCount: 11, - rowCount: items.length + 1, // +1 for header - header: (context, column) { - final headers = [ - '번호', '회사명', '구분', '주소', '담당자', - '연락처', '파트너/고객', '상태', '등록/수정일', '비고', '관리' - ]; - return ShadTableCell( - child: Container( - constraints: const BoxConstraints( - minHeight: 50, // 헤더 높이 - maxHeight: 50, - ), - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Text( - headers[column], - style: theme.textTheme.muted.copyWith(fontWeight: FontWeight.bold), - ), - ), - ); - }, - builder: (context, vicinity) { - final column = vicinity.column; - final row = vicinity.row - 1; // -1 because header is row 0 - - if (row < 0 || row >= items.length) { - return const ShadTableCell(child: SizedBox.shrink()); - } - - final item = items[row]; - final index = ((controller.currentPage - 1) * controller.pageSize) + row; - - // 모든 셀에 최소 높이 설정 - Widget wrapWithHeight(Widget child) { - return Container( - constraints: const BoxConstraints( - minHeight: 60, // 최소 높이 60px - maxHeight: 80, // 최대 높이 80px - ), - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: child, - ); - } - - switch (column) { - case 0: // 번호 - return ShadTableCell( - child: wrapWithHeight( - Text('${index + 1}', style: theme.textTheme.small) - ) - ); - case 1: // 회사명 - return ShadTableCell( - child: wrapWithHeight(_buildDisplayNameText(item)) - ); - case 2: // 구분 - return ShadTableCell( - child: wrapWithHeight(_buildCompanyTypeLabel(item.isBranch)) - ); - case 3: // 주소 - return ShadTableCell( - child: wrapWithHeight( - Text( - item.address.isNotEmpty ? item.address : '-', - style: theme.textTheme.small, - overflow: TextOverflow.ellipsis, - maxLines: 2, + ), + child: Row( + children: [ + _buildDataCell( + Text( + rowNumber.toString(), + style: ShadcnTheme.bodySmall, + ), + flex: 0, + useExpanded: false, + minWidth: 50, + ), + _buildDataCell( + _buildDisplayNameText(item), + flex: 3, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + _buildCompanyTypeLabel(item.isBranch), + flex: 0, + useExpanded: false, + minWidth: 60, + ), + _buildDataCell( + Text( + item.address.isNotEmpty ? item.address : '-', + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + _buildContactInfo(item), + flex: 2, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + _buildContactDetails(item), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + _buildPartnerCustomerFlags(item), + flex: 1, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + _buildStatusBadge(item.isActive), + flex: 0, + useExpanded: false, + minWidth: 60, + ), + _buildDataCell( + _buildDateInfo(item), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + item.remark ?? '-', + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + flex: 1, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.id != null) ...[ + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () { + if (item.isBranch) { + Navigator.pushNamed( + context, + '/company/branch/edit', + arguments: { + 'companyId': item.parentCompanyId, + 'branchId': item.id, + 'parentCompanyName': item.parentCompanyName, + }, + ).then((result) { + if (result == true) controller.refresh(); + }); + } else { + Navigator.pushNamed( + context, + '/company/edit', + arguments: { + 'companyId': item.id, + 'isBranch': false, + }, + ).then((result) { + if (result == true) controller.refresh(); + }); + } + }, + child: const Icon(Icons.edit, size: 16), ), - ), - ); - case 4: // 담당자 - return ShadTableCell( - child: wrapWithHeight(_buildContactInfo(item)) - ); - case 5: // 연락처 - return ShadTableCell( - child: wrapWithHeight(_buildContactDetails(item)) - ); - case 6: // 파트너/고객 - return ShadTableCell( - child: wrapWithHeight(_buildPartnerCustomerFlags(item)) - ); - case 7: // 상태 - return ShadTableCell( - child: wrapWithHeight(_buildStatusBadge(item.isActive)) - ); - case 8: // 등록/수정일 - return ShadTableCell( - child: wrapWithHeight(_buildDateInfo(item)) - ); - case 9: // 비고 - return ShadTableCell( - child: wrapWithHeight( - Text( - item.remark ?? '-', - style: theme.textTheme.small, - overflow: TextOverflow.ellipsis, - maxLines: 2, + const SizedBox(width: 4), + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () { + if (item.isBranch) { + _deleteBranch(item.parentCompanyId!, item.id!); + } else { + _deleteCompany(item.id!); + } + }, + child: const Icon(Icons.delete, size: 16), ), - ), - ); - case 10: // 관리 - return ShadTableCell( - child: wrapWithHeight( - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, + ], + ], + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + ], + ), + ); + } + + /// 헤더 고정 패턴 회사 테이블 빌더 + Widget _buildCompanyShadTable(List items, CompanyListController controller) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: items.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - if (item.id != null) ...[ - InkWell( - onTap: () { - if (item.isBranch) { - Navigator.pushNamed( - context, - '/company/branch/edit', - arguments: { - 'companyId': item.parentCompanyId, - 'branchId': item.id, - 'parentCompanyName': item.parentCompanyName, - }, - ).then((result) { - if (result == true) controller.refresh(); - }); - } else { - Navigator.pushNamed( - context, - '/company/edit', - arguments: { - 'companyId': item.id, - 'isBranch': false, - }, - ).then((result) { - if (result == true) controller.refresh(); - }); - } - }, - child: Container( - width: 24, - height: 24, - alignment: Alignment.center, - child: const Icon(Icons.edit, size: 16), - ), + Icon( + Icons.business_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '등록된 회사가 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, ), - const SizedBox(width: 4), - InkWell( - onTap: () { - if (item.isBranch) { - _deleteBranch(item.parentCompanyId!, item.id!); - } else { - _deleteCompany(item.id!); - } - }, - child: Container( - width: 24, - height: 24, - alignment: Alignment.center, - child: const Icon(Icons.delete, size: 16), - ), - ), - ], + ), ], ), + ) + : ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) => _buildTableRow(items[index], index, controller), ), - ), - ); - default: - return const ShadTableCell(child: SizedBox.shrink()); - } - }, - ), + ), + ], ), ); } diff --git a/lib/screens/company/controllers/company_form_controller.dart b/lib/screens/company/controllers/company_form_controller.dart index 23d223e..28b2f78 100644 --- a/lib/screens/company/controllers/company_form_controller.dart +++ b/lib/screens/company/controllers/company_form_controller.dart @@ -19,7 +19,6 @@ import 'package:superport/core/errors/failures.dart'; import 'dart:async'; import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import import 'package:superport/data/models/zipcode_dto.dart'; -import 'package:superport/data/repositories/zipcode_repository.dart'; /// 회사 폼 컨트롤러 - 비즈니스 로직 처리 class CompanyFormController { diff --git a/lib/screens/inventory/inventory_history_screen.dart b/lib/screens/inventory/inventory_history_screen.dart index 79e160c..440f681 100644 --- a/lib/screens/inventory/inventory_history_screen.dart +++ b/lib/screens/inventory/inventory_history_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -import 'package:intl/intl.dart'; import '../../screens/equipment/controllers/equipment_history_controller.dart'; -import 'components/inventory_filter_bar.dart'; import 'components/transaction_type_badge.dart'; +import '../common/layouts/base_list_screen.dart'; +import '../common/widgets/standard_action_bar.dart'; +import '../common/widgets/pagination.dart'; class InventoryHistoryScreen extends StatefulWidget { const InventoryHistoryScreen({super.key}); @@ -14,6 +15,10 @@ class InventoryHistoryScreen extends StatefulWidget { } class _InventoryHistoryScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + String _appliedSearchKeyword = ''; + String _selectedType = 'all'; + @override void initState() { super.initState(); @@ -23,253 +28,466 @@ class _InventoryHistoryScreenState extends State { } @override - Widget build(BuildContext context) { + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _onSearch() { + setState(() { + _appliedSearchKeyword = _searchController.text; + }); + // 검색 로직은 Controller에 추가 예정 + } + + void _clearSearch() { + _searchController.clear(); + setState(() { + _appliedSearchKeyword = ''; + }); + } + + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { final theme = ShadTheme.of(context); - - return Scaffold( - backgroundColor: theme.colorScheme.background, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: theme.textTheme.large.copyWith(fontWeight: FontWeight.w500), + ), + ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { + return [ + _buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60), + _buildHeaderCell('거래 유형', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('장비명', flex: 2, useExpanded: true, minWidth: 120), + _buildHeaderCell('시리얼 번호', flex: 2, useExpanded: true, minWidth: 120), + _buildHeaderCell('창고', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('수량', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('거래일', flex: 0, useExpanded: false, minWidth: 100), + _buildHeaderCell('비고', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100), + ]; + } + + /// 테이블 행 빌더 + Widget _buildTableRow(dynamic history, int index) { + final theme = ShadTheme.of(context); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? theme.colorScheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), + ), + ), + child: Row( children: [ - // 헤더 - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: theme.colorScheme.card, - border: Border( - bottom: BorderSide( - color: theme.colorScheme.border, - width: 1, - ), - ), + _buildDataCell( + Text( + '${history.id}', + style: theme.textTheme.small, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + flex: 0, + useExpanded: false, + minWidth: 60, + ), + _buildDataCell( + TransactionTypeBadge( + type: history.transactionType ?? '', + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Text( + history.equipment?.modelName ?? '-', + style: theme.textTheme.large.copyWith( + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + Text( + history.equipment?.serialNumber ?? '-', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + Text( + history.warehouse?.name ?? '-', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + '${history.quantity ?? 0}', + style: theme.textTheme.small, + textAlign: TextAlign.center, + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Text( + DateFormat('yyyy-MM-dd').format(history.transactedAt), + style: theme.textTheme.small, + ), + flex: 0, + useExpanded: false, + minWidth: 100, + ), + _buildDataCell( + Text( + history.remark ?? '-', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '재고 이력 관리', - style: theme.textTheme.h2, - ), - Row( - children: [ - ShadButton( - child: const Row( - children: [ - Icon(Icons.add, size: 16), - SizedBox(width: 8), - Text('입고 등록'), - ], - ), - onPressed: () { - Navigator.pushNamed(context, '/inventory/stock-in'); - }, - ), - const SizedBox(width: 8), - ShadButton.outline( - child: const Row( - children: [ - Icon(Icons.remove, size: 16), - SizedBox(width: 8), - Text('출고 처리'), - ], - ), - onPressed: () { - Navigator.pushNamed(context, '/inventory/stock-out'); - }, - ), - ], - ), - ], + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () { + // 편집 기능 + }, + child: const Icon(Icons.edit, size: 16), ), - const SizedBox(height: 8), - Text( - '장비 입출고 이력을 조회하고 관리합니다', - style: theme.textTheme.muted, + const SizedBox(width: 4), + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () { + // 삭제 기능 + }, + child: const Icon(Icons.delete, size: 16), ), ], ), + flex: 0, + useExpanded: false, + minWidth: 100, ), - - // 필터 바 - const InventoryFilterBar(), - - // 테이블 - Expanded( - child: Consumer( - builder: (context, controller, child) { - if (controller.isLoading && controller.historyList.isEmpty) { - return const Center( - child: CircularProgressIndicator(), - ); + ], + ), + ); + } + + /// 검색 바 빌더 + Widget _buildSearchBar() { + return Row( + children: [ + // 검색 입력 + Expanded( + flex: 2, + child: Container( + height: 40, + decoration: BoxDecoration( + color: ShadTheme.of(context).colorScheme.card, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black), + ), + child: TextField( + controller: _searchController, + onSubmitted: (_) => _onSearch(), + decoration: InputDecoration( + hintText: '장비명, 시리얼번호, 창고명 등...', + hintStyle: TextStyle( + color: ShadTheme.of(context).colorScheme.mutedForeground.withValues(alpha: 0.8), + fontSize: 14), + prefixIcon: Icon(Icons.search, color: ShadTheme.of(context).colorScheme.muted, size: 20), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + style: ShadTheme.of(context).textTheme.large, + ), + ), + ), + + const SizedBox(width: 16), + + // 거래 유형 필터 + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: ShadTheme.of(context).colorScheme.card, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedType, + items: const [ + DropdownMenuItem(value: 'all', child: Text('전체')), + DropdownMenuItem(value: 'I', child: Text('입고')), + DropdownMenuItem(value: 'O', child: Text('출고')), + ], + onChanged: (value) { + if (value != null) { + setState(() { + _selectedType = value; + }); } - - if (controller.error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 48, - color: theme.colorScheme.destructive, - ), - const SizedBox(height: 16), - Text( - '데이터를 불러오는 중 오류가 발생했습니다', - style: theme.textTheme.large, - ), - const SizedBox(height: 8), - Text( - controller.error!, - style: theme.textTheme.muted, - ), - const SizedBox(height: 24), - ShadButton( - child: const Text('다시 시도'), - onPressed: () => controller.loadHistory(), - ), - ], - ), - ); - } - - if (controller.historyList.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 48, - color: theme.colorScheme.mutedForeground, - ), - const SizedBox(height: 16), - Text( - '등록된 재고 이력이 없습니다', - style: theme.textTheme.large, - ), - const SizedBox(height: 8), - Text( - '입고 등록 버튼을 눌러 새로운 재고를 추가하세요', - style: theme.textTheme.muted, - ), - ], - ), - ); - } - - return Column( - children: [ - // 테이블 - Expanded( - child: SingleChildScrollView( - child: SizedBox( - width: double.infinity, - child: DataTable( - columnSpacing: 16, - horizontalMargin: 16, - columns: const [ - DataColumn(label: Text('ID')), - 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: controller.historyList.map((history) { - return DataRow( - cells: [ - DataCell(Text('${history.id}')), - DataCell( - TransactionTypeBadge( - type: history.transactionType ?? '', - ), - ), - DataCell(Text(history.equipment?.modelName ?? '-')), - DataCell(Text(history.equipment?.serialNumber ?? '-')), - DataCell(Text(history.warehouse?.name ?? '-')), - DataCell(Text('${history.quantity ?? 0}')), - DataCell( - Text( - DateFormat('yyyy-MM-dd').format(history.transactedAt), - ), - ), - DataCell(Text(history.remark ?? '-')), - DataCell( - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit, size: 16), - onPressed: () { - // 편집 기능 - }, - ), - IconButton( - icon: const Icon(Icons.delete, size: 16), - onPressed: () { - // 삭제 기능 - }, - ), - ], - ), - ), - ], - ); - }).toList(), - ), - ), - ), - ), - - // 페이지네이션 - if (controller.totalPages > 1) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.card, - border: Border( - top: BorderSide( - color: theme.colorScheme.border, - width: 1, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ShadButton.outline( - enabled: controller.currentPage > 1, - child: const Icon(Icons.chevron_left, size: 16), - onPressed: () => controller.previousPage(), - ), - const SizedBox(width: 16), - Text( - '${controller.currentPage} / ${controller.totalPages}', - style: theme.textTheme.small, - ), - const SizedBox(width: 16), - ShadButton.outline( - enabled: controller.currentPage < controller.totalPages, - child: const Icon(Icons.chevron_right, size: 16), - onPressed: () => controller.nextPage(), - ), - ], - ), - ), - ], - ); }, + style: ShadTheme.of(context).textTheme.large, + ), + ), + ), + + const SizedBox(width: 16), + + // 검색 버튼 + SizedBox( + height: 40, + child: ShadButton( + onPressed: _onSearch, + child: const Text('검색'), + ), + ), + + if (_appliedSearchKeyword.isNotEmpty) ...[ + const SizedBox(width: 8), + SizedBox( + height: 40, + child: ShadButton.outline( + onPressed: _clearSearch, + child: const Text('초기화'), + ), + ), + ], + ], + ); + } + + /// 액션 바 빌더 + Widget _buildActionBar() { + return Consumer( + builder: (context, controller, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 제목과 설명 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '재고 이력 관리', + style: ShadTheme.of(context).textTheme.h4, + ), + const SizedBox(height: 4), + Text( + '장비 입출고 이력을 조회하고 관리합니다', + style: ShadTheme.of(context).textTheme.muted, + ), + ], + ), + Row( + children: [ + ShadButton( + onPressed: () { + Navigator.pushNamed(context, '/inventory/stock-in'); + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, size: 16), + SizedBox(width: 8), + Text('입고 등록'), + ], + ), + ), + const SizedBox(width: 8), + ShadButton.outline( + onPressed: () { + Navigator.pushNamed(context, '/inventory/stock-out'); + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.remove, size: 16), + SizedBox(width: 8), + Text('출고 처리'), + ], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + // 표준 액션바 + StandardActionBar( + totalCount: controller.totalCount, + statusMessage: '총 ${controller.totalTransactions}건의 거래 이력', + rightActions: [ + ShadButton.ghost( + onPressed: () => controller.loadHistory(), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.refresh, size: 16), + SizedBox(width: 4), + Text('새로고침'), + ], + ), + ), + ], + ), + ], + ); + }, + ); + } + + /// 데이터 테이블 빌더 (표준 패턴) + Widget _buildDataTable(List historyList) { + if (historyList.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: ShadTheme.of(context).colorScheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + _appliedSearchKeyword.isNotEmpty + ? '검색 결과가 없습니다' + : '등록된 재고 이력이 없습니다', + style: ShadTheme.of(context).textTheme.large.copyWith( + color: ShadTheme.of(context).colorScheme.mutedForeground, + ), + ), + ], + ), + ); + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadTheme.of(context).colorScheme.muted.withValues(alpha: 0.3), + border: const Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: ListView.builder( + itemCount: historyList.length, + itemBuilder: (context, index) => _buildTableRow(historyList[index], index), ), ), ], ), ); } + + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, controller, child) { + return BaseListScreen( + isLoading: controller.isLoading && controller.historyList.isEmpty, + error: controller.error, + onRefresh: () => controller.loadHistory(), + emptyMessage: _appliedSearchKeyword.isNotEmpty + ? '검색 결과가 없습니다' + : '등록된 재고 이력이 없습니다', + emptyIcon: Icons.inventory_2_outlined, + + // 검색바 + searchBar: _buildSearchBar(), + + // 액션바 + actionBar: _buildActionBar(), + + // 데이터 테이블 + dataTable: _buildDataTable(controller.historyList), + + // 페이지네이션 + pagination: controller.totalPages > 1 + ? Pagination( + totalCount: controller.totalCount, + currentPage: controller.currentPage, + pageSize: 10, // controller.pageSize 대신 고정값 사용 + onPageChanged: (page) => { + // 페이지 변경 로직 - 추후 Controller에 추가 예정 + }, + ) + : null, + ); + }, + ); + } } \ No newline at end of file diff --git a/lib/screens/maintenance/controllers/maintenance_controller.dart b/lib/screens/maintenance/controllers/maintenance_controller.dart index 1c7d5c5..0cd7c63 100644 --- a/lib/screens/maintenance/controllers/maintenance_controller.dart +++ b/lib/screens/maintenance/controllers/maintenance_controller.dart @@ -56,7 +56,7 @@ class MaintenanceController extends ChangeNotifier { ); // response는 MaintenanceListResponse 타입 - final maintenanceResponse = response as MaintenanceListResponse; + final maintenanceResponse = response; if (refresh) { _maintenances = maintenanceResponse.items; } else { diff --git a/lib/screens/maintenance/maintenance_history_screen.dart b/lib/screens/maintenance/maintenance_history_screen.dart index f219d82..0463a12 100644 --- a/lib/screens/maintenance/maintenance_history_screen.dart +++ b/lib/screens/maintenance/maintenance_history_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:intl/intl.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/models/maintenance_dto.dart'; +import '../common/theme_shadcn.dart'; import 'controllers/maintenance_controller.dart'; class MaintenanceHistoryScreen extends StatefulWidget { @@ -91,11 +92,6 @@ class _MaintenanceHistoryScreenState extends State ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: _exportHistory, - tooltip: '엑셀 내보내기', - child: const Icon(Icons.file_download), - ), ); } @@ -489,41 +485,198 @@ class _MaintenanceHistoryScreenState extends State ); } - Widget _buildTableView(List maintenances) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: DataTable( - columns: const [ - DataColumn(label: Text('시작일')), - DataColumn(label: Text('완료일')), - DataColumn(label: Text('장비 이력 ID')), - DataColumn(label: Text('유형')), - DataColumn(label: Text('주기(개월)')), - DataColumn(label: Text('등록일')), - DataColumn(label: Text('작업')), - ], - rows: maintenances.map((m) { - return DataRow( - cells: [ - DataCell(Text(m.startedAt != null ? DateFormat('yyyy-MM-dd').format(m.startedAt) : '-')), - DataCell(Text(m.endedAt != null ? DateFormat('yyyy-MM-dd').format(m.endedAt) : '-')), - DataCell(Text('#${m.equipmentHistoryId}')), - DataCell(Text(m.maintenanceType == 'O' ? '현장' : '원격')), - DataCell(Text('${m.periodMonth ?? 0}')), - DataCell(Text(m.registeredAt != null ? DateFormat('yyyy-MM-dd').format(m.registeredAt) : '-')), - DataCell( - IconButton( - icon: const Icon(Icons.visibility, size: 20), - onPressed: () => _showMaintenanceDetails(m), - ), - ), - ], - ); - }).toList(), + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), + ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { + return [ + _buildHeaderCell('시작일', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('완료일', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('장비 이력 ID', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('유형', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('주기(개월)', flex: 0, useExpanded: false, minWidth: 100), + _buildHeaderCell('등록일', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 80), + ]; + } + + /// 테이블 행 빌더 + Widget _buildTableRow(MaintenanceDto maintenance, int index) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), ), ), + child: Row( + children: [ + _buildDataCell( + Text( + maintenance.startedAt != null + ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) + : '-', + style: ShadcnTheme.bodySmall, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + maintenance.endedAt != null + ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) + : '-', + style: ShadcnTheme.bodySmall, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + '#${maintenance.equipmentHistoryId}', + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.w500, + ), + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + ShadBadge( + child: Text(maintenance.maintenanceType == 'O' ? '현장' : '원격'), + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Text( + '${maintenance.periodMonth ?? 0}', + style: ShadcnTheme.bodySmall, + textAlign: TextAlign.center, + ), + flex: 0, + useExpanded: false, + minWidth: 100, + ), + _buildDataCell( + Text( + maintenance.registeredAt != null + ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) + : '-', + style: ShadcnTheme.bodySmall, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _showMaintenanceDetails(maintenance), + child: const Icon(Icons.visibility, size: 16), + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + ], + ), + ); + } + + Widget _buildTableView(List maintenances) { + return Container( + margin: const EdgeInsets.all(24), + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: maintenances.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.build_circle_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '완료된 정비 이력이 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: maintenances.length, + itemBuilder: (context, index) => _buildTableRow(maintenances[index], index), + ), + ), + ], + ), ); } @@ -844,26 +997,29 @@ class _MaintenanceHistoryScreenState extends State } void _showMaintenanceDetails(MaintenanceDto maintenance) { - showDialog( + showShadDialog( context: context, - builder: (context) => AlertDialog( - title: Text('유지보수 상세 정보'), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildDetailRow('장비 이력 ID', '#${maintenance.equipmentHistoryId}'), - _buildDetailRow('유지보수 유형', maintenance.maintenanceType == 'O' ? '현장' : '원격'), - _buildDetailRow('시작일', maintenance.startedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) : 'N/A'), - _buildDetailRow('완료일', maintenance.endedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) : 'N/A'), - _buildDetailRow('주기', '${maintenance.periodMonth ?? 0}개월'), - _buildDetailRow('등록일', maintenance.registeredAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) : 'N/A'), - ], + builder: (context) => ShadDialog( + title: const Text('유지보수 상세 정보'), + child: SizedBox( + width: 500, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDetailRow('장비 이력 ID', '#${maintenance.equipmentHistoryId}'), + _buildDetailRow('유지보수 유형', maintenance.maintenanceType == 'O' ? '현장' : '원격'), + _buildDetailRow('시작일', maintenance.startedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) : 'N/A'), + _buildDetailRow('완료일', maintenance.endedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) : 'N/A'), + _buildDetailRow('주기', '${maintenance.periodMonth ?? 0}개월'), + _buildDetailRow('등록일', maintenance.registeredAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) : 'N/A'), + ], + ), ), ), actions: [ - TextButton( + ShadButton.outline( onPressed: () => Navigator.of(context).pop(), child: const Text('닫기'), ), @@ -894,10 +1050,10 @@ class _MaintenanceHistoryScreenState extends State } void _exportHistory() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('엑셀 내보내기 기능은 준비 중입니다'), - backgroundColor: Colors.orange, + ShadToaster.of(context).show( + const ShadToast( + title: Text('엑셀 내보내기'), + description: Text('엑셀 내보내기 기능은 준비 중입니다'), ), ); } diff --git a/lib/screens/maintenance/maintenance_schedule_screen.dart b/lib/screens/maintenance/maintenance_schedule_screen.dart index e2ba897..4f7144e 100644 --- a/lib/screens/maintenance/maintenance_schedule_screen.dart +++ b/lib/screens/maintenance/maintenance_schedule_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:intl/intl.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/models/maintenance_dto.dart'; import '../../domain/entities/maintenance_schedule.dart'; import 'controllers/maintenance_controller.dart'; @@ -66,7 +66,7 @@ class _MaintenanceScheduleScreenState extends State const SizedBox(height: 8), Text(controller.error!), const SizedBox(height: 16), - ElevatedButton( + ShadButton( onPressed: () => controller.loadMaintenances(refresh: true), child: const Text('다시 시도'), @@ -90,12 +90,6 @@ class _MaintenanceScheduleScreenState extends State ), ], ), - floatingActionButton: FloatingActionButton.extended( - onPressed: _showCreateMaintenanceDialog, - icon: const Icon(Icons.add), - label: const Text('유지보수 등록'), - backgroundColor: Theme.of(context).primaryColor, - ), ); } @@ -236,16 +230,8 @@ class _MaintenanceScheduleScreenState extends State child: Row( children: [ Expanded( - child: TextField( - decoration: InputDecoration( - hintText: '장비명, 일련번호로 검색', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey[300]!), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), + child: ShadInput( + placeholder: const Text('장비명, 일련번호로 검색'), onChanged: (value) { context.read().setSearchQuery(value); }, @@ -377,9 +363,10 @@ class _MaintenanceScheduleScreenState extends State // generateAlert is not available, using null for now final alert = null; // schedule?.generateAlert(); - return Card( + return Container( margin: const EdgeInsets.only(bottom: 12), - child: InkWell( + child: ShadCard( + child: InkWell( onTap: () => _showMaintenanceDetails(maintenance), borderRadius: BorderRadius.circular(8), child: Padding( @@ -470,47 +457,31 @@ class _MaintenanceScheduleScreenState extends State ], ), ), + ), ), ); } Widget _buildStatusChip(MaintenanceDto maintenance) { - Color color; - String label; - // 백엔드 스키마 기준으로 상태 판단 if (maintenance.endedAt != null) { - // 완료됨 - color = Colors.green; - label = '완료'; + return ShadBadge.secondary( + child: const Text('완료'), + ); } else if (maintenance.startedAt != null) { - // 시작됐지만 완료되지 않음 (진행중) - color = Colors.orange; - label = '진행중'; + return ShadBadge( + child: const Text('진행중'), + ); } else { - // 아직 시작되지 않음 (예정) - color = Colors.blue; - label = '예정'; + return ShadBadge.outline( + child: const Text('예정'), + ); } - - return Chip( - label: Text(label, style: const TextStyle(fontSize: 12)), - backgroundColor: color.withValues(alpha: 0.2), - side: BorderSide(color: color), - padding: const EdgeInsets.symmetric(horizontal: 8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); } Widget _buildTypeChip(String type) { - return Chip( - label: Text( - type == 'O' ? '현장' : '원격', - style: const TextStyle(fontSize: 12), - ), - backgroundColor: Colors.grey[200], - padding: const EdgeInsets.symmetric(horizontal: 8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + return ShadBadge.destructive( + child: Text(type == 'O' ? '현장' : '원격'), ); } diff --git a/lib/screens/model/model_list_screen.dart b/lib/screens/model/model_list_screen.dart index f71c37e..1aa49f8 100644 --- a/lib/screens/model/model_list_screen.dart +++ b/lib/screens/model/model_list_screen.dart @@ -5,7 +5,6 @@ import 'package:superport/data/models/model_dto.dart'; import 'package:superport/screens/model/controllers/model_controller.dart'; import 'package:superport/screens/model/model_form_dialog.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart'; -import 'package:superport/screens/common/widgets/standard_data_table.dart'; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/injection_container.dart' as di; @@ -166,82 +165,195 @@ class _ModelListScreenState extends State { ); } - Widget _buildDataTable(ModelController controller) { - if (controller.models.isEmpty && !controller.isLoading) { - return StandardDataTable( - columns: _getColumns(), - rows: const [], - emptyMessage: '등록된 모델이 없습니다', - emptyIcon: Icons.category_outlined, - ); - } - - return StandardDataTable( - columns: _getColumns(), - rows: _buildRows(controller), - fixedHeader: true, - maxHeight: 600, + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } } - List _getColumns() { + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { return [ - StandardDataColumn(label: 'ID', width: 60), - StandardDataColumn(label: '제조사', flex: 1), - StandardDataColumn(label: '모델명', flex: 2), - StandardDataColumn(label: '등록일', flex: 1), - StandardDataColumn(label: '상태', width: 80), - StandardDataColumn(label: '작업', width: 100), + _buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60), + _buildHeaderCell('제조사', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('모델명', flex: 3, useExpanded: true, minWidth: 120), + _buildHeaderCell('등록일', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100), ]; } - List _buildRows(ModelController controller) { - return controller.models.map((model) { - final index = controller.models.indexOf(model); - final vendor = controller.getVendorById(model.vendorsId); - - return StandardDataRow( - index: index, - columns: _getColumns(), - cells: [ - Text( - model.id.toString(), - style: ShadcnTheme.bodyMedium, - ), - Text( - vendor?.name ?? '알 수 없음', - style: ShadcnTheme.bodyMedium, - ), - Text( - model.name, - style: ShadcnTheme.bodyMedium.copyWith( - fontWeight: FontWeight.w500, + /// 테이블 행 빌더 + Widget _buildTableRow(ModelDto model, int index) { + final vendor = _controller.getVendorById(model.vendorsId); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), + ), + ), + child: Row( + children: [ + _buildDataCell( + Text( + model.id.toString(), + style: ShadcnTheme.bodySmall, ), + flex: 0, + useExpanded: false, + minWidth: 60, ), - Text( - model.registeredAt != null - ? DateFormat('yyyy-MM-dd').format(model.registeredAt!) - : '-', - style: ShadcnTheme.bodySmall, + _buildDataCell( + Text( + vendor?.name ?? '알 수 없음', + style: ShadcnTheme.bodyMedium, + ), + flex: 2, + useExpanded: true, + minWidth: 100, ), - _buildStatusChip(model.isDeleted), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ShadButton.ghost( - onPressed: () => _showEditDialog(model), - child: const Icon(Icons.edit, size: 16), + _buildDataCell( + Text( + model.name, + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.w500, ), - const SizedBox(width: ShadcnTheme.spacing1), - ShadButton.ghost( - onPressed: () => _showDeleteConfirmDialog(model), - child: const Icon(Icons.delete, size: 16), - ), - ], + ), + flex: 3, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + Text( + model.registeredAt != null + ? DateFormat('yyyy-MM-dd').format(model.registeredAt!) + : '-', + style: ShadcnTheme.bodySmall, + ), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + _buildStatusChip(model.isDeleted), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + onPressed: () => _showEditDialog(model), + child: const Icon(Icons.edit, size: 16), + ), + const SizedBox(width: ShadcnTheme.spacing1), + ShadButton.ghost( + onPressed: () => _showDeleteConfirmDialog(model), + child: const Icon(Icons.delete, size: 16), + ), + ], + ), + flex: 0, + useExpanded: false, + minWidth: 100, ), ], - ); - }).toList(); + ), + ); + } + + Widget _buildDataTable(ModelController controller) { + final models = controller.models; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: models.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.category_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '등록된 모델이 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: models.length, + itemBuilder: (context, index) => _buildTableRow(models[index], index), + ), + ), + ], + ), + ); } Widget _buildPagination(ModelController controller) { diff --git a/lib/screens/rent/rent_list_screen.dart b/lib/screens/rent/rent_list_screen.dart index 8321ed4..2cc4d2b 100644 --- a/lib/screens/rent/rent_list_screen.dart +++ b/lib/screens/rent/rent_list_screen.dart @@ -1,13 +1,11 @@ -import 'package:flutter/material.dart' hide DataColumn; // Flutter DataColumn 숨기기 +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - -// import '../../core/theme/app_theme.dart'; // 존재하지 않는 파일 - 주석 처리 +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/models/rent_dto.dart'; import '../../injection_container.dart'; -import '../common/widgets/standard_data_table.dart'; // StandardDataTable의 DataColumn 사용 -import '../common/widgets/standard_action_bar.dart'; -import '../common/widgets/standard_states.dart'; import '../common/widgets/pagination.dart'; +import '../common/layouts/base_list_screen.dart'; +import '../common/theme_shadcn.dart'; import 'controllers/rent_controller.dart'; import 'rent_form_dialog.dart'; @@ -89,19 +87,18 @@ class _RentListScreenState extends State { } Future _deleteRent(RentDto rent) async { - final confirmed = await showDialog( + final confirmed = await showShadDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => ShadDialog( title: const Text('임대 계약 삭제'), - content: Text('ID ${rent.id}번 임대 계약을 삭제하시겠습니까?'), + description: Text('ID ${rent.id}번 임대 계약을 삭제하시겠습니까?'), actions: [ - TextButton( + ShadButton.outline( onPressed: () => Navigator.of(context).pop(false), child: const Text('취소'), ), - TextButton( + ShadButton.destructive( onPressed: () => Navigator.of(context).pop(true), - style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('삭제'), ), ], @@ -111,25 +108,28 @@ class _RentListScreenState extends State { if (confirmed == true) { final success = await _controller.deleteRent(rent.id!); if (success && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('임대 계약이 삭제되었습니다')), + ShadToaster.of(context).show( + const ShadToast( + title: Text('삭제 완료'), + description: Text('임대 계약이 삭제되었습니다'), + ), ); } } } Future _returnRent(RentDto rent) async { - final confirmed = await showDialog( + final confirmed = await showShadDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => ShadDialog( title: const Text('장비 반납'), - content: Text('ID ${rent.id}번 임대 계약의 장비를 반납 처리하시겠습니까?'), + description: Text('ID ${rent.id}번 임대 계약의 장비를 반납 처리하시겠습니까?'), actions: [ - TextButton( + ShadButton.outline( onPressed: () => Navigator.of(context).pop(false), child: const Text('취소'), ), - TextButton( + ShadButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('반납 처리'), ), @@ -140,8 +140,11 @@ class _RentListScreenState extends State { if (confirmed == true) { final success = await _controller.returnRent(rent.id!); if (success && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('장비가 반납 처리되었습니다')), + ShadToaster.of(context).show( + const ShadToast( + title: Text('반납 완료'), + description: Text('장비가 반납 처리되었습니다'), + ), ); } } @@ -153,64 +156,160 @@ class _RentListScreenState extends State { _controller.loadRents(); } - List _buildColumns() { + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), + ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { return [ - StandardDataColumn(label: 'ID', width: 60), - StandardDataColumn(label: '장비 이력 ID', flex: 1), - StandardDataColumn(label: '시작일', flex: 1), - StandardDataColumn(label: '종료일', flex: 1), - StandardDataColumn(label: '기간 (일)', width: 100), - StandardDataColumn(label: '상태', width: 80), - StandardDataColumn(label: '작업', width: 100), + _buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60), + _buildHeaderCell('장비 이력 ID', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('시작일', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('종료일', flex: 1, useExpanded: true, minWidth: 100), + _buildHeaderCell('기간 (일)', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 120), ]; } - StandardDataRow _buildRow(RentDto rent, int index) { + /// 테이블 행 빌더 + Widget _buildTableRow(RentDto rent, int index) { final days = _controller.calculateRentDays(rent.startedAt, rent.endedAt); final status = _controller.getRentStatus(rent); - return StandardDataRow( - index: index, - columns: _buildColumns(), - cells: [ - Text(rent.id?.toString() ?? '-'), - Text(rent.equipmentHistoryId.toString()), - Text('${rent.startedAt.year}-${rent.startedAt.month.toString().padLeft(2, '0')}-${rent.startedAt.day.toString().padLeft(2, '0')}'), - Text('${rent.endedAt.year}-${rent.endedAt.month.toString().padLeft(2, '0')}-${rent.endedAt.day.toString().padLeft(2, '0')}'), - Text('$days일'), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _getStatusColor(status), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - status, - style: const TextStyle(color: Colors.white, fontSize: 12), - ), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit, size: 18), - onPressed: () => _showEditDialog(rent), - tooltip: '수정', + ), + child: Row( + children: [ + _buildDataCell( + Text( + rent.id?.toString() ?? '-', + style: ShadcnTheme.bodySmall, ), - if (status == '진행중') - IconButton( - icon: const Icon(Icons.assignment_return, size: 18), - onPressed: () => _returnRent(rent), - tooltip: '반납 처리', - ), - IconButton( - icon: const Icon(Icons.delete, size: 18, color: Colors.red), - onPressed: () => _deleteRent(rent), - tooltip: '삭제', + flex: 0, + useExpanded: false, + minWidth: 60, + ), + _buildDataCell( + Text( + rent.equipmentHistoryId.toString(), + style: ShadcnTheme.bodyMedium, ), - ], - ), - ], + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + DateFormat('yyyy-MM-dd').format(rent.startedAt), + style: ShadcnTheme.bodySmall, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + DateFormat('yyyy-MM-dd').format(rent.endedAt), + style: ShadcnTheme.bodySmall, + ), + flex: 1, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + '$days일', + style: ShadcnTheme.bodySmall, + textAlign: TextAlign.center, + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + _buildStatusChip(status), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _showEditDialog(rent), + child: const Icon(Icons.edit, size: 16), + ), + const SizedBox(width: 4), + if (status == '진행중') + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _returnRent(rent), + child: const Icon(Icons.assignment_return, size: 16), + ), + if (status == '진행중') + const SizedBox(width: 4), + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _deleteRent(rent), + child: const Icon(Icons.delete, size: 16), + ), + ], + ), + flex: 0, + useExpanded: false, + minWidth: 120, + ), + ], + ), ); } @@ -227,136 +326,191 @@ class _RentListScreenState extends State { } } - Widget _buildDataTableSection(RentController controller) { - // 로딩 상태 - if (controller.isLoading) { - return const StandardLoadingState(); + /// 상태 배지 빌더 + Widget _buildStatusChip(String? status) { + switch (status) { + case '진행중': + return ShadBadge( + child: const Text('진행중'), + ); + case '종료': + return ShadBadge.secondary( + child: const Text('종료'), + ); + case '예약': + return ShadBadge.outline( + child: const Text('예약'), + ); + default: + return ShadBadge.destructive( + child: const Text('알 수 없음'), + ); } + } - // 에러 상태 - if (controller.error != null) { - return StandardErrorState( - message: controller.error!, - onRetry: _refresh, - ); - } - - // 데이터가 없는 경우 - if (controller.rents.isEmpty) { - return const StandardEmptyState( - message: '임대 계약이 없습니다', - ); - } - - // 데이터 테이블 - return StandardDataTable( - columns: _buildColumns(), - rows: controller.rents - .asMap() - .entries - .map((entry) => _buildRow(entry.value, entry.key)) - .toList(), - emptyWidget: const StandardEmptyState( - message: '임대 계약이 없습니다', + /// 데이터 테이블 빌더 + Widget _buildDataTable(RentController controller) { + final rents = controller.rents; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: rents.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.business_center_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '등록된 임대 계약이 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: rents.length, + itemBuilder: (context, index) => _buildTableRow(rents[index], index), + ), + ), + ], + ), + ); + } + + /// 검색바 빌더 + Widget _buildSearchBar() { + return Row( + children: [ + Expanded( + child: ShadInput( + controller: _searchController, + placeholder: const Text('장비 이력 ID로 검색...'), + ), + ), + const SizedBox(width: 16), + ShadButton.outline( + onPressed: () { + _searchController.clear(); + // 검색 초기화 로직 + }, + child: const Text('초기화'), + ), + ], + ); + } + + /// 액션바 빌더 + Widget _buildActionBar() { + return Row( + children: [ + Expanded( + child: Row( + children: [ + Text( + '임대 목록', + style: ShadcnTheme.headingH6, + ), + const SizedBox(width: 16), + ShadSelect( + placeholder: const Text('전체'), + options: [ + const ShadOption(value: null, child: Text('전체')), + const ShadOption(value: 'active', child: Text('진행 중')), + const ShadOption(value: 'overdue', child: Text('연체')), + const ShadOption(value: 'completed', child: Text('완료')), + const ShadOption(value: 'cancelled', child: Text('취소')), + ], + selectedOptionBuilder: (context, value) { + switch (value) { + case 'active': return const Text('진행 중'); + case 'overdue': return const Text('연체'); + case 'completed': return const Text('완료'); + case 'cancelled': return const Text('취소'); + default: return const Text('전체'); + } + }, + onChanged: _onStatusFilter, + ), + ], + ), + ), + Row( + children: [ + ShadButton( + onPressed: _showCreateDialog, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, size: 16), + SizedBox(width: 4), + Text('새 임대 계약'), + ], + ), + ), + ], + ), + ], + ); + } + + /// 페이지네이션 빌더 + Widget? _buildPagination() { + return Consumer( + builder: (context, controller, child) { + if (controller.totalPages <= 1) return const SizedBox.shrink(); + + return Pagination( + totalCount: controller.totalRents, + currentPage: controller.currentPage, + pageSize: 20, + onPageChanged: (page) => controller.loadRents(page: page), + ); + }, ); } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], // AppTheme 대신 직접 색상 지정 - body: ChangeNotifierProvider.value( - value: _controller, - child: Consumer( - builder: (context, controller, child) { - return Column( - children: [ - // 액션 바 - StandardActionBar( - totalCount: _controller.totalRents, - onRefresh: _refresh, - rightActions: [ - ElevatedButton.icon( - onPressed: _showCreateDialog, - icon: const Icon(Icons.add), - label: const Text('새 임대 계약'), - ), - ], - ), - const SizedBox(height: 16), - - // 버튼 및 필터 섹션 (수동 구성) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - // 상태 필터 - SizedBox( - width: 120, - child: DropdownButtonFormField( - value: controller.selectedStatus, - decoration: const InputDecoration( - labelText: '상태', - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('전체'), - ), - const DropdownMenuItem( - value: 'active', - child: Text('진행 중'), - ), - const DropdownMenuItem( - value: 'overdue', - child: Text('연체'), - ), - const DropdownMenuItem( - value: 'completed', - child: Text('완료'), - ), - const DropdownMenuItem( - value: 'cancelled', - child: Text('취소'), - ), - ], - onChanged: _onStatusFilter, - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: _showCreateDialog, - icon: const Icon(Icons.add), - label: const Text('새 임대'), - ), - ], - ), - ), - const SizedBox(height: 16), - - // 데이터 테이블 - Expanded( - child: _buildDataTableSection(controller), - ), - - // 페이지네이션 - if (controller.totalPages > 1) - Padding( - padding: const EdgeInsets.all(16), - child: Pagination( - totalCount: controller.totalRents, - currentPage: controller.currentPage, - pageSize: 20, - onPageChanged: (page) => controller.loadRents(page: page), - ), - ), - ], - ); - }, - ), + return ChangeNotifierProvider.value( + value: _controller, + child: Consumer( + builder: (context, controller, child) { + return BaseListScreen( + searchBar: _buildSearchBar(), + actionBar: _buildActionBar(), + dataTable: _buildDataTable(controller), + pagination: _buildPagination(), + isLoading: controller.isLoading, + error: controller.error, + onRefresh: () => controller.loadRents(refresh: true), + emptyMessage: '등록된 임대 계약이 없습니다', + emptyIcon: Icons.business_center_outlined, + ); + }, ), ); } diff --git a/lib/screens/user/user_list.dart b/lib/screens/user/user_list.dart index 6f9b1ab..3139fee 100644 --- a/lib/screens/user/user_list.dart +++ b/lib/screens/user/user_list.dart @@ -5,7 +5,6 @@ import 'package:superport/models/user_model.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart'; -import 'package:superport/screens/common/widgets/standard_data_table.dart'; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; import 'package:superport/screens/user/controllers/user_list_controller.dart'; @@ -263,89 +262,203 @@ class _UserListState extends State { } Widget _buildDataTable() { - if (_controller.users.isEmpty && !_controller.isLoading) { - return StandardDataTable( - columns: _getColumns(), - rows: const [], - emptyMessage: '등록된 사용자가 없습니다', - emptyIcon: Icons.people_outlined, - ); - } - - return StandardDataTable( - columns: _getColumns(), - rows: _buildRows(), - fixedHeader: true, - maxHeight: 600, + final users = _controller.users; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: users.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '등록된 사용자가 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: users.length, + itemBuilder: (context, index) => _buildTableRow(users[index], index), + ), + ), + ], + ), ); } - List _getColumns() { + List _buildHeaderCells() { return [ - StandardDataColumn(label: 'No.', width: 60), - StandardDataColumn(label: '이름', flex: 1), - StandardDataColumn(label: '이메일', flex: 2), - StandardDataColumn(label: '회사', flex: 1), - StandardDataColumn(label: '권한', width: 80), - StandardDataColumn(label: '상태', width: 80), - StandardDataColumn(label: '작업', width: 120), + _buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50), + _buildHeaderCell('이름', flex: 2, useExpanded: true, minWidth: 80), + _buildHeaderCell('이메일', flex: 3, useExpanded: true, minWidth: 120), + _buildHeaderCell('회사', flex: 2, useExpanded: true, minWidth: 80), + _buildHeaderCell('권한', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 120), ]; } - List _buildRows() { - return _controller.users.asMap().entries.map((entry) { - final index = entry.key; - final user = entry.value; - final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1; + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), + ); - return StandardDataRow( - index: index, - cells: [ - Text( - rowNumber.toString(), - style: ShadcnTheme.bodyMedium, - ), - Text( - user.name, - style: ShadcnTheme.bodyMedium.copyWith( - fontWeight: FontWeight.w500, + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + Widget _buildTableRow(User user, int index) { + final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), + ), + ), + child: Row( + children: [ + _buildDataCell( + Text( + rowNumber.toString(), + style: ShadcnTheme.bodySmall, ), + flex: 0, + useExpanded: false, + minWidth: 50, ), - Text( - user.email ?? '', - style: ShadcnTheme.bodyMedium, - ), - Text( - '-', // Company name not available in current model - style: ShadcnTheme.bodySmall, - ), - _buildUserRoleBadge(user.role), - _buildStatusChip(user.isActive), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ShadButton.ghost( - onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null, - child: const Icon(Icons.edit, size: 16), + _buildDataCell( + Text( + user.name, + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.w500, ), - const SizedBox(width: ShadcnTheme.spacing1), - ShadButton.ghost( - onPressed: () => _showStatusChangeDialog(user), - child: Icon( - user.isActive ? Icons.person_off : Icons.person, - size: 16, + ), + flex: 2, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + Text( + user.email ?? '', + style: ShadcnTheme.bodyMedium, + ), + flex: 3, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + Text( + '-', // Company name not available in current model + style: ShadcnTheme.bodySmall, + ), + flex: 2, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + _buildUserRoleBadge(user.role), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + _buildStatusChip(user.isActive), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null, + child: const Icon(Icons.edit, size: 16), ), - ), - const SizedBox(width: ShadcnTheme.spacing1), - ShadButton.ghost( - onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null, - child: const Icon(Icons.delete, size: 16), - ), - ], + const SizedBox(width: ShadcnTheme.spacing1), + ShadButton.ghost( + onPressed: () => _showStatusChangeDialog(user), + child: Icon( + user.isActive ? Icons.person_off : Icons.person, + size: 16, + ), + ), + const SizedBox(width: ShadcnTheme.spacing1), + ShadButton.ghost( + onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null, + child: const Icon(Icons.delete, size: 16), + ), + ], + ), + flex: 0, + useExpanded: false, + minWidth: 120, ), ], - ); - }).toList(); + ), + ); } Widget _buildPagination() { @@ -423,64 +536,3 @@ class _UserListState extends State { // StandardDataRow 임시 정의 } -/// 표준 데이터 행 위젯 (임시) -class StandardDataRow extends StatelessWidget { - final int index; - final List cells; - final VoidCallback? onTap; - final bool selected; - - const StandardDataRow({ - super.key, - required this.index, - required this.cells, - this.onTap, - this.selected = false, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - height: 56, - padding: const EdgeInsets.symmetric( - horizontal: ShadcnTheme.spacing4, - vertical: ShadcnTheme.spacing3, - ), - decoration: BoxDecoration( - color: selected - ? ShadcnTheme.primaryLight.withValues(alpha: 0.1) - : (index.isEven - ? ShadcnTheme.muted.withValues(alpha: 0.3) - : null), - border: const Border( - bottom: BorderSide(color: ShadcnTheme.border, width: 1), - ), - ), - child: Row( - children: _buildCellWidgets(), - ), - ), - ); - } - - List _buildCellWidgets() { - return cells.asMap().entries.map((entry) { - final index = entry.key; - final cell = entry.value; - - // 마지막 셀이 아니면 오른쪽에 간격 추가 - if (index < cells.length - 1) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(right: ShadcnTheme.spacing2), - child: cell, - ), - ); - } else { - return cell; - } - }).toList(); - } -} \ No newline at end of file diff --git a/lib/screens/vendor/vendor_list_screen.dart b/lib/screens/vendor/vendor_list_screen.dart index a5c8a45..3e0817c 100644 --- a/lib/screens/vendor/vendor_list_screen.dart +++ b/lib/screens/vendor/vendor_list_screen.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -import 'package:intl/intl.dart'; import 'package:superport/screens/vendor/controllers/vendor_controller.dart'; import 'package:superport/screens/vendor/vendor_form_dialog.dart'; import 'package:superport/screens/vendor/components/vendor_search_filter.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart'; -import 'package:superport/screens/common/widgets/standard_data_table.dart'; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; @@ -224,76 +222,185 @@ class _VendorListScreenState extends State { ); } - Widget _buildDataTable(VendorController controller) { - if (controller.vendors.isEmpty && !controller.isLoading) { - return StandardDataTable( - columns: _getColumns(), - rows: const [], - emptyMessage: '등록된 벤더가 없습니다', - emptyIcon: Icons.business_outlined, - ); - } - - return StandardDataTable( - columns: _getColumns(), - rows: _buildRows(controller), - fixedHeader: true, - maxHeight: 600, + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } } - List _getColumns() { + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { return [ - StandardDataColumn(label: 'No.', width: 60), - StandardDataColumn(label: '벤더명', flex: 2), - StandardDataColumn(label: '등록일', flex: 1), - StandardDataColumn(label: '상태', width: 80), - StandardDataColumn(label: '작업', width: 100), + _buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50), + _buildHeaderCell('벤더명', flex: 3, useExpanded: true, minWidth: 120), + _buildHeaderCell('등록일', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80), + _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100), ]; } - List _buildRows(VendorController controller) { - return controller.vendors.map((vendor) { - final index = controller.vendors.indexOf(vendor); - final rowNumber = (controller.currentPage - 1) * 10 + index + 1; - - return StandardDataRow( - index: index, - cells: [ - Text( - rowNumber.toString(), - style: ShadcnTheme.bodyMedium, - ), - Text( - vendor.name, - style: ShadcnTheme.bodyMedium.copyWith( - fontWeight: FontWeight.w500, + /// 테이블 행 빌더 + Widget _buildTableRow(dynamic vendor, int index) { + final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), + ), + ), + child: Row( + children: [ + _buildDataCell( + Text( + rowNumber.toString(), + style: ShadcnTheme.bodySmall, ), + flex: 0, + useExpanded: false, + minWidth: 50, ), - Text( - vendor.createdAt != null - ? DateFormat('yyyy-MM-dd').format(vendor.createdAt!) - : '-', - style: ShadcnTheme.bodySmall, + _buildDataCell( + Text( + vendor.name, + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.w500, + ), + ), + flex: 3, + useExpanded: true, + minWidth: 120, ), - _buildStatusChip(vendor.isDeleted), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ShadButton.ghost( - onPressed: () => _showEditDialog(vendor.id!), - child: const Icon(Icons.edit, size: 16), - ), - const SizedBox(width: ShadcnTheme.spacing1), - ShadButton.ghost( - onPressed: () => _showDeleteConfirmDialog(vendor.id!, vendor.name), - child: const Icon(Icons.delete, size: 16), - ), - ], + _buildDataCell( + Text( + vendor.createdAt != null + ? DateFormat('yyyy-MM-dd').format(vendor.createdAt!) + : '-', + style: ShadcnTheme.bodySmall, + ), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + _buildStatusChip(vendor.isDeleted), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + onPressed: () => _showEditDialog(vendor.id!), + child: const Icon(Icons.edit, size: 16), + ), + const SizedBox(width: ShadcnTheme.spacing1), + ShadButton.ghost( + onPressed: () => _showDeleteConfirmDialog(vendor.id!, vendor.name), + child: const Icon(Icons.delete, size: 16), + ), + ], + ), + flex: 0, + useExpanded: false, + minWidth: 100, ), ], - ); - }).toList(); + ), + ); + } + + Widget _buildDataTable(VendorController controller) { + final vendors = controller.vendors; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), + ), + child: Column( + children: [ + // 고정 헤더 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: ShadcnTheme.muted.withValues(alpha: 0.3), + border: Border(bottom: BorderSide(color: Colors.black)), + ), + child: Row(children: _buildHeaderCells()), + ), + // 스크롤 바디 + Expanded( + child: vendors.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.business_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, + ), + const SizedBox(height: 16), + Text( + '등록된 벤더가 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: vendors.length, + itemBuilder: (context, index) => _buildTableRow(vendors[index], index), + ), + ), + ], + ), + ); } Widget _buildPagination(VendorController controller) { @@ -371,64 +478,3 @@ class _VendorListScreenState extends State { // StandardDataRow 클래스 정의 (임시) } -/// 표준 데이터 행 위젯 (임시 - StandardDataTable에 포함될 예정) -class StandardDataRow extends StatelessWidget { - final int index; - final List cells; - final VoidCallback? onTap; - final bool selected; - - const StandardDataRow({ - super.key, - required this.index, - required this.cells, - this.onTap, - this.selected = false, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - height: 56, - padding: const EdgeInsets.symmetric( - horizontal: ShadcnTheme.spacing4, - vertical: ShadcnTheme.spacing3, - ), - decoration: BoxDecoration( - color: selected - ? ShadcnTheme.primaryLight.withValues(alpha: 0.1) - : (index.isEven - ? ShadcnTheme.muted.withValues(alpha: 0.3) - : null), - border: const Border( - bottom: BorderSide(color: ShadcnTheme.border, width: 1), - ), - ), - child: Row( - children: _buildCellWidgets(), - ), - ), - ); - } - - List _buildCellWidgets() { - return cells.asMap().entries.map((entry) { - final index = entry.key; - final cell = entry.value; - - // 마지막 셀이 아니면 오른쪽에 간격 추가 - if (index < cells.length - 1) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(right: ShadcnTheme.spacing2), - child: cell, - ), - ); - } else { - return cell; - } - }).toList(); - } -} \ No newline at end of file diff --git a/lib/screens/warehouse_location/warehouse_location_list.dart b/lib/screens/warehouse_location/warehouse_location_list.dart index f2cfc94..ce62791 100644 --- a/lib/screens/warehouse_location/warehouse_location_list.dart +++ b/lib/screens/warehouse_location/warehouse_location_list.dart @@ -278,26 +278,196 @@ class _WarehouseLocationListState ); } - /// 데이터 테이블 - Widget _buildDataTable(List pagedLocations) { - // 전체 데이터가 없는지 확인 (API의 total 사용) - if (_controller.total == 0 && pagedLocations.isEmpty) { - return StandardEmptyState( - title: - _controller.searchQuery.isNotEmpty - ? '검색 결과가 없습니다' - : '등록된 입고지가 없습니다', - icon: Icons.warehouse_outlined, - action: - _controller.searchQuery.isEmpty - ? StandardActionButtons.addButton( - text: '첫 창고 추가하기', - onPressed: _navigateToAdd, - ) - : null, + /// 헤더 셀 빌더 + Widget _buildHeaderCell( + String text, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final child = Container( + alignment: Alignment.centerLeft, + child: Text( + text, + style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), + ), + ); + + if (useExpanded) { + return Expanded(flex: flex, child: child); + } else { + return SizedBox(width: minWidth, child: child); + } + } + + /// 데이터 셀 빌더 + Widget _buildDataCell( + Widget child, { + required int flex, + required bool useExpanded, + required double minWidth, + }) { + final container = Container( + alignment: Alignment.centerLeft, + child: child, + ); + + if (useExpanded) { + return Expanded(flex: flex, child: container); + } else { + return SizedBox(width: minWidth, child: container); + } + } + + /// 헤더 셀 리스트 + List _buildHeaderCells() { + return [ + _buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50), + _buildHeaderCell('창고명', flex: 2, useExpanded: true, minWidth: 80), + _buildHeaderCell('주소', flex: 3, useExpanded: true, minWidth: 120), + _buildHeaderCell('담당자', flex: 2, useExpanded: true, minWidth: 80), + _buildHeaderCell('연락처', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('수용량', flex: 1, useExpanded: true, minWidth: 70), + _buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 70), + _buildHeaderCell('생성일', flex: 2, useExpanded: true, minWidth: 100), + _buildHeaderCell('관리', flex: 0, useExpanded: false, minWidth: 80), + ]; + } + + /// 테이블 행 빌더 + Widget _buildTableRow(WarehouseLocation location, int index) { + final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: index.isEven + ? ShadcnTheme.muted.withValues(alpha: 0.1) + : null, + border: const Border( + bottom: BorderSide(color: Colors.black), + ), + ), + child: Row( + children: [ + _buildDataCell( + Text( + rowNumber.toString(), + style: ShadcnTheme.bodySmall, + ), + flex: 0, + useExpanded: false, + minWidth: 50, + ), + _buildDataCell( + Text( + location.name, + style: ShadcnTheme.bodyMedium.copyWith( + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + Text( + location.address ?? '-', + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + flex: 3, + useExpanded: true, + minWidth: 120, + ), + _buildDataCell( + Text( + location.managerName ?? '-', + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 80, + ), + _buildDataCell( + Text( + location.managerPhone ?? '-', + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Text( + location.capacity?.toString() ?? '-', + style: ShadcnTheme.bodySmall, + textAlign: TextAlign.center, + ), + flex: 1, + useExpanded: true, + minWidth: 70, + ), + _buildDataCell( + _buildStatusChip(location.isActive), + flex: 0, + useExpanded: false, + minWidth: 70, + ), + _buildDataCell( + Text( + _formatDate(location.createdAt), + style: ShadcnTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + flex: 2, + useExpanded: true, + minWidth: 100, + ), + _buildDataCell( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _navigateToEdit(location), + child: const Icon(Icons.edit, size: 16), + ), + const SizedBox(width: 4), + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => _showDeleteDialog(location.id), + child: const Icon(Icons.delete, size: 16), + ), + ], + ), + flex: 0, + useExpanded: false, + minWidth: 80, + ), + ], + ), + ); + } + + /// 상태 배지 빌더 (shadcn_ui 일관성) + Widget _buildStatusChip(bool isActive) { + if (isActive) { + return ShadBadge.secondary( + child: const Text('활성'), + ); + } else { + return ShadBadge.destructive( + child: const Text('비활성'), ); } + } + /// 데이터 테이블 + Widget _buildDataTable(List pagedLocations) { return Container( width: double.infinity, decoration: BoxDecoration( @@ -305,216 +475,45 @@ class _WarehouseLocationListState borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 테이블 헤더 + // 고정 헤더 Container( - padding: const EdgeInsets.symmetric( - horizontal: ShadcnTheme.spacing4, - vertical: 10, - ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.3), - border: Border( - bottom: BorderSide(color: Colors.black), - ), - ), - child: Row( - children: [ - Expanded( - flex: 1, - child: Text('번호', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 2, - child: Text('창고명', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 3, - child: Text('주소', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 2, - child: Text('담당자', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 2, - child: Text('연락처', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 1, - child: Text('수용량', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 1, - child: Text('상태', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 2, - child: Text('생성일', style: ShadcnTheme.bodyMedium), - ), - Expanded( - flex: 1, - child: Text('관리', style: ShadcnTheme.bodyMedium), - ), - ], + border: Border(bottom: BorderSide(color: Colors.black)), ), + child: Row(children: _buildHeaderCells()), ), - - // 테이블 데이터 - ...pagedLocations.asMap().entries.map((entry) { - final int index = entry.key; - final WarehouseLocation location = entry.value; - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: ShadcnTheme.spacing4, - vertical: 4, - ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.black), - ), - ), - child: Row( - children: [ - // 번호 - Expanded( - flex: 1, - child: Text( - '${(_controller.currentPage - 1) * _controller.pageSize + index + 1}', - style: ShadcnTheme.bodySmall, - ), - ), - // 창고명 - Expanded( - flex: 2, - child: Text( - location.name, - style: ShadcnTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - ), - ), - // 주소 - Expanded( - flex: 3, - child: Text( - location.address ?? '-', - style: ShadcnTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - // 담당자 - Expanded( - flex: 2, - child: Text( - location.managerName ?? '-', - style: ShadcnTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - // 연락처 - Expanded( - flex: 2, - child: Text( - location.managerPhone ?? '-', - style: ShadcnTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - // 수용량 - Expanded( - flex: 1, - child: Text( - location.capacity?.toString() ?? '-', - style: ShadcnTheme.bodySmall, - textAlign: TextAlign.center, - ), - ), - // 상태 - Expanded( - flex: 1, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: location.isActive - ? ShadcnTheme.success.withValues(alpha: 0.1) - : ShadcnTheme.muted.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: location.isActive - ? ShadcnTheme.success.withValues(alpha: 0.3) - : ShadcnTheme.muted.withValues(alpha: 0.3) - ), - ), - child: Text( - location.isActive ? '활성' : '비활성', - style: TextStyle( - fontSize: 10, - color: location.isActive - ? ShadcnTheme.success - : ShadcnTheme.muted, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ), - ), - // 생성일 - Expanded( - flex: 2, - child: Text( - _formatDate(location.createdAt), - style: ShadcnTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - // 관리 - Expanded( - flex: 1, - child: Row( - mainAxisSize: MainAxisSize.min, + // 스크롤 바디 + Expanded( + child: pagedLocations.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible( - child: IconButton( - constraints: const BoxConstraints( - minWidth: 30, - minHeight: 30, - ), - padding: const EdgeInsets.all(4), - icon: Icon( - Icons.edit, - size: 16, - color: ShadcnTheme.primary, - ), - onPressed: () => _navigateToEdit(location), - tooltip: '수정', - ), + Icon( + Icons.warehouse_outlined, + size: 64, + color: ShadcnTheme.mutedForeground, ), - Flexible( - child: IconButton( - constraints: const BoxConstraints( - minWidth: 30, - minHeight: 30, - ), - padding: const EdgeInsets.all(4), - icon: Icon( - Icons.delete, - size: 16, - color: ShadcnTheme.destructive, - ), - onPressed: () => - _showDeleteDialog(location.id), - tooltip: '삭제', + const SizedBox(height: 16), + Text( + _controller.searchQuery.isNotEmpty + ? '검색 결과가 없습니다' + : '등록된 창고가 없습니다', + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.mutedForeground, ), ), ], ), + ) + : ListView.builder( + itemCount: pagedLocations.length, + itemBuilder: (context, index) => _buildTableRow(pagedLocations[index], index), ), - ], - ), - ); - }), + ), ], ), );