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:
@@ -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',
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user