feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

- 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:
JiWoong Sul
2025-09-05 14:33:20 +09:00
parent 2c20999025
commit 519e1883a3
46 changed files with 7804 additions and 1034 deletions

View File

@@ -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); // 선택 해제
}
});
}