web: migrate health notifications to js_interop; add browser hook
- Replace dart:js with package:js in health_check_service_web.dart\n- Implement showHealthCheckNotification in web/index.html\n- Pin js dependency to ^0.6.7 for flutter_secure_storage_web compatibility auth: harden AuthInterceptor + tests - Allow overrideAuthRepository injection for testing\n- Normalize imports to package: paths\n- Add unit test covering token attach, 401→refresh→retry, and failure path\n- Add integration test skeleton gated by env vars ui/data: map User.companyName to list column - Add companyName to domain User\n- Map UserDto.company?.name\n- Render companyName in user_list cleanup: remove legacy equipment table + unused code; minor warnings - Remove _buildFlexibleTable and unused helpers\n- Remove unused zipcode details and cache retry constant\n- Fix null-aware and non-null assertions\n- Address child-last warnings in administrator dialog docs: update AGENTS.md session context
This commit is contained in:
@@ -233,13 +233,13 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '장비 번호 * 🔒' : '장비 번호 *'),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
if ((value ?? '').trim().isEmpty) {
|
||||
return '장비 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value?.trim() ?? '';
|
||||
_controller.serialNumber = value.trim();
|
||||
setState(() {});
|
||||
print('DEBUG [장비번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
},
|
||||
@@ -252,7 +252,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
placeholder: const Text('바코드를 입력하세요'),
|
||||
label: const Text('바코드'),
|
||||
onChanged: (value) {
|
||||
_controller.barcode = value?.trim() ?? '';
|
||||
_controller.barcode = value.trim();
|
||||
print('DEBUG [바코드 입력] value: "$value", controller.barcode: "${_controller.barcode}"');
|
||||
},
|
||||
),
|
||||
@@ -504,7 +504,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
label: const Text('워런티 번호 *'),
|
||||
placeholder: const Text('워런티 번호를 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty ?? true) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '워런티 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
@@ -683,4 +683,4 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 드롭다운 데이터를 미리 로드하는 메서드
|
||||
Future<void> _preloadDropdownData() async {
|
||||
try {
|
||||
@@ -94,6 +96,157 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
});
|
||||
}
|
||||
|
||||
/// ShadTable 기반 장비 목록 테이블
|
||||
///
|
||||
/// - 표준 컴포넌트 사용으로 일관성 확보
|
||||
/// - 핵심 컬럼만 우선 도입 (상태/장비번호/시리얼/제조사/모델/회사/창고/일자/관리)
|
||||
/// - 반응형: 가용 너비에 따라 일부 컬럼은 숨김 처리 가능
|
||||
Widget _buildShadTable(List<UnifiedEquipment> items, {required double availableWidth}) {
|
||||
final allSelected = items.isNotEmpty &&
|
||||
items.every((e) => _selectedItems.contains(e.equipment.id));
|
||||
|
||||
return ShadTable.list(
|
||||
header: [
|
||||
// 선택
|
||||
ShadTableCell.header(
|
||||
child: ShadCheckbox(
|
||||
value: allSelected,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
if (checked == true) {
|
||||
_selectedItems
|
||||
..clear()
|
||||
..addAll(items.map((e) => e.equipment.id).whereType<int>());
|
||||
} else {
|
||||
_selectedItems.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ShadTableCell.header(child: const Text('상태')),
|
||||
ShadTableCell.header(child: const Text('장비번호')),
|
||||
ShadTableCell.header(child: const Text('시리얼')),
|
||||
ShadTableCell.header(child: const Text('제조사')),
|
||||
ShadTableCell.header(child: const Text('모델')),
|
||||
if (availableWidth > 900) ShadTableCell.header(child: const Text('회사')),
|
||||
if (availableWidth > 1100) ShadTableCell.header(child: const Text('창고')),
|
||||
if (availableWidth > 800) ShadTableCell.header(child: const Text('일자')),
|
||||
ShadTableCell.header(child: const Text('관리')),
|
||||
],
|
||||
children: items.map((item) {
|
||||
final id = item.equipment.id;
|
||||
final selected = id != null && _selectedItems.contains(id);
|
||||
return [
|
||||
// 선택 체크박스
|
||||
ShadTableCell(
|
||||
child: ShadCheckbox(
|
||||
value: selected,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
if (id == null) return;
|
||||
if (checked == true) {
|
||||
_selectedItems.add(id);
|
||||
} else {
|
||||
_selectedItems.remove(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 상태
|
||||
ShadTableCell(child: _buildStatusBadge(item.status)),
|
||||
// 장비번호
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.equipment.equipmentNumber,
|
||||
item.equipment.equipmentNumber,
|
||||
),
|
||||
),
|
||||
// 시리얼
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.equipment.serialNumber ?? '-',
|
||||
item.equipment.serialNumber ?? '-',
|
||||
),
|
||||
),
|
||||
// 제조사
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.vendorName ?? item.equipment.manufacturer,
|
||||
item.vendorName ?? item.equipment.manufacturer,
|
||||
),
|
||||
),
|
||||
// 모델
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.modelName ?? item.equipment.modelName,
|
||||
item.modelName ?? item.equipment.modelName,
|
||||
),
|
||||
),
|
||||
// 회사 (반응형)
|
||||
if (availableWidth > 900)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.companyName ?? item.currentCompany ?? '-',
|
||||
item.companyName ?? item.currentCompany ?? '-',
|
||||
),
|
||||
),
|
||||
// 창고 (반응형)
|
||||
if (availableWidth > 1100)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.warehouseLocation ?? '-',
|
||||
item.warehouseLocation ?? '-',
|
||||
),
|
||||
),
|
||||
// 일자 (반응형)
|
||||
if (availableWidth > 800)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
_formatDate(item.date),
|
||||
_formatDate(item.date),
|
||||
),
|
||||
),
|
||||
// 관리 액션
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: '이력 보기',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(item.equipment.id ?? 0),
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEdit(item),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDelete(item),
|
||||
child: const Icon(Icons.delete_outline, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 라우트에 따른 초기 필터 설정
|
||||
void _setInitialFilter() {
|
||||
switch (widget.currentRoute) {
|
||||
@@ -173,32 +326,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
|
||||
/// 전체 선택/해제
|
||||
void _onSelectAll(bool? value) {
|
||||
setState(() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
_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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 전체 선택 상태 확인
|
||||
bool _isAllSelected() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
if (equipments.isEmpty) return false;
|
||||
return equipments.every((e) =>
|
||||
_controller.selectedEquipmentIds.contains('${e.equipment.id}:${e.status}'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 필터링된 장비 목록 반환
|
||||
@@ -986,339 +1114,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 13, 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 유연한 테이블 빌더 - Virtual Scrolling 적용
|
||||
Widget _buildFlexibleTable(List<UnifiedEquipment> pagedEquipments, {required bool useExpanded, required double availableWidth}) {
|
||||
final hasOutOrRent = pagedEquipments.any((e) =>
|
||||
e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent
|
||||
);
|
||||
|
||||
// 헤더를 별도로 빌드 - 반응형 컬럼 적용
|
||||
Widget header = Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 6, // 8 -> 6으로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _isAllSelected(),
|
||||
onChanged: (bool? value) => _onSelectAll(value),
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 회사명 (소유회사)
|
||||
_buildHeaderCell('소유회사', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 제조사
|
||||
_buildHeaderCell('제조사', flex: 2, useExpanded: useExpanded, minWidth: 60),
|
||||
// 모델명
|
||||
_buildHeaderCell('모델명', flex: 3, useExpanded: useExpanded, minWidth: 80),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 70),
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 50),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 100),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량
|
||||
_buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
_buildHeaderCell('바코드', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매가격', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('보증기간', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 빈 상태 처리
|
||||
if (pagedEquipments.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
header,
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'데이터가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Virtual Scrolling을 위한 CustomScrollView 사용
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
header, // 헤더는 고정
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: pagedEquipments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final UnifiedEquipment equipment = pagedEquipments[index];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 2, // 3 -> 2로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _selectedItems.contains(equipment.equipment.id ?? 0),
|
||||
onChanged: (bool? value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_onItemSelected(equipment.equipment.id!, value ?? false);
|
||||
}
|
||||
},
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 소유회사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.companyName ?? 'N/A',
|
||||
equipment.companyName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 제조사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.vendorName ?? 'N/A',
|
||||
equipment.vendorName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 60,
|
||||
),
|
||||
// 모델명
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.modelName ?? '-',
|
||||
equipment.modelName ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 장비번호
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 상태
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(equipment.status),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 관리 (아이콘 전용 버튼으로 최적화)
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: '이력 보기',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipment.equipment.id ?? 0),
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEdit(equipment),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDelete(equipment),
|
||||
child: const Icon(Icons.delete_outline, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 100,
|
||||
),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량 (백엔드에서 관리하지 않으므로 고정값)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'1',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 입출고일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.date),
|
||||
_formatDate(equipment.date),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
// 바코드
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.barcode ?? '-',
|
||||
equipment.equipment.barcode ?? '-',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매가격
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 보증기간
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<UnifiedEquipment> filteredEquipments) {
|
||||
@@ -1367,19 +1167,18 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final minimumWidth = _getMinimumTableWidth(pagedEquipments, availableWidth);
|
||||
final needsHorizontalScroll = minimumWidth > availableWidth;
|
||||
|
||||
// ShadTable 경로로 일괄 전환 (가로 스크롤은 ShadTable 외부에서 처리)
|
||||
if (needsHorizontalScroll) {
|
||||
// 최소 너비보다 작을 때만 스크롤 활성화
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: minimumWidth,
|
||||
child: _buildFlexibleTable(pagedEquipments, useExpanded: false, availableWidth: availableWidth),
|
||||
child: _buildShadTable(pagedEquipments, availableWidth: availableWidth),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 충분한 공간이 있을 때는 Expanded 사용
|
||||
return _buildFlexibleTable(pagedEquipments, useExpanded: true, availableWidth: availableWidth);
|
||||
return _buildShadTable(pagedEquipments, availableWidth: availableWidth);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1399,10 +1198,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
/// 가격 포맷팅
|
||||
String _formatPrice(double? price) {
|
||||
if (price == null) return '-';
|
||||
return '${(price / 10000).toStringAsFixed(0)}만원';
|
||||
}
|
||||
|
||||
|
||||
/// 날짜 포맷팅
|
||||
String _formatDate(DateTime? date) {
|
||||
@@ -1411,75 +1207,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
/// 보증기간 포맷팅
|
||||
String _formatWarrantyPeriod(DateTime? startDate, DateTime? endDate) {
|
||||
if (startDate == null || endDate == null) return '-';
|
||||
|
||||
final now = DateTime.now();
|
||||
final isExpired = now.isAfter(endDate);
|
||||
final remainingDays = isExpired ? 0 : endDate.difference(now).inDays;
|
||||
|
||||
if (isExpired) {
|
||||
return '만료됨';
|
||||
} else if (remainingDays <= 30) {
|
||||
return '$remainingDays일 남음';
|
||||
} else {
|
||||
return _formatDate(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 재고 상태 위젯 빌더 (백엔드 기반 단순화)
|
||||
Widget _buildInventoryStatus(UnifiedEquipment equipment) {
|
||||
// 백엔드 Equipment_History 기반으로 단순 상태만 표시
|
||||
Widget stockInfo;
|
||||
if (equipment.status == 'I') {
|
||||
// 입고 상태: 재고 있음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'보유중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.green[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == 'O') {
|
||||
// 출고 상태: 재고 없음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'출고됨',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.orange[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == 'T') {
|
||||
// 대여 상태
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.schedule, color: Colors.blue, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'대여중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.blue[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 기타 상태
|
||||
stockInfo = Text(
|
||||
'-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
|
||||
return stockInfo;
|
||||
}
|
||||
|
||||
/// 상태 배지 빌더
|
||||
Widget _buildStatusBadge(String status) {
|
||||
@@ -1528,51 +1257,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 입출고일 위젯 빌더
|
||||
Widget _buildCreatedDateWidget(UnifiedEquipment equipment) {
|
||||
String dateStr = equipment.date.toString().substring(0, 10);
|
||||
return Text(
|
||||
dateStr,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 액션 버튼 빌더
|
||||
Widget _buildActionButtons(int equipmentId) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 이력 버튼 - 텍스트 + 아이콘으로 강화
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipmentId),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.history, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('이력', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 편집 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEditById(equipmentId),
|
||||
child: const Icon(Icons.edit_outlined, size: 14),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 삭제 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDeleteById(equipmentId),
|
||||
child: const Icon(Icons.delete_outline, size: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 장비 이력 다이얼로그 표시
|
||||
void _showEquipmentHistoryDialog(int equipmentId) async {
|
||||
@@ -1596,43 +1282,10 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
|
||||
// 편집 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리
|
||||
void _handleEditById(int equipmentId) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == equipmentId,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
_handleEdit(equipment);
|
||||
}
|
||||
|
||||
// 삭제 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리
|
||||
void _handleDeleteById(int equipmentId) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == equipmentId,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
_handleDelete(equipment);
|
||||
}
|
||||
|
||||
/// 체크박스 선택 관련 함수들
|
||||
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); // 선택 해제
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 API에서 제공하지 않음)
|
||||
|
||||
Reference in New Issue
Block a user