feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
- V/R 시스템 완전 전환: WARRANTY/CONTRACT/INSPECTION → V(방문)/R(원격) - 유지보수 대시보드 카드 → StandardDataTable 테이블 형태 전환 - "조회중..." 문제 해결: 백엔드 직접 필드 사용 (equipment_model, company_name) - MaintenanceDto 신규 필드 추가: company_id, company_name, equipment_serial, equipment_model - preloadEquipmentData 비활성화로 불필요한 equipment-history API 호출 제거 - CO-STAR 프레임워크 적용 및 CLAUDE.md v3.0 업데이트 - Flutter Analyze ERROR: 0 유지, 100% shadcn_ui 컴플라이언스 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,13 @@ import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_search_dialog.dart';
|
||||
import 'package:superport/screens/equipment/dialogs/equipment_outbound_dialog.dart';
|
||||
import 'package:superport/data/models/equipment/equipment_dto.dart';
|
||||
import 'package:superport/domain/usecases/equipment/get_equipment_detail_usecase.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/data/repositories/equipment_history_repository.dart';
|
||||
import 'package:superport/data/models/stock_status_dto.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
||||
class EquipmentList extends StatefulWidget {
|
||||
@@ -92,15 +99,15 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
switch (widget.currentRoute) {
|
||||
case Routes.equipmentInList:
|
||||
_selectedStatus = 'in';
|
||||
_controller.selectedStatusFilter = EquipmentStatus.in_;
|
||||
_controller.selectedStatusFilter = 'I'; // 영문 코드 사용
|
||||
break;
|
||||
case Routes.equipmentOutList:
|
||||
_selectedStatus = 'out';
|
||||
_controller.selectedStatusFilter = EquipmentStatus.out;
|
||||
_controller.selectedStatusFilter = 'O'; // 영문 코드 사용
|
||||
break;
|
||||
case Routes.equipmentRentList:
|
||||
_selectedStatus = 'rent';
|
||||
_controller.selectedStatusFilter = EquipmentStatus.rent;
|
||||
_controller.selectedStatusFilter = 'T'; // 영문 코드 사용
|
||||
break;
|
||||
default:
|
||||
_selectedStatus = 'all';
|
||||
@@ -114,31 +121,31 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
Future<void> _onStatusFilterChanged(String status) async {
|
||||
setState(() {
|
||||
_selectedStatus = status;
|
||||
// 상태 필터를 EquipmentStatus 상수로 변환
|
||||
// 상태 필터를 영문 코드로 변환
|
||||
switch (status) {
|
||||
case 'all':
|
||||
_controller.selectedStatusFilter = null;
|
||||
break;
|
||||
case 'in':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.in_;
|
||||
_controller.selectedStatusFilter = 'I';
|
||||
break;
|
||||
case 'out':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.out;
|
||||
_controller.selectedStatusFilter = 'O';
|
||||
break;
|
||||
case 'rent':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.rent;
|
||||
_controller.selectedStatusFilter = 'T';
|
||||
break;
|
||||
case 'repair':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.repair;
|
||||
_controller.selectedStatusFilter = 'R';
|
||||
break;
|
||||
case 'damaged':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.damaged;
|
||||
_controller.selectedStatusFilter = 'D';
|
||||
break;
|
||||
case 'lost':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.lost;
|
||||
_controller.selectedStatusFilter = 'L';
|
||||
break;
|
||||
case 'disposed':
|
||||
_controller.selectedStatusFilter = EquipmentStatus.disposed;
|
||||
_controller.selectedStatusFilter = 'P';
|
||||
break;
|
||||
default:
|
||||
_controller.selectedStatusFilter = null;
|
||||
@@ -170,8 +177,17 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
void _onSelectAll(bool? value) {
|
||||
setState(() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
for (final equipment in equipments) {
|
||||
_controller.selectEquipment(equipment);
|
||||
_selectedItems.clear(); // UI 체크박스 상태 초기화
|
||||
|
||||
if (value == true) {
|
||||
for (final equipment in equipments) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_selectedItems.add(equipment.equipment.id!);
|
||||
_controller.selectEquipment(equipment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_controller.clearSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -181,7 +197,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final equipments = _getFilteredEquipments();
|
||||
if (equipments.isEmpty) return false;
|
||||
return equipments.every((e) =>
|
||||
_controller.selectedEquipmentIds.contains('${e.id}:${e.status}'));
|
||||
_controller.selectedEquipmentIds.contains('${e.equipment.id}:${e.status}'));
|
||||
}
|
||||
|
||||
|
||||
@@ -221,20 +237,103 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 가져와서 출고 폼으로 전달
|
||||
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
||||
// ✅ 장비 수정과 동일한 방식: GetEquipmentDetailUseCase를 사용해서 완전한 데이터 로드
|
||||
final selectedEquipmentIds = _controller.getSelectedEquipments()
|
||||
.where((e) => e.status == 'I') // 영문 코드로 통일
|
||||
.map((e) => e.equipment.id)
|
||||
.where((id) => id != null)
|
||||
.cast<int>()
|
||||
.toList();
|
||||
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.equipmentOutAdd,
|
||||
arguments: {'selectedEquipments': selectedEquipmentsSummary},
|
||||
print('[EquipmentList] Loading complete equipment details for ${selectedEquipmentIds.length} equipments using GetEquipmentDetailUseCase');
|
||||
|
||||
// ✅ stock-status API를 사용해서 실제 현재 창고 정보가 포함된 데이터 로드
|
||||
final selectedEquipments = <EquipmentDto>[];
|
||||
final equipmentHistoryRepository = EquipmentHistoryRepositoryImpl(GetIt.instance<ApiClient>().dio);
|
||||
|
||||
// stock-status API를 시도하되, 실패해도 출고 프로세스 계속 진행
|
||||
Map<int, StockStatusDto> stockStatusMap = {};
|
||||
try {
|
||||
// 1. 모든 재고 상태 정보를 한 번에 로드 (실패해도 계속 진행)
|
||||
print('[EquipmentList] Attempting to load stock status...');
|
||||
final stockStatusList = await equipmentHistoryRepository.getStockStatus();
|
||||
for (final status in stockStatusList) {
|
||||
stockStatusMap[status.equipmentId] = status;
|
||||
}
|
||||
print('[EquipmentList] Stock status loaded successfully: ${stockStatusMap.length} items');
|
||||
} catch (e) {
|
||||
print('[EquipmentList] ⚠️ Stock status API failed, continuing with basic equipment data: $e');
|
||||
// 경고 메시지만 표시하고 계속 진행
|
||||
ShadToaster.of(context).show(ShadToast(
|
||||
title: const Text('알림'),
|
||||
description: const Text('실시간 창고 정보를 가져올 수 없어 기본 정보로 진행합니다.'),
|
||||
));
|
||||
}
|
||||
|
||||
// 2. 각 장비의 상세 정보를 로드하고 가능하면 창고 정보를 매핑
|
||||
final getEquipmentDetailUseCase = GetIt.instance<GetEquipmentDetailUseCase>();
|
||||
|
||||
for (final equipmentId in selectedEquipmentIds) {
|
||||
print('[EquipmentList] Loading details for equipment $equipmentId');
|
||||
final result = await getEquipmentDetailUseCase(equipmentId);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
print('[EquipmentList] Failed to load equipment $equipmentId: ${failure.message}');
|
||||
ShadToaster.of(context).show(ShadToast(
|
||||
title: const Text('오류'),
|
||||
description: Text('장비 정보를 불러오는데 실패했습니다: ${failure.message}'),
|
||||
));
|
||||
return; // 실패 시 종료
|
||||
},
|
||||
(equipment) {
|
||||
// ✅ stock-status가 있으면 실제 창고 정보로 업데이트, 없으면 기존 정보 사용
|
||||
final stockStatus = stockStatusMap[equipmentId];
|
||||
EquipmentDto updatedEquipment = equipment;
|
||||
|
||||
if (stockStatus != null) {
|
||||
updatedEquipment = equipment.copyWith(
|
||||
warehousesId: stockStatus.warehouseId,
|
||||
warehousesName: stockStatus.warehouseName,
|
||||
);
|
||||
print('[EquipmentList] ===== REAL WAREHOUSE DATA =====');
|
||||
print('[EquipmentList] Equipment ID: $equipmentId');
|
||||
print('[EquipmentList] Serial Number: ${equipment.serialNumber}');
|
||||
print('[EquipmentList] REAL Warehouse ID: ${stockStatus.warehouseId}');
|
||||
print('[EquipmentList] REAL Warehouse Name: ${stockStatus.warehouseName}');
|
||||
print('[EquipmentList] =====================================');
|
||||
} else {
|
||||
print('[EquipmentList] ⚠️ No stock status found for equipment $equipmentId, using basic warehouse info');
|
||||
print('[EquipmentList] Basic Warehouse ID: ${equipment.warehousesId}');
|
||||
print('[EquipmentList] Basic Warehouse Name: ${equipment.warehousesName}');
|
||||
}
|
||||
|
||||
selectedEquipments.add(updatedEquipment);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 모든 장비 정보를 성공적으로 로드했는지 확인
|
||||
if (selectedEquipments.length != selectedEquipmentIds.length) {
|
||||
print('[EquipmentList] Failed to load complete equipment information');
|
||||
return; // 일부 장비 정보 로드 실패 시 중단
|
||||
}
|
||||
|
||||
// 출고 다이얼로그 표시
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return EquipmentOutboundDialog(
|
||||
selectedEquipments: selectedEquipments,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData(isRefresh: true);
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
// 선택 상태 초기화 및 데이터 새로고침
|
||||
_controller.clearSelection();
|
||||
_controller.loadData(isRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +361,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
/// 폐기 처리 버튼 핸들러
|
||||
void _handleDisposeEquipment() async {
|
||||
final selectedEquipments = _controller.getSelectedEquipments()
|
||||
.where((equipment) => equipment.status != EquipmentStatus.disposed)
|
||||
.where((equipment) => equipment.status != 'P') // 영문 코드로 통일
|
||||
.toList();
|
||||
|
||||
if (selectedEquipments.isEmpty) {
|
||||
@@ -865,7 +964,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
totalWidth += 80; // 모델명 (100->80)
|
||||
totalWidth += 70; // 장비번호 (90->70)
|
||||
totalWidth += 50; // 상태 (60->50)
|
||||
totalWidth += 90; // 관리 (120->90, 아이콘 전용으로 최적화)
|
||||
totalWidth += 100; // 관리 (120->90->100, 아이콘 3개 수용)
|
||||
|
||||
// 중간 화면용 추가 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) {
|
||||
@@ -972,7 +1071,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 50),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90),
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 100),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
@@ -1119,7 +1218,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
@@ -1128,7 +1227,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
@@ -1141,7 +1240,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 90,
|
||||
minWidth: 100,
|
||||
),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
@@ -1332,7 +1431,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
Widget _buildInventoryStatus(UnifiedEquipment equipment) {
|
||||
// 백엔드 Equipment_History 기반으로 단순 상태만 표시
|
||||
Widget stockInfo;
|
||||
if (equipment.status == EquipmentStatus.in_) {
|
||||
if (equipment.status == 'I') {
|
||||
// 입고 상태: 재고 있음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1345,7 +1444,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == EquipmentStatus.out) {
|
||||
} else if (equipment.status == 'O') {
|
||||
// 출고 상태: 재고 없음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1358,7 +1457,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == EquipmentStatus.rent) {
|
||||
} else if (equipment.status == 'T') {
|
||||
// 대여 상태
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1387,19 +1486,36 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
String displayText;
|
||||
ShadcnBadgeVariant variant;
|
||||
|
||||
// 영문 코드만 사용 (EquipmentStatus 상수들도 실제로는 'I', 'O' 등의 값)
|
||||
switch (status) {
|
||||
case EquipmentStatus.in_:
|
||||
case 'I':
|
||||
displayText = '입고';
|
||||
variant = ShadcnBadgeVariant.success;
|
||||
break;
|
||||
case EquipmentStatus.out:
|
||||
case 'O':
|
||||
displayText = '출고';
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
break;
|
||||
case EquipmentStatus.rent:
|
||||
case 'T':
|
||||
displayText = '대여';
|
||||
variant = ShadcnBadgeVariant.warning;
|
||||
break;
|
||||
case 'R':
|
||||
displayText = '수리';
|
||||
variant = ShadcnBadgeVariant.secondary;
|
||||
break;
|
||||
case 'D':
|
||||
displayText = '손상';
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
break;
|
||||
case 'L':
|
||||
displayText = '분실';
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
break;
|
||||
case 'P':
|
||||
displayText = '폐기';
|
||||
variant = ShadcnBadgeVariant.secondary;
|
||||
break;
|
||||
default:
|
||||
displayText = '알수없음';
|
||||
variant = ShadcnBadgeVariant.secondary;
|
||||
@@ -1501,11 +1617,19 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
/// 체크박스 선택 관련 함수들
|
||||
void _onItemSelected(int id, bool selected) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == id,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedItems.add(id);
|
||||
_controller.selectEquipment(equipment); // Controller에도 전달
|
||||
} else {
|
||||
_selectedItems.remove(id);
|
||||
_controller.toggleSelection(equipment); // 선택 해제
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user