장비 이력 상세보기 지원 및 상태/선택 동기화 개선
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

- EquipmentHistoryDialog: 좌우 분할 상세 패널(데스크톱) + 하단 시트(모바일) 추가
  • 거래일/유형/수량/창고/장비/회사/비고/생성·수정일 표시
  • 리스트 항목 선택 하이라이트, ID 복사 기능
- EquipmentListController: 이력 최신순 정렬 후 상태/일자 결정(오래된 순 가정 제거)
- EquipmentList(ShadTable): 체크박스 선택 시 컨트롤러 선택집합과 동기화
- ResponseInterceptor: equipment-history는 transaction_type 로깅, 장비 목록은 status 로깅으로 혼동 제거
This commit is contained in:
JiWoong Sul
2025-09-14 16:27:05 +09:00
parent d777333187
commit 7543df5a02
4 changed files with 232 additions and 18 deletions

View File

@@ -11,12 +11,22 @@ class ResponseInterceptor extends Interceptor {
debugPrint('[ResponseInterceptor] 상태 코드: ${response.statusCode}');
debugPrint('[ResponseInterceptor] 응답 데이터 타입: ${response.data.runtimeType}');
// 장비 관련 API 응답 상세 로깅
if (response.requestOptions.path.contains('equipment')) {
debugPrint('[ResponseInterceptor] 장비 API 응답 전체: ${response.data}');
// 장비/이력 관련 API 응답 상세 로깅 (혼동 방지용)
if (response.requestOptions.path.contains('equipment-history')) {
if (response.data is List && (response.data as List).isNotEmpty) {
final firstItem = (response.data as List).first;
debugPrint('[ResponseInterceptor] 첫 번째 장비 상태: ${firstItem['status']}');
debugPrint('[ResponseInterceptor] equipment-history 첫 항목 transaction_type: ${firstItem['transaction_type']}');
} else if (response.data is Map && (response.data as Map).containsKey('data')) {
final data = (response.data as Map)['data'];
if (data is List && data.isNotEmpty) {
debugPrint('[ResponseInterceptor] equipment-history 첫 항목 transaction_type: ${data.first['transaction_type']}');
}
}
} else if (response.requestOptions.path.contains('equipment')) {
debugPrint('[ResponseInterceptor] 장비 API 응답 도메인: ${response.requestOptions.path}');
if (response.data is List && (response.data as List).isNotEmpty) {
final firstItem = (response.data as List).first;
debugPrint('[ResponseInterceptor] 장비 첫 항목 status 필드: ${firstItem['status']}');
}
}
@@ -112,4 +122,4 @@ class ResponseInterceptor extends Interceptor {
return !hasMetaKey;
}
}
}

View File

@@ -135,11 +135,16 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
try {
final histories = await _historyService.getEquipmentHistoriesByEquipmentId(dto.id);
if (histories.isNotEmpty) {
// 최신 히스토리의 transaction_type 사용
// 히스토리는 최신순으로 정렬되어 있다고 가정
status = histories.first.transactionType ?? 'I';
transactionDate = histories.first.transactedAt ?? transactionDate;
print('DEBUG [EquipmentListController] Equipment ${dto.id} status from history: $status');
// 최신 히스토리를 기준으로 상태 결정 (서버 정렬 보장 없음 → 클라이언트 정렬)
histories.sort((a, b) {
final aDate = a.transactedAt ?? a.createdAt;
final bDate = b.transactedAt ?? b.createdAt;
return bDate.compareTo(aDate); // 내림차순: 최신 우선
});
final latest = histories.first;
status = latest.transactionType ?? 'I';
transactionDate = latest.transactedAt ?? transactionDate;
print('DEBUG [EquipmentListController] Equipment ${dto.id} latest status from history: $status @ ${latest.transactedAt ?? latest.createdAt}');
}
} catch (e) {
print('DEBUG [EquipmentListController] Failed to get history for equipment ${dto.id}: $e');

View File

@@ -189,11 +189,20 @@ class _EquipmentListState extends State<EquipmentList> {
onChanged: (checked) {
setState(() {
if (id == null) return;
// 로컬 선택 상태 업데이트
if (checked == true) {
_selectedItems.add(id);
} else {
_selectedItems.remove(id);
}
// 컨트롤러 선택 상태와 동기화
final equipmentKey = '${item.equipment.id}:${item.status}';
final isInController = _controller.selectedEquipmentIds.contains(equipmentKey);
if (checked == true && !isInController) {
_controller.toggleSelection(item);
} else if (checked == false && isInController) {
_controller.toggleSelection(item);
}
});
},
),

View File

@@ -56,6 +56,9 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
bool _hasMore = true;
String _searchQuery = '';
// 상세보기 상태
EquipmentHistoryDto? _selectedHistory;
@override
void initState() {
super.initState();
@@ -99,6 +102,12 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
}
}
void _selectHistory(EquipmentHistoryDto history) {
setState(() {
_selectedHistory = history;
});
}
Future<void> _loadHistory({
bool isRefresh = false,
bool isInitialLoad = false
@@ -208,13 +217,14 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
Widget _buildHistoryItem(EquipmentHistoryDto history) {
final typeColor = _getTransactionTypeColor(history.transactionType);
final typeText = _getTransactionTypeText(history.transactionType);
final isSelected = _selectedHistory?.id != null && _selectedHistory?.id == history.id;
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: ShadcnTheme.card,
color: isSelected ? ShadcnTheme.cardHover : ShadcnTheme.card,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ShadcnTheme.border),
border: Border.all(color: isSelected ? ShadcnTheme.primary : ShadcnTheme.border),
boxShadow: ShadcnTheme.shadowSm,
),
child: Material(
@@ -222,7 +232,7 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {}, // 클릭 효과를 위한 빈 핸들러
onTap: () => _selectHistory(history),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
@@ -543,7 +553,40 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
);
}
// 이력 목록
// 이력 목록 + 상세 (데스크톱: 분할, 모바일: 목록만)
if (isDesktop) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 목록
Expanded(
flex: 6,
child: ListView.builder(
controller: _scrollController,
itemCount: _filteredHistories.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0),
itemBuilder: (context, index) {
if (index == _filteredHistories.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildHistoryItem(_filteredHistories[index]);
},
),
),
const SizedBox(width: 16),
// 상세 패널
Expanded(
flex: 5,
child: _buildDetailPanel(),
),
],
),
);
}
// 모바일: 목록만, 항목 탭 시 상세 다이얼로그 표시
return Padding(
padding: const EdgeInsets.all(16),
child: ListView.builder(
@@ -553,12 +596,17 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
if (index == _filteredHistories.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildHistoryItem(_filteredHistories[index]);
final item = _filteredHistories[index];
return GestureDetector(
onTap: () {
_selectHistory(item);
_showDetailBottomSheet(context);
},
child: _buildHistoryItem(item),
);
},
),
);
@@ -617,4 +665,146 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
),
);
}
Widget _buildDetailPanel() {
final h = _selectedHistory;
return Container(
decoration: BoxDecoration(
color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ShadcnTheme.border),
),
padding: const EdgeInsets.all(16),
child: h == null
? Center(
child: Text(
'왼쪽에서 이력을 선택하세요',
style: TextStyle(color: Colors.grey.shade600),
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.receipt_long, color: Colors.grey.shade700),
const SizedBox(width: 8),
const Text('이력 상세', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
if (h.id != null)
IconButton(
tooltip: 'ID 복사',
icon: const Icon(Icons.copy, size: 18),
onPressed: () {
Clipboard.setData(ClipboardData(text: h.id.toString()));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('ID가 복사되었습니다')),
);
},
),
],
),
const SizedBox(height: 12),
_detailRow('거래일', _formatDate(h.transactedAt)),
_detailRow('유형', _getTransactionTypeText(h.transactionType)),
_detailRow('수량', h.quantity.toString()),
_detailRow('창고', h.warehouseName ?? (h.warehousesId > 0 ? '#${h.warehousesId}' : '-')),
_detailRow('장비', h.equipment?.serialNumber ?? h.equipmentSerial ?? '-'),
_detailRow('회사', h.companies.isNotEmpty
? h.companies.map((e) => e['name'] ?? e['id']?.toString() ?? '-').join(', ')
: '-'),
const SizedBox(height: 8),
const Text('비고', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: ShadcnTheme.background,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: ShadcnTheme.border),
),
child: Text(h.remark?.isNotEmpty == true ? h.remark! : '없음'),
),
const SizedBox(height: 12),
Divider(color: ShadcnTheme.border),
const SizedBox(height: 8),
_detailRow('생성일', _formatDate(h.createdAt)),
_detailRow('수정일', _formatDate(h.updatedAt)),
],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 80, child: Text(label, style: const TextStyle(fontWeight: FontWeight.w600))),
const SizedBox(width: 8),
Expanded(child: Text(value)),
],
),
);
}
void _showDetailBottomSheet(BuildContext context) {
if (_selectedHistory == null) return;
final h = _selectedHistory!;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: ShadcnTheme.card,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('이력 상세', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
)
],
),
const SizedBox(height: 12),
_detailRow('거래일', _formatDate(h.transactedAt)),
_detailRow('유형', _getTransactionTypeText(h.transactionType)),
_detailRow('수량', h.quantity.toString()),
_detailRow('창고', h.warehouseName ?? (h.warehousesId > 0 ? '#${h.warehousesId}' : '-')),
_detailRow('장비', h.equipment?.serialNumber ?? h.equipmentSerial ?? '-'),
_detailRow('회사', h.companies.isNotEmpty
? h.companies.map((e) => e['name'] ?? e['id']?.toString() ?? '-').join(', ')
: '-'),
const SizedBox(height: 8),
const Text('비고', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: ShadcnTheme.background,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: ShadcnTheme.border),
),
child: Text(h.remark?.isNotEmpty == true ? h.remark! : '없음'),
),
const SizedBox(height: 16),
],
),
),
);
},
);
}
}