fix: API 응답 파싱 오류 수정 및 에러 처리 개선

주요 변경사항:
- 창고 관리 API 응답 구조와 DTO 불일치 수정
  - WarehouseLocationDto에 code, manager_phone 필드 추가
  - RemoteDataSource에서 API 응답을 DTO 구조에 맞게 변환
- 회사 관리 API 응답 파싱 오류 수정
  - CompanyResponse의 필수 필드를 nullable로 변경
  - PaginatedResponse 구조 매핑 로직 개선
- 에러 처리 및 로깅 개선
  - Service Layer에 상세 에러 로깅 추가
  - Controller에서 에러 타입별 처리
- 새로운 유틸리티 추가
  - ResponseInterceptor: API 응답 정규화
  - DebugLogger: 디버깅 도구
  - HealthCheckService: 서버 상태 확인
- 문서화
  - API 통합 테스트 가이드
  - 에러 분석 보고서
  - 리팩토링 계획서
This commit is contained in:
JiWoong Sul
2025-07-31 19:15:39 +09:00
parent ad2c699ff7
commit f08b7fec79
89 changed files with 10521 additions and 892 deletions

View File

@@ -10,6 +10,7 @@ import 'package:superport/screens/license/license_list_redesign.dart';
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
import 'package:superport/services/auth_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/data/models/auth/auth_user.dart';
/// Microsoft Dynamics 365 스타일의 메인 레이아웃
/// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조
@@ -28,6 +29,8 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
late String _currentRoute;
bool _sidebarCollapsed = false;
late AnimationController _sidebarAnimationController;
AuthUser? _currentUser;
late final AuthService _authService;
late Animation<double> _sidebarAnimation;
@override
@@ -35,6 +38,17 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
super.initState();
_currentRoute = widget.initialRoute;
_setupAnimations();
_authService = GetIt.instance<AuthService>();
_loadCurrentUser();
}
Future<void> _loadCurrentUser() async {
final user = await _authService.getCurrentUser();
if (mounted) {
setState(() {
_currentUser = user;
});
}
}
void _setupAnimations() {
@@ -74,6 +88,12 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
return const LicenseListRedesign();
case Routes.warehouseLocation:
return const WarehouseLocationListRedesign();
case '/test/api':
// Navigator를 사용하여 별도 화면으로 이동
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushNamed(context, '/test/api');
});
return const Center(child: CircularProgressIndicator());
default:
return const OverviewScreenRedesign();
}
@@ -115,6 +135,8 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
return '유지보수 관리';
case Routes.warehouseLocation:
return '입고지 관리';
case '/test/api':
return 'API 테스트';
default:
return '대시보드';
}
@@ -139,6 +161,8 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
return ['', '유지보수 관리'];
case Routes.warehouseLocation:
return ['', '입고지 관리'];
case '/test/api':
return ['', '개발자 도구', 'API 테스트'];
default:
return ['', '대시보드'];
}
@@ -330,7 +354,10 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
onTap: () {
_showProfileMenu(context);
},
child: ShadcnAvatar(initials: 'A', size: 36),
child: ShadcnAvatar(
initials: _currentUser != null ? _currentUser!.name.substring(0, 1).toUpperCase() : 'U',
size: 36,
),
),
],
);
@@ -410,14 +437,20 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
// 프로필 정보
Row(
children: [
ShadcnAvatar(initials: 'A', size: 48),
ShadcnAvatar(
initials: _currentUser != null ? _currentUser!.name.substring(0, 1).toUpperCase() : 'U',
size: 48,
),
const SizedBox(width: ShadcnTheme.spacing4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('관리자', style: ShadcnTheme.headingH4),
Text(
'admin@superport.com',
_currentUser?.name ?? '사용자',
style: ShadcnTheme.headingH4,
),
Text(
_currentUser?.email ?? '',
style: ShadcnTheme.bodyMuted,
),
],
@@ -551,6 +584,17 @@ class SidebarMenuRedesign extends StatelessWidget {
isActive: currentRoute == Routes.license,
),
const SizedBox(height: ShadcnTheme.spacing4),
const Divider(),
const SizedBox(height: ShadcnTheme.spacing4),
_buildMenuItem(
icon: Icons.bug_report,
title: 'API 테스트',
route: '/test/api',
isActive: currentRoute == '/test/api',
),
],
),
),

View File

@@ -184,6 +184,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
'mainCompanyName': null,
});
if (company.branches != null) {
print('[CompanyListRedesign] Company ${company.name} has ${company.branches!.length} branches');
for (final branch in company.branches!) {
displayCompanies.add({
'branch': branch,
@@ -192,10 +193,13 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
'mainCompanyName': company.name,
});
}
} else {
print('[CompanyListRedesign] Company ${company.name} has no branches');
}
}
final int totalCount = displayCompanies.length;
print('[CompanyListRedesign] Total display items: $totalCount (companies + branches)');
return SingleChildScrollView(
controller: _scrollController,

View File

@@ -43,6 +43,8 @@ class CompanyListController extends ChangeNotifier {
// 데이터 로드 및 필터 적용
Future<void> loadData({bool isRefresh = false}) async {
print('[CompanyListController] loadData called - isRefresh: $isRefresh');
if (isRefresh) {
_currentPage = 1;
_hasMore = true;
@@ -59,12 +61,14 @@ class CompanyListController extends ChangeNotifier {
try {
if (_useApi) {
// API 호출
print('[CompanyListController] Using API to fetch companies');
final apiCompanies = await _companyService.getCompanies(
page: _currentPage,
perPage: _perPage,
search: searchKeyword.isNotEmpty ? searchKeyword : null,
isActive: _isActiveFilter,
);
print('[CompanyListController] API returned ${apiCompanies.length} companies');
if (isRefresh) {
companies = apiCompanies;
@@ -76,16 +80,23 @@ class CompanyListController extends ChangeNotifier {
if (_hasMore) _currentPage++;
} else {
// Mock 데이터 사용
print('[CompanyListController] Using Mock data');
companies = dataService.getAllCompanies();
print('[CompanyListController] Mock returned ${companies.length} companies');
_hasMore = false;
}
// 필터 적용
applyFilters();
print('[CompanyListController] After filtering: ${filteredCompanies.length} companies shown');
selectedCompanyIds.clear();
} on Failure catch (e) {
print('[CompanyListController] Failure loading companies: ${e.message}');
_error = e.message;
} catch (e) {
} catch (e, stackTrace) {
print('[CompanyListController] Error loading companies: $e');
print('[CompanyListController] Error type: ${e.runtimeType}');
print('[CompanyListController] Stack trace: $stackTrace');
_error = '회사 목록을 불러오는 중 오류가 발생했습니다: $e';
} finally {
_isLoading = false;

View File

@@ -6,12 +6,14 @@ import 'package:superport/services/mock_data_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/models/equipment_unified_model.dart' as legacy;
import 'package:superport/core/utils/debug_logger.dart';
// companyTypeToString 함수 import
import 'package:superport/utils/constants.dart'
show companyTypeToString, CompanyType;
import 'package:superport/models/company_model.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/core/utils/equipment_status_converter.dart';
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
class EquipmentListController extends ChangeNotifier {
@@ -56,19 +58,48 @@ class EquipmentListController extends ChangeNotifier {
try {
if (_useApi) {
// API 호출
final apiEquipments = await _equipmentService.getEquipments(
DebugLogger.log('장비 목록 API 호출 시작', tag: 'EQUIPMENT', data: {
'page': _currentPage,
'perPage': _perPage,
'statusFilter': selectedStatusFilter,
});
// DTO 형태로 가져와서 status 정보 유지
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
page: _currentPage,
perPage: _perPage,
status: selectedStatusFilter,
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
);
// API 모델을 UnifiedEquipment로 변환
final List<UnifiedEquipment> unifiedEquipments = apiEquipments.map((equipment) {
DebugLogger.log('장비 목록 API 응답', tag: 'EQUIPMENT', data: {
'count': apiEquipmentDtos.length,
'firstItem': apiEquipmentDtos.isNotEmpty ? {
'id': apiEquipmentDtos.first.id,
'equipmentNumber': apiEquipmentDtos.first.equipmentNumber,
'manufacturer': apiEquipmentDtos.first.manufacturer,
'status': apiEquipmentDtos.first.status,
} : null,
});
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
final equipment = Equipment(
id: dto.id,
manufacturer: dto.manufacturer,
name: dto.modelName ?? dto.equipmentNumber,
category: '', // 세부 정보는 상세 조회에서 가져와야 함
subCategory: '',
subSubCategory: '',
serialNumber: dto.serialNumber,
quantity: 1,
inDate: dto.createdAt,
);
return UnifiedEquipment(
id: equipment.id,
id: dto.id,
equipment: equipment,
date: DateTime.now(), // 실제로는 API에서 날짜 정보를 가져와야 함
status: EquipmentStatus.in_, // 기본값, 실제로는 API에서 가져와야 함
date: dto.createdAt,
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
);
}).toList();

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:dartz/dartz.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/data/models/auth/login_request.dart';
import 'package:superport/di/injection_container.dart';
import 'package:superport/services/auth_service.dart';
import 'package:superport/services/health_test_service.dart';
/// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러
class LoginController extends ChangeNotifier {
@@ -40,7 +42,7 @@ class LoginController extends ChangeNotifier {
Future<bool> login() async {
// 입력값 검증
if (idController.text.trim().isEmpty) {
_errorMessage = '이메일을 입력해주세요.';
_errorMessage = '아이디 또는 이메일을 입력해주세요.';
notifyListeners();
return false;
}
@@ -51,13 +53,10 @@ class LoginController extends ChangeNotifier {
return false;
}
// 이메일 형식 검증
// 입력값이 이메일인지 username인지 판단
final inputValue = idController.text.trim();
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(idController.text.trim())) {
_errorMessage = '올바른 이메일 형식이 아닙니다.';
notifyListeners();
return false;
}
final isEmail = emailRegex.hasMatch(inputValue);
// 로딩 시작
_isLoading = true;
@@ -65,29 +64,103 @@ class LoginController extends ChangeNotifier {
notifyListeners();
try {
// 로그인 요청
// 로그인 요청 (이메일 또는 username으로)
final request = LoginRequest(
email: idController.text.trim(),
email: isEmail ? inputValue : null,
username: !isEmail ? inputValue : null,
password: pwController.text,
);
final result = await _authService.login(request);
print('[LoginController] 로그인 요청 시작: ${isEmail ? 'email: ${request.email}' : 'username: ${request.username}'}');
print('[LoginController] 요청 데이터: ${request.toJson()}');
final result = await _authService.login(request).timeout(
const Duration(seconds: 10),
onTimeout: () async {
print('[LoginController] 로그인 요청 타임아웃 (10초)');
return Left(NetworkFailure(message: '요청 시간이 초과되었습니다. 네트워크 연결을 확인해주세요.'));
},
);
print('[LoginController] 로그인 결과 수신: ${result.isRight() ? '성공' : '실패'}');
return result.fold(
(failure) {
print('[LoginController] 로그인 실패: ${failure.message}');
_errorMessage = failure.message;
_isLoading = false;
notifyListeners();
return false;
},
(loginResponse) {
(loginResponse) async {
print('[LoginController] 로그인 성공: ${loginResponse.user.email}');
// Health Test 실행
try {
print('[LoginController] ========== Health Test 시작 ==========');
final healthTestService = HealthTestService();
final testResults = await healthTestService.checkAllEndpoints();
// 상세한 결과 출력
print('\n[LoginController] === 인증 상태 ===');
print('인증됨: ${testResults['auth']?['success']}');
print('Access Token: ${testResults['auth']?['accessToken'] == true ? '있음' : '없음'}');
print('Refresh Token: ${testResults['auth']?['refreshToken'] == true ? '있음' : '없음'}');
print('\n[LoginController] === 대시보드 API ===');
print('Overview Stats: ${testResults['dashboard_stats']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['dashboard_stats']?['error'] != null) {
print(' 에러: ${testResults['dashboard_stats']['error']}');
}
if (testResults['dashboard_stats']?['data'] != null) {
print(' 데이터: ${testResults['dashboard_stats']['data']}');
}
print('\n[LoginController] === 장비 상태 분포 ===');
print('Equipment Status: ${testResults['equipment_status_distribution']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['equipment_status_distribution']?['error'] != null) {
print(' 에러: ${testResults['equipment_status_distribution']['error']}');
}
if (testResults['equipment_status_distribution']?['data'] != null) {
print(' 데이터: ${testResults['equipment_status_distribution']['data']}');
}
print('\n[LoginController] === 장비 목록 ===');
print('Equipments: ${testResults['equipments']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['equipments']?['error'] != null) {
print(' 에러: ${testResults['equipments']['error']}');
}
if (testResults['equipments']?['sample'] != null) {
print(' 샘플: ${testResults['equipments']['sample']}');
}
print('\n[LoginController] === 입고지 ===');
print('Warehouses: ${testResults['warehouses']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['warehouses']?['error'] != null) {
print(' 에러: ${testResults['warehouses']['error']}');
}
print('\n[LoginController] === 회사 ===');
print('Companies: ${testResults['companies']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['companies']?['error'] != null) {
print(' 에러: ${testResults['companies']['error']}');
}
print('\n[LoginController] ========== Health Test 완료 ==========\n');
} catch (e, stackTrace) {
print('[LoginController] Health Test 오류: $e');
print('[LoginController] Stack Trace: $stackTrace');
}
_isLoading = false;
notifyListeners();
return true;
},
);
} catch (e) {
_errorMessage = '로그인 중 오류가 발생했습니다.';
} catch (e, stackTrace) {
print('[LoginController] 로그인 예외 발생: $e');
print('[LoginController] 스택 트레이스: $stackTrace');
_errorMessage = '로그인 중 오류가 발생했습니다: ${e.toString()}';
_isLoading = false;
notifyListeners();
return false;

View File

@@ -130,7 +130,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
),
boxShadow: [
BoxShadow(
color: ShadcnTheme.gradient1.withOpacity(0.3),
color: ShadcnTheme.gradient1.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
@@ -186,10 +186,10 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
),
const SizedBox(height: ShadcnTheme.spacing8),
// 사용자명 입력
// 아이디/이메일 입력
ShadcnInput(
label: '사용자명',
placeholder: '사용자명을 입력하세요',
label: '아이디/이메일',
placeholder: '아이디 또는 이메일을 입력하세요',
controller: controller.idController,
prefixIcon: const Icon(Icons.person_outline),
keyboardType: TextInputType.text,
@@ -229,10 +229,10 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4),
decoration: BoxDecoration(
color: ShadcnTheme.destructive.withOpacity(0.1),
color: ShadcnTheme.destructive.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
border: Border.all(
color: ShadcnTheme.destructive.withOpacity(0.3),
color: ShadcnTheme.destructive.withValues(alpha: 0.3),
),
),
child: Row(
@@ -270,8 +270,13 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
// 테스트 로그인 버튼
ShadcnButton(
text: '테스트 로그인',
onPressed: () {
widget.onLoginSuccess();
onPressed: () async {
// 테스트 계정 정보 자동 입력
widget.controller.idController.text = 'admin@superport.kr';
widget.controller.pwController.text = 'admin123!';
// 실제 로그인 프로세스 실행
await _handleLogin();
},
variant: ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.medium,
@@ -298,7 +303,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing2),
decoration: BoxDecoration(
color: ShadcnTheme.info.withOpacity(0.1),
color: ShadcnTheme.info.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
),
child: Icon(
@@ -323,7 +328,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
Text(
'Copyright 2025 NatureBridgeAI. All rights reserved.',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.foreground.withOpacity(0.7),
color: ShadcnTheme.foreground.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),

View File

@@ -6,6 +6,7 @@ import 'package:superport/data/models/dashboard/overview_stats.dart';
import 'package:superport/data/models/dashboard/recent_activity.dart';
import 'package:superport/services/dashboard_service.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/core/utils/debug_logger.dart';
// 대시보드(Overview) 화면의 상태 및 비즈니스 로직을 담답하는 컨트롤러
class OverviewController extends ChangeNotifier {
@@ -110,14 +111,23 @@ class OverviewController extends ChangeNotifier {
_equipmentStatusError = null;
notifyListeners();
DebugLogger.log('장비 상태 분포 로드 시작', tag: 'DASHBOARD');
final result = await _dashboardService.getEquipmentStatusDistribution();
result.fold(
(failure) {
_equipmentStatusError = failure.message;
DebugLogger.logError('장비 상태 분포 로드 실패', error: failure.message);
},
(status) {
_equipmentStatus = status;
DebugLogger.log('장비 상태 분포 로드 성공', tag: 'DASHBOARD', data: {
'available': status.available,
'inUse': status.inUse,
'maintenance': status.maintenance,
'disposed': status.disposed,
});
},
);

View File

@@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
import 'package:superport/models/warehouse_location_model.dart';
import 'package:superport/services/warehouse_service.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/core/errors/failures.dart';
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
@@ -45,6 +46,8 @@ class WarehouseLocationListController extends ChangeNotifier {
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
if (_isLoading) return;
print('[WarehouseLocationListController] loadWarehouseLocations started - isInitialLoad: $isInitialLoad');
_isLoading = true;
_error = null;
@@ -59,12 +62,15 @@ class WarehouseLocationListController extends ChangeNotifier {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
// API 사용
print('[WarehouseLocationListController] Using API to fetch warehouse locations');
final fetchedLocations = await _warehouseService.getWarehouseLocations(
page: _currentPage,
perPage: _pageSize,
isActive: _isActive,
);
print('[WarehouseLocationListController] API returned ${fetchedLocations.length} locations');
if (isInitialLoad) {
_warehouseLocations = fetchedLocations;
} else {
@@ -77,9 +83,12 @@ class WarehouseLocationListController extends ChangeNotifier {
_total = await _warehouseService.getTotalWarehouseLocations(
isActive: _isActive,
);
print('[WarehouseLocationListController] Total warehouse locations: $_total');
} else {
// Mock 데이터 사용
print('[WarehouseLocationListController] Using Mock data');
final allLocations = mockDataService?.getAllWarehouseLocations() ?? [];
print('[WarehouseLocationListController] Mock data has ${allLocations.length} locations');
// 필터링 적용
var filtered = allLocations;
@@ -113,12 +122,21 @@ class WarehouseLocationListController extends ChangeNotifier {
}
_applySearchFilter();
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
if (!isInitialLoad) {
_currentPage++;
}
} catch (e) {
_error = e.toString();
} catch (e, stackTrace) {
print('[WarehouseLocationListController] Error loading warehouse locations: $e');
print('[WarehouseLocationListController] Error type: ${e.runtimeType}');
print('[WarehouseLocationListController] Stack trace: $stackTrace');
if (e is ServerFailure) {
_error = e.message;
} else {
_error = '오류 발생: ${e.toString()}';
}
} finally {
_isLoading = false;
notifyListeners();

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:superport/models/warehouse_location_model.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
@@ -16,23 +17,30 @@ class WarehouseLocationListRedesign extends StatefulWidget {
class _WarehouseLocationListRedesignState
extends State<WarehouseLocationListRedesign> {
final WarehouseLocationListController _controller =
WarehouseLocationListController();
late WarehouseLocationListController _controller;
int _currentPage = 1;
final int _pageSize = 10;
@override
void initState() {
super.initState();
_controller.loadWarehouseLocations();
_controller = WarehouseLocationListController();
// 초기 데이터 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.loadWarehouseLocations();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
/// 리스트 새로고침
void _reload() {
setState(() {
_controller.loadWarehouseLocations();
_currentPage = 1;
});
_currentPage = 1;
_controller.loadWarehouseLocations();
}
/// 입고지 추가 폼으로 이동
@@ -72,11 +80,9 @@ class _WarehouseLocationListRedesignState
child: const Text('취소'),
),
TextButton(
onPressed: () {
setState(() {
_controller.deleteWarehouseLocation(id);
});
onPressed: () async {
Navigator.of(context).pop();
await _controller.deleteWarehouseLocation(id);
},
child: const Text('삭제'),
),
@@ -87,17 +93,52 @@ class _WarehouseLocationListRedesignState
@override
Widget build(BuildContext context) {
final int totalCount = _controller.warehouseLocations.length;
final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex =
(startIndex + _pageSize) > totalCount
? totalCount
: (startIndex + _pageSize);
final List<WarehouseLocation> pagedLocations = _controller
.warehouseLocations
.sublist(startIndex, endIndex);
return ChangeNotifierProvider.value(
value: _controller,
child: Consumer<WarehouseLocationListController>(
builder: (context, controller, child) {
// 로딩 중일 때
if (controller.isLoading && controller.warehouseLocations.isEmpty) {
return Center(
child: CircularProgressIndicator(),
);
}
return SingleChildScrollView(
// 에러가 있을 때
if (controller.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text(
'오류가 발생했습니다',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(controller.error!),
SizedBox(height: 16),
ElevatedButton(
onPressed: _reload,
child: Text('다시 시도'),
),
],
),
);
}
final int totalCount = controller.warehouseLocations.length;
final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex =
(startIndex + _pageSize) > totalCount
? totalCount
: (startIndex + _pageSize);
final List<WarehouseLocation> pagedLocations = totalCount > 0 && startIndex < totalCount
? controller.warehouseLocations.sublist(startIndex, endIndex)
: [];
return SingleChildScrollView(
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -106,7 +147,17 @@ class _WarehouseLocationListRedesignState
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${totalCount}개 입고지', style: ShadcnTheme.bodyMuted),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${totalCount}개 입고지', style: ShadcnTheme.bodyMuted),
if (controller.searchQuery.isNotEmpty)
Text(
'"${controller.searchQuery}" 검색 결과',
style: ShadcnTheme.bodyMuted.copyWith(fontSize: 12),
),
],
),
ShadcnButton(
text: '입고지 추가',
onPressed: _navigateToAdd,
@@ -168,12 +219,27 @@ class _WarehouseLocationListRedesignState
),
// 테이블 데이터
if (pagedLocations.isEmpty)
if (controller.isLoading)
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
child: Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('데이터를 불러오는 중...', style: ShadcnTheme.bodyMuted),
],
),
),
)
else if (pagedLocations.isEmpty)
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
child: Center(
child: Text(
'등록된 입고지가 없습니다.',
controller.searchQuery.isNotEmpty
? '검색 결과가 없습니다.'
: '등록된 입고지가 없습니다.',
style: ShadcnTheme.bodyMuted,
),
),
@@ -306,7 +372,10 @@ class _WarehouseLocationListRedesignState
),
],
],
),
);
},
),
);
}
}
}