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

@@ -0,0 +1,537 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport/data/models/inventory_history_view_model.dart';
import 'package:superport/screens/inventory/controllers/inventory_history_controller.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
/// 장비 이력 상세보기 다이얼로그
/// 특정 장비의 전체 히스토리를 시간순으로 표시
class EquipmentHistoryDetailDialog extends StatefulWidget {
final int equipmentId;
final String equipmentName;
final String serialNumber;
final InventoryHistoryController controller;
const EquipmentHistoryDetailDialog({
super.key,
required this.equipmentId,
required this.equipmentName,
required this.serialNumber,
required this.controller,
});
@override
State<EquipmentHistoryDetailDialog> createState() =>
_EquipmentHistoryDetailDialogState();
}
class _EquipmentHistoryDetailDialogState
extends State<EquipmentHistoryDetailDialog> {
List<InventoryHistoryViewModel>? _historyList;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadEquipmentHistory();
}
/// 장비별 이력 로드
Future<void> _loadEquipmentHistory() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final histories = await widget.controller.loadEquipmentHistory(widget.equipmentId);
setState(() {
_historyList = histories;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
print('[EquipmentHistoryDetailDialog] Error loading equipment history: $e');
}
}
/// 거래 유형 아이콘 반환
IconData _getTransactionIcon(String transactionType) {
switch (transactionType) {
case 'I':
return Icons.arrow_downward; // 입고
case 'O':
return Icons.arrow_upward; // 출고
case 'R':
return Icons.share; // 대여
case 'D':
return Icons.delete_outline; // 폐기
default:
return Icons.help_outline;
}
}
/// 거래 유형 색상 반환
Color _getTransactionColor(String transactionType) {
switch (transactionType) {
case 'I':
return Colors.green; // 입고
case 'O':
return Colors.orange; // 출고
case 'R':
return Colors.blue; // 대여
case 'D':
return Colors.red; // 폐기
default:
return Colors.grey;
}
}
/// 타임라인 아이템 빌더
Widget _buildTimelineItem(InventoryHistoryViewModel history, int index) {
final isFirst = index == 0;
final isLast = index == (_historyList?.length ?? 0) - 1;
final color = _getTransactionColor(history.transactionType);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 타임라인 인디케이터
SizedBox(
width: 60,
child: Column(
children: [
// 위쪽 연결선
if (!isFirst)
Container(
width: 2,
height: 20,
color: Colors.grey.withValues(alpha: 0.3),
),
// 원형 인디케이터
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
border: Border.all(
color: Colors.white,
width: 2,
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(
_getTransactionIcon(history.transactionType),
color: Colors.white,
size: 16,
),
),
// 아래쪽 연결선
if (!isLast)
Container(
width: 2,
height: 20,
color: Colors.grey.withValues(alpha: 0.3),
),
],
),
),
const SizedBox(width: 16),
// 이력 정보
Expanded(
child: Container(
margin: EdgeInsets.only(bottom: isLast ? 0 : 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: color.withValues(alpha: 0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 헤더 (거래 유형 + 날짜)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ShadcnBadge(
text: history.transactionTypeDisplay,
variant: _getBadgeVariant(history.transactionType),
size: ShadcnBadgeSize.small,
),
const SizedBox(width: 8),
if (isFirst)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.orange.withValues(alpha: 0.3),
),
),
child: const Text(
'최근',
style: TextStyle(
fontSize: 10,
color: Colors.orange,
fontWeight: FontWeight.w500,
),
),
),
],
),
Text(
history.formattedDate,
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
// 위치 정보
Row(
children: [
Icon(
history.isCustomerLocation ? Icons.business : Icons.warehouse,
size: 16,
color: history.isCustomerLocation ? Colors.blue : Colors.green,
),
const SizedBox(width: 6),
Text(
'위치: ',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
Expanded(
child: Text(
history.location,
style: ShadcnTheme.bodySmall.copyWith(
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
// 수량 정보
if (history.quantity > 0) ...[
const SizedBox(height: 6),
Row(
children: [
Icon(
Icons.inventory,
size: 16,
color: ShadcnTheme.mutedForeground,
),
const SizedBox(width: 6),
Text(
'수량: ',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
Text(
'${history.quantity}',
style: ShadcnTheme.bodySmall.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
],
// 비고
if (history.remark != null && history.remark!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.note,
size: 14,
color: ShadcnTheme.mutedForeground,
),
const SizedBox(width: 6),
Expanded(
child: Text(
history.remark!,
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
],
],
),
),
),
],
);
}
/// Badge Variant 반환
ShadcnBadgeVariant _getBadgeVariant(String transactionType) {
switch (transactionType) {
case 'I':
return ShadcnBadgeVariant.success; // 입고
case 'O':
return ShadcnBadgeVariant.warning; // 출고
case 'R':
return ShadcnBadgeVariant.info; // 대여
case 'D':
return ShadcnBadgeVariant.destructive; // 폐기
default:
return ShadcnBadgeVariant.secondary;
}
}
@override
Widget build(BuildContext context) {
return ShadDialog(
title: Row(
children: [
Icon(
Icons.history,
color: ShadcnTheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text('장비 이력 상세'),
],
),
description: SingleChildScrollView(
child: SizedBox(
width: 600,
height: 500,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 장비 정보 헤더
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ShadcnTheme.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.precision_manufacturing,
color: ShadcnTheme.primary,
size: 18,
),
const SizedBox(width: 8),
Text(
widget.equipmentName,
style: ShadcnTheme.bodyLarge.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.qr_code,
color: ShadcnTheme.mutedForeground,
size: 16,
),
const SizedBox(width: 6),
Text(
'시리얼: ${widget.serialNumber}',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
],
),
],
),
),
const SizedBox(height: 20),
// 이력 목록 헤더
Row(
children: [
Icon(
Icons.timeline,
color: ShadcnTheme.mutedForeground,
size: 16,
),
const SizedBox(width: 6),
Text(
'변동 이력 (시간순)',
style: ShadcnTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w500,
color: ShadcnTheme.mutedForeground,
),
),
if (_historyList != null) ...[
const SizedBox(width: 8),
ShadcnBadge(
text: '${_historyList!.length}',
variant: ShadcnBadgeVariant.secondary,
size: ShadcnBadgeSize.small,
),
],
],
),
const SizedBox(height: 12),
// 이력 목록
Expanded(
child: _buildHistoryContent(),
),
],
),
),
),
actions: [
ShadcnButton(
text: '새로고침',
onPressed: _loadEquipmentHistory,
variant: ShadcnButtonVariant.secondary,
icon: const Icon(Icons.refresh, size: 16),
),
ShadcnButton(
text: '닫기',
onPressed: () => Navigator.of(context).pop(),
variant: ShadcnButtonVariant.primary,
),
],
);
}
/// 이력 컨텐츠 빌더
Widget _buildHistoryContent() {
if (_isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 12),
Text('이력을 불러오는 중...'),
],
),
);
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Colors.red.withValues(alpha: 0.6),
),
const SizedBox(height: 12),
Text(
'이력을 불러올 수 없습니다',
style: ShadcnTheme.bodyMedium.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
_error!,
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.mutedForeground,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
ShadcnButton(
text: '다시 시도',
onPressed: _loadEquipmentHistory,
variant: ShadcnButtonVariant.secondary,
icon: const Icon(Icons.refresh, size: 16),
),
],
),
);
}
if (_historyList == null || _historyList!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 48,
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.6),
),
const SizedBox(height: 12),
Text(
'등록된 이력이 없습니다',
style: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _historyList!.length,
itemBuilder: (context, index) {
return _buildTimelineItem(_historyList![index], index);
},
);
}
}