diff --git a/lib/data/datasources/remote/interceptors/response_interceptor.dart b/lib/data/datasources/remote/interceptors/response_interceptor.dart index a816032..1b410ba 100644 --- a/lib/data/datasources/remote/interceptors/response_interceptor.dart +++ b/lib/data/datasources/remote/interceptors/response_interceptor.dart @@ -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; } -} \ No newline at end of file +} diff --git a/lib/screens/equipment/controllers/equipment_list_controller.dart b/lib/screens/equipment/controllers/equipment_list_controller.dart index 2ee4915..1524caa 100644 --- a/lib/screens/equipment/controllers/equipment_list_controller.dart +++ b/lib/screens/equipment/controllers/equipment_list_controller.dart @@ -135,11 +135,16 @@ class EquipmentListController extends BaseListController { 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'); diff --git a/lib/screens/equipment/equipment_list.dart b/lib/screens/equipment/equipment_list.dart index 3f85484..1e9eb70 100644 --- a/lib/screens/equipment/equipment_list.dart +++ b/lib/screens/equipment/equipment_list.dart @@ -189,11 +189,20 @@ class _EquipmentListState extends State { 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); + } }); }, ), diff --git a/lib/screens/equipment/widgets/equipment_history_dialog.dart b/lib/screens/equipment/widgets/equipment_history_dialog.dart index ee00b9f..caaacd9 100644 --- a/lib/screens/equipment/widgets/equipment_history_dialog.dart +++ b/lib/screens/equipment/widgets/equipment_history_dialog.dart @@ -56,6 +56,9 @@ class _EquipmentHistoryDialogState extends State { bool _hasMore = true; String _searchQuery = ''; + // 상세보기 상태 + EquipmentHistoryDto? _selectedHistory; + @override void initState() { super.initState(); @@ -99,6 +102,12 @@ class _EquipmentHistoryDialogState extends State { } } + void _selectHistory(EquipmentHistoryDto history) { + setState(() { + _selectedHistory = history; + }); + } + Future _loadHistory({ bool isRefresh = false, bool isInitialLoad = false @@ -208,13 +217,14 @@ class _EquipmentHistoryDialogState extends State { 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 { 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 { ); } - // 이력 목록 + // 이력 목록 + 상세 (데스크톱: 분할, 모바일: 목록만) + 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 { 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 { ), ); } + + 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), + ], + ), + ), + ); + }, + ); + } }