feat: 장비 관리 기능 강화 및 이력 추적 개선
- EquipmentHistoryDto 모델 확장 (상세 정보 추가) - 장비 이력 화면 UI/UX 개선 - 장비 입고 폼 검증 로직 강화 - 테스트 이력 화면 추가 - API 응답 처리 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,8 @@ class EquipmentHistoryScreen extends StatefulWidget {
|
||||
class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
List<EquipmentHistoryDto> _histories = [];
|
||||
bool _isLoading = true;
|
||||
bool _isLoading = false; // 초기값을 false로 변경
|
||||
bool _isInitialLoad = true; // 초기 로딩 상태 추가
|
||||
String? _error;
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
@@ -34,7 +35,8 @@ class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHistory();
|
||||
print('[INIT] EquipmentHistoryScreen initialized for equipment ${widget.equipmentId}');
|
||||
_loadHistory(isInitialLoad: true);
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@@ -50,27 +52,62 @@ class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadHistory({bool isRefresh = false}) async {
|
||||
Future<void> _loadHistory({bool isRefresh = false, bool isInitialLoad = false}) async {
|
||||
print('[_loadHistory] Called - isRefresh: $isRefresh, isInitialLoad: $isInitialLoad, _isLoading: $_isLoading');
|
||||
|
||||
if (isRefresh) {
|
||||
_currentPage = 1;
|
||||
_hasMore = true;
|
||||
_histories.clear();
|
||||
}
|
||||
|
||||
if (!_hasMore || (!isRefresh && _isLoading)) return;
|
||||
// 초기 로딩이 아닌 경우에만 중복 호출 방지
|
||||
if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) {
|
||||
print('[_loadHistory] Skipping - hasMore: $_hasMore, isLoading: $_isLoading');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
if (isInitialLoad) _isInitialLoad = false;
|
||||
});
|
||||
|
||||
try {
|
||||
print('[DEBUG] ==== STARTING HISTORY LOAD ====');
|
||||
print('[DEBUG] Equipment ID: ${widget.equipmentId}');
|
||||
print('[DEBUG] Equipment Name: ${widget.equipmentName}');
|
||||
print('[DEBUG] Page: $_currentPage, PerPage: $_perPage');
|
||||
print('[DEBUG] Current time: ${DateTime.now()}');
|
||||
|
||||
// 타임아웃 설정
|
||||
final histories = await _equipmentService.getEquipmentHistory(
|
||||
widget.equipmentId,
|
||||
page: _currentPage,
|
||||
perPage: _perPage,
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
print('[ERROR] API call timeout after 10 seconds');
|
||||
throw Exception('API 호출 시간 초과 (10초)');
|
||||
},
|
||||
);
|
||||
|
||||
print('[DEBUG] API call completed successfully');
|
||||
print('[DEBUG] Received ${histories.length} history records');
|
||||
if (histories.isNotEmpty) {
|
||||
print('[DEBUG] First history record: ${histories.first.toJson()}');
|
||||
} else {
|
||||
print('[DEBUG] No history records found');
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
print('[WARNING] Widget not mounted, skipping setState');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (isRefresh) {
|
||||
_histories = histories;
|
||||
@@ -80,17 +117,25 @@ class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
_hasMore = histories.length == _perPage;
|
||||
if (_hasMore) _currentPage++;
|
||||
_isLoading = false;
|
||||
print('[DEBUG] State updated - Loading: false, Histories count: ${_histories.length}');
|
||||
});
|
||||
} on Failure catch (e) {
|
||||
print('[ERROR] Failure loading history: ${e.message}');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
print('[ERROR] Unexpected error loading history: $e');
|
||||
print('[ERROR] Stack trace: $stackTrace');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = '이력을 불러오는 중 오류가 발생했습니다: $e';
|
||||
_isLoading = false;
|
||||
});
|
||||
} finally {
|
||||
print('[DEBUG] ==== HISTORY LOAD COMPLETED ====');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +228,8 @@ class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print('[BUILD] EquipmentHistoryScreen - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
@@ -195,51 +242,110 @@ class _EquipmentHistoryScreenState extends State<EquipmentHistoryScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => _loadHistory(isRefresh: true),
|
||||
tooltip: '새로고침',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading && _histories.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null && _histories.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
print('[UI] Building body - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}');
|
||||
|
||||
// 초기 로딩 또는 일반 로딩 중
|
||||
if ((_isInitialLoad || _isLoading) && _histories.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text('장비 ID ${widget.equipmentId}의 이력을 불러오는 중...'),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
print('[USER] Cancel loading clicked');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_error = '사용자가 로딩을 취소했습니다';
|
||||
});
|
||||
},
|
||||
child: const Text('로딩 취소'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_error != null && _histories.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Text('오류 발생', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
_error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('다시 시도'),
|
||||
onPressed: () => _loadHistory(isRefresh: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _loadHistory(isRefresh: true),
|
||||
child: _histories.isEmpty
|
||||
? ListView(
|
||||
children: [
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 200),
|
||||
const Icon(Icons.history, size: 48, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _loadHistory(isRefresh: true),
|
||||
child: const Text('다시 시도'),
|
||||
const Center(
|
||||
child: Text(
|
||||
'이력이 없습니다.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => _loadHistory(isRefresh: true),
|
||||
child: const Text('새로고침'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _histories.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _histories.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return _buildHistoryItem(_histories[index]);
|
||||
},
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => _loadHistory(isRefresh: true),
|
||||
child: _histories.isEmpty
|
||||
? ListView(
|
||||
children: const [
|
||||
SizedBox(height: 200),
|
||||
Center(
|
||||
child: Text(
|
||||
'이력이 없습니다.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _histories.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _histories.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return _buildHistoryItem(_histories[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user