feat: 장비 관리 API 연동 구현

- Equipment 관련 DTO 모델 생성 (Request/Response/List/History/In/Out/IO)
- EquipmentRemoteDataSource 구현 (10개 API 엔드포인트)
- EquipmentService 비즈니스 로직 구현
- Controller를 ChangeNotifier 패턴으로 개선
- 장비 목록 화면에 Provider 패턴 및 무한 스크롤 적용
- 장비 입고 화면 API 연동 및 비동기 처리
- DI 컨테이너에 Equipment 관련 의존성 등록
- API/Mock 데이터 소스 전환 가능 (Feature Flag)
- API 통합 진행 상황 문서 업데이트
This commit is contained in:
JiWoong Sul
2025-07-24 16:26:04 +09:00
parent a13c485302
commit 1d1e38bcfa
30 changed files with 4920 additions and 80 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
@@ -20,7 +21,6 @@ class EquipmentListRedesign extends StatefulWidget {
class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
late final EquipmentListController _controller;
bool _isLoading = false;
bool _showDetailedColumns = true;
final TextEditingController _searchController = TextEditingController();
final ScrollController _horizontalScrollController = ScrollController();
@@ -36,8 +36,14 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
super.initState();
_controller = EquipmentListController(dataService: MockDataService());
_setInitialFilter();
_controller.loadData(); // 원본과 동일하게 직접 호출
print('DEBUG: Equipment count after loadData: ${_controller.equipments.length}'); // 디버그 정보
// 무한 스크롤 리스너 추가
_verticalScrollController.addListener(_onScroll);
// API 호출을 위해 Future로 변경
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.loadData(); // 비동기 호출
});
}
@override
@@ -45,6 +51,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
_searchController.dispose();
_horizontalScrollController.dispose();
_verticalScrollController.dispose();
_controller.dispose();
super.dispose();
}
@@ -85,12 +92,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
}
/// 데이터 로드
void _loadData() {
_controller.loadData();
Future<void> _loadData({bool isRefresh = false}) async {
await _controller.loadData(isRefresh: isRefresh);
}
/// 상태 필터 변경
void _onStatusFilterChanged(String status) {
Future<void> _onStatusFilterChanged(String status) async {
setState(() {
_selectedStatus = status;
// 상태 필터를 EquipmentStatus 상수로 변환
@@ -103,9 +110,9 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
} else if (status == 'rent') {
_controller.selectedStatusFilter = EquipmentStatus.rent;
}
_controller.loadData();
_currentPage = 1;
});
await _controller.changeStatusFilter(_controller.selectedStatusFilter);
}
/// 검색 실행
@@ -141,6 +148,17 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
_controller.selectedEquipmentIds.contains('${e.id}:${e.status}'));
}
/// 스크롤 이벤트 처리 (무한 스크롤)
void _onScroll() {
if (_verticalScrollController.position.pixels >=
_verticalScrollController.position.maxScrollExtent * 0.8) {
// 스크롤이 80% 이상 내려갔을 때 다음 페이지 로드
if (!_controller.isLoading && _controller.hasMore) {
_controller.loadData();
}
}
}
/// 필터링된 장비 목록 반환
List<UnifiedEquipment> _getFilteredEquipments() {
var equipments = _controller.equipments;
@@ -325,24 +343,35 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
@override
Widget build(BuildContext context) {
// 선택된 장비 개수
final int selectedCount = _controller.getSelectedEquipmentCount();
final int selectedInCount = _controller.getSelectedInStockCount();
final int selectedOutCount = _controller.getSelectedEquipmentCountByStatus(EquipmentStatus.out);
final int selectedRentCount = _controller.getSelectedEquipmentCountByStatus(EquipmentStatus.rent);
return ChangeNotifierProvider<EquipmentListController>.value(
value: _controller,
child: Consumer<EquipmentListController>(
builder: (context, controller, child) {
// 선택된 장비 개수
final int selectedCount = controller.getSelectedEquipmentCount();
final int selectedInCount = controller.getSelectedInStockCount();
final int selectedOutCount = controller.getSelectedEquipmentCountByStatus(EquipmentStatus.out);
final int selectedRentCount = controller.getSelectedEquipmentCountByStatus(EquipmentStatus.rent);
return Container(
color: ShadcnTheme.background,
child: Column(
children: [
// 필터 및 액션 바
_buildFilterBar(selectedCount, selectedInCount, selectedOutCount, selectedRentCount),
return Container(
color: ShadcnTheme.background,
child: Column(
children: [
// 필터 및 액션 바
_buildFilterBar(selectedCount, selectedInCount, selectedOutCount, selectedRentCount),
// 장비 테이블
Expanded(
child: _isLoading ? _buildLoadingState() : _buildEquipmentTable(),
),
],
// 장비 테이블
Expanded(
child: controller.isLoading
? _buildLoadingState()
: controller.error != null
? _buildErrorState()
: _buildEquipmentTable(),
),
],
),
);
},
),
);
}
@@ -606,6 +635,30 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
);
}
/// 에러 상태 위젯
Widget _buildErrorState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: ShadcnTheme.destructive),
const SizedBox(height: 16),
Text('데이터를 불러오는 중 오류가 발생했습니다.', style: ShadcnTheme.bodyMuted),
const SizedBox(height: 8),
Text(_controller.error ?? '', style: ShadcnTheme.bodySmall),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _controller.loadData(isRefresh: true),
style: ElevatedButton.styleFrom(
backgroundColor: ShadcnTheme.primary,
),
child: const Text('다시 시도'),
),
],
),
);
}
/// 테이블 너비 계산
double _calculateTableWidth(List<UnifiedEquipment> pagedEquipments) {
double totalWidth = 0;