refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.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/overview/overview_screen_redesign.dart';
|
||||
@@ -10,6 +11,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/services/dashboard_service.dart';
|
||||
import 'package:superport/services/lookup_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
@@ -34,6 +36,7 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
AuthUser? _currentUser;
|
||||
late final AuthService _authService;
|
||||
late final DashboardService _dashboardService;
|
||||
late final LookupService _lookupService;
|
||||
late Animation<double> _sidebarAnimation;
|
||||
int _expiringLicenseCount = 0; // 30일 내 만료 예정 라이선스 수
|
||||
|
||||
@@ -50,8 +53,10 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
_setupAnimations();
|
||||
_authService = GetIt.instance<AuthService>();
|
||||
_dashboardService = GetIt.instance<DashboardService>();
|
||||
_lookupService = GetIt.instance<LookupService>();
|
||||
_loadCurrentUser();
|
||||
_loadLicenseExpirySummary();
|
||||
_initializeLookupData(); // Lookup 데이터 초기화
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentUser() async {
|
||||
@@ -92,6 +97,38 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
print('[ERROR] 스택 트레이스: ${StackTrace.current}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup 데이터 초기화 (앱 시작 시 한 번만 호출)
|
||||
Future<void> _initializeLookupData() async {
|
||||
try {
|
||||
print('[DEBUG] Lookup 데이터 초기화 시작...');
|
||||
|
||||
// 캐시가 유효하지 않을 때만 로드
|
||||
if (!_lookupService.isCacheValid) {
|
||||
await _lookupService.loadAllLookups();
|
||||
|
||||
if (_lookupService.hasData) {
|
||||
print('[DEBUG] Lookup 데이터 로드 성공!');
|
||||
print('[DEBUG] - 장비 타입: ${_lookupService.equipmentTypes.length}개');
|
||||
print('[DEBUG] - 장비 상태: ${_lookupService.equipmentStatuses.length}개');
|
||||
print('[DEBUG] - 라이선스 타입: ${_lookupService.licenseTypes.length}개');
|
||||
print('[DEBUG] - 제조사: ${_lookupService.manufacturers.length}개');
|
||||
print('[DEBUG] - 사용자 역할: ${_lookupService.userRoles.length}개');
|
||||
print('[DEBUG] - 회사 상태: ${_lookupService.companyStatuses.length}개');
|
||||
} else {
|
||||
print('[WARNING] Lookup 데이터가 비어있습니다.');
|
||||
}
|
||||
} else {
|
||||
print('[DEBUG] Lookup 데이터 캐시 사용 (유효)');
|
||||
}
|
||||
|
||||
if (_lookupService.error != null) {
|
||||
print('[ERROR] Lookup 데이터 로드 실패: ${_lookupService.error}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('[ERROR] Lookup 데이터 초기화 중 예외 발생: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_sidebarAnimationController = AnimationController(
|
||||
@@ -222,81 +259,84 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isWideScreen = screenWidth >= 1920;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.backgroundSecondary,
|
||||
body: Column(
|
||||
children: [
|
||||
// F-Pattern: 1차 시선 - 상단 헤더
|
||||
_buildTopHeader(),
|
||||
return Provider<AuthService>.value(
|
||||
value: _authService,
|
||||
child: Scaffold(
|
||||
backgroundColor: ShadcnTheme.backgroundSecondary,
|
||||
body: Column(
|
||||
children: [
|
||||
// F-Pattern: 1차 시선 - 상단 헤더
|
||||
_buildTopHeader(),
|
||||
|
||||
// 메인 콘텐츠 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 좌측 사이드바
|
||||
AnimatedBuilder(
|
||||
animation: _sidebarAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _sidebarAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildSidebar(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 메인 콘텐츠 (최대 너비 제한)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.all(
|
||||
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
||||
_buildPageHeader(),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// F-Pattern: 주요 작업 영역
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: ShadcnTheme.shadowSm,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
||||
child: _getContentForRoute(_currentRoute),
|
||||
),
|
||||
// 메인 콘텐츠 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 좌측 사이드바
|
||||
AnimatedBuilder(
|
||||
animation: _sidebarAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _sidebarAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildSidebar(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 메인 콘텐츠 (최대 너비 제한)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.all(
|
||||
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
||||
_buildPageHeader(),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// F-Pattern: 주요 작업 영역
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: ShadcnTheme.shadowSm,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
||||
child: _getContentForRoute(_currentRoute),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ class AppThemeTailwind {
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
background: surface,
|
||||
surface: cardBackground,
|
||||
surface: surface,
|
||||
error: danger,
|
||||
),
|
||||
scaffoldBackgroundColor: surface,
|
||||
|
||||
@@ -117,7 +117,6 @@ class _AddressInputState extends State<AddressInput> {
|
||||
/// 시/도 드롭다운 오버레이를 표시합니다.
|
||||
void _showRegionOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
@@ -142,7 +141,7 @@ class _AddressInputState extends State<AddressInput> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
/// - 유틸리티:
|
||||
/// - PhoneUtils: 전화번호 관련 유틸리티
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
// import 'package:superport/models/address_model.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||
import 'package:superport/screens/company/widgets/branch_card.dart';
|
||||
// import 'package:superport/screens/company/widgets/branch_card.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||
import 'package:superport/screens/company/widgets/duplicate_company_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/map_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/branch_form_widget.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:superport/screens/company/controllers/branch_form_controller.dart';
|
||||
@@ -114,9 +114,8 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId');
|
||||
|
||||
_controller = CompanyFormController(
|
||||
dataService: useApi ? null : MockDataService(),
|
||||
companyId: companyId,
|
||||
useApi: useApi,
|
||||
useApi: true, // 항상 API 사용
|
||||
);
|
||||
|
||||
// 일반 회사 수정 모드일 때 데이터 로드
|
||||
@@ -140,10 +139,13 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
|
||||
// 지점 수정 모드일 때 branchId로 branch 정보 세팅
|
||||
if (isBranch && branchId != null) {
|
||||
final company = MockDataService().getCompanyById(companyId!);
|
||||
// 디버그: 진입 시 companyId, branchId, company, branches 정보 출력
|
||||
// Mock 서비스 제거 - API를 통해 데이터 로드
|
||||
// 디버그: 진입 시 companyId, branchId 정보 출력
|
||||
print('[DEBUG] 지점 수정 진입: companyId=$companyId, branchId=$branchId');
|
||||
if (company != null && company.branches != null) {
|
||||
// TODO: API를 통해 회사 데이터 로드 필요
|
||||
// 아래 코드는 Mock 서비스 제거로 인해 주석 처리됨
|
||||
/*
|
||||
if (false) { // 임시로 비활성화
|
||||
print(
|
||||
'[DEBUG] 불러온 company.name=${company.name}, branches=${company.branches!.map((b) => 'id:${b.id}, name:${b.name}, remark:${b.remark}').toList()}',
|
||||
);
|
||||
@@ -181,6 +183,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:async';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
@@ -11,7 +12,7 @@ import 'package:superport/screens/common/widgets/standard_data_table.dart'
|
||||
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||
import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
|
||||
import 'package:superport/screens/company/controllers/company_list_controller.dart';
|
||||
|
||||
@@ -33,7 +34,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CompanyListController(dataService: MockDataService());
|
||||
_controller = CompanyListController();
|
||||
_controller.initializeWithPageSize(_pageSize);
|
||||
}
|
||||
|
||||
@@ -48,11 +49,11 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
/// 검색어 입력 처리 (디바운싱)
|
||||
void _onSearchChanged(String value) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
_debounceTimer = Timer(AppConstants.searchDebounce, () {
|
||||
setState(() {
|
||||
_currentPage = 1;
|
||||
});
|
||||
_controller.updateSearchKeyword(value);
|
||||
_controller.search(value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,12 +81,13 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
final success = await _controller.deleteCompany(id);
|
||||
if (!success) {
|
||||
try {
|
||||
await _controller.deleteCompany(id);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error ?? '삭제에 실패했습니다'),
|
||||
content: Text(e.toString()),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -268,7 +270,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
error: controller.error,
|
||||
onRefresh: controller.refresh,
|
||||
emptyMessage:
|
||||
controller.searchKeyword.isNotEmpty
|
||||
controller.searchQuery.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
: '등록된 회사가 없습니다',
|
||||
emptyIcon: Icons.business_outlined,
|
||||
@@ -279,7 +281,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
placeholder: '회사명, 담당자명, 연락처로 검색',
|
||||
onChanged: _onSearchChanged, // 실시간 검색 (디바운싱)
|
||||
onSearch:
|
||||
() => _controller.updateSearchKeyword(
|
||||
() => _controller.search(
|
||||
_searchController.text,
|
||||
), // 즉시 검색
|
||||
onClear: () {
|
||||
@@ -298,8 +300,8 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
totalCount: totalCount,
|
||||
onRefresh: controller.refresh,
|
||||
statusMessage:
|
||||
controller.searchKeyword.isNotEmpty
|
||||
? '"${controller.searchKeyword}" 검색 결과'
|
||||
controller.searchQuery.isNotEmpty
|
||||
? '"${controller.searchQuery}" 검색 결과'
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -319,12 +321,12 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
displayCompanies.isEmpty
|
||||
? StandardEmptyState(
|
||||
title:
|
||||
controller.searchKeyword.isNotEmpty
|
||||
controller.searchQuery.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
: '등록된 회사가 없습니다',
|
||||
icon: Icons.business_outlined,
|
||||
action:
|
||||
controller.searchKeyword.isEmpty
|
||||
controller.searchQuery.isEmpty
|
||||
? StandardActionButtons.addButton(
|
||||
text: '첫 회사 추가하기',
|
||||
onPressed: _navigateToAddScreen,
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
@@ -21,7 +21,7 @@ import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
|
||||
|
||||
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
|
||||
class CompanyFormController {
|
||||
final MockDataService? dataService;
|
||||
// final MockDataService? dataService; // Mock 서비스 제거
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
final int? companyId;
|
||||
|
||||
@@ -77,7 +77,7 @@ class CompanyFormController {
|
||||
bool preventAutoFocus = false;
|
||||
final Map<int, bool> isNewlyAddedBranch = {};
|
||||
|
||||
CompanyFormController({this.dataService, this.companyId, bool useApi = false})
|
||||
CompanyFormController({this.companyId, bool useApi = true})
|
||||
: _useApi = useApi {
|
||||
_setupFocusNodes();
|
||||
_setupControllerListeners();
|
||||
@@ -96,13 +96,8 @@ class CompanyFormController {
|
||||
try {
|
||||
List<Company> companies;
|
||||
|
||||
if (_useApi) {
|
||||
companies = await _companyService.getCompanies();
|
||||
} else if (dataService != null) {
|
||||
companies = dataService!.getAllCompanies();
|
||||
} else {
|
||||
companies = [];
|
||||
}
|
||||
// API만 사용
|
||||
companies = await _companyService.getCompanies();
|
||||
|
||||
companyNames = companies.map((c) => c.name).toList();
|
||||
filteredCompanyNames = companyNames;
|
||||
@@ -125,9 +120,9 @@ class CompanyFormController {
|
||||
if (_useApi) {
|
||||
debugPrint('📝 API에서 회사 정보 로드 중...');
|
||||
company = await _companyService.getCompanyDetail(companyId!);
|
||||
} else if (dataService != null) {
|
||||
debugPrint('📝 Mock에서 회사 정보 로드 중...');
|
||||
company = dataService!.getCompanyById(companyId!);
|
||||
} else {
|
||||
debugPrint('📝 API만 사용 가능');
|
||||
throw Exception('API를 통해만 데이터를 로드할 수 있습니다');
|
||||
}
|
||||
|
||||
debugPrint('📝 로드된 회사: $company');
|
||||
@@ -234,8 +229,9 @@ class CompanyFormController {
|
||||
debugPrint('Failed to load company data: ${e.message}');
|
||||
return;
|
||||
}
|
||||
} else if (dataService != null) {
|
||||
company = dataService!.getCompanyById(companyId!);
|
||||
} else {
|
||||
// API만 사용
|
||||
debugPrint('API를 통해만 데이터를 로드할 수 있습니다');
|
||||
}
|
||||
|
||||
if (company != null) {
|
||||
@@ -364,8 +360,9 @@ class CompanyFormController {
|
||||
// 오류 발생 시 중복 없음으로 처리
|
||||
return null;
|
||||
}
|
||||
} else if (dataService != null) {
|
||||
return dataService!.findCompanyByName(name);
|
||||
} else {
|
||||
// API만 사용
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -440,12 +437,9 @@ class CompanyFormController {
|
||||
debugPrint('Unexpected error saving company: $e');
|
||||
return false;
|
||||
}
|
||||
} else if (dataService != null) {
|
||||
if (companyId == null) {
|
||||
dataService!.addCompany(company);
|
||||
} else {
|
||||
dataService!.updateCompany(company);
|
||||
}
|
||||
} else {
|
||||
// API만 사용
|
||||
throw Exception('API를 통해만 데이터를 저장할 수 있습니다');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -484,10 +478,9 @@ class CompanyFormController {
|
||||
debugPrint('Failed to save branch: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
} else if (dataService != null) {
|
||||
// Mock 데이터 서비스 사용
|
||||
dataService!.updateBranch(companyId!, branch);
|
||||
return true;
|
||||
} else {
|
||||
// API만 사용
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
|
||||
// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class CompanyListController extends ChangeNotifier {
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
|
||||
List<Company> companies = [];
|
||||
List<Company> filteredCompanies = [];
|
||||
String searchKeyword = '';
|
||||
final Set<int> selectedCompanyIds = {};
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
// API만 사용
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
|
||||
// 필터
|
||||
bool? _isActiveFilter;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
bool? get isActiveFilter => _isActiveFilter;
|
||||
|
||||
CompanyListController();
|
||||
|
||||
// 초기 데이터 로드
|
||||
Future<void> initialize() async {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🚀 회사 목록 초기화 시작');
|
||||
print('║ • 페이지 크기: $_perPage개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 페이지 크기를 지정하여 초기화
|
||||
Future<void> initializeWithPageSize(int pageSize) async {
|
||||
_perPage = pageSize;
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🚀 회사 목록 초기화 시작 (커스텀 페이지 크기)');
|
||||
print('║ • 페이지 크기: $_perPage개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 데이터 로드 및 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false}) async {
|
||||
print('🔍 [DEBUG] loadData 시작 - currentPage: $_currentPage, hasMore: $_hasMore, companies.length: ${companies.length}');
|
||||
print('[CompanyListController] loadData called - isRefresh: $isRefresh');
|
||||
|
||||
if (isRefresh) {
|
||||
_currentPage = 1;
|
||||
_hasMore = true;
|
||||
companies.clear();
|
||||
filteredCompanies.clear();
|
||||
}
|
||||
|
||||
if (_isLoading || (!_hasMore && !isRefresh)) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 호출 - 지점 정보 포함
|
||||
print('[CompanyListController] Using API to fetch companies with branches');
|
||||
|
||||
// 지점 정보를 포함한 전체 회사 목록 가져오기
|
||||
final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat();
|
||||
|
||||
// 상세한 회사 정보 로그 출력
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 회사 목록 로드 완료');
|
||||
print('║ ▶ 총 회사 수: ${apiCompaniesWithBranches.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 지점이 있는 회사와 없는 회사 구분
|
||||
int companiesWithBranches = 0;
|
||||
int totalBranches = 0;
|
||||
|
||||
for (final company in apiCompaniesWithBranches) {
|
||||
if (company.branches?.isNotEmpty ?? false) {
|
||||
companiesWithBranches++;
|
||||
totalBranches += company.branches!.length;
|
||||
print('║ • ${company.name}: ${company.branches!.length}개 지점');
|
||||
}
|
||||
}
|
||||
|
||||
final companiesWithoutBranches = apiCompaniesWithBranches.length - companiesWithBranches;
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📈 통계');
|
||||
print('║ • 지점이 있는 회사: ${companiesWithBranches}개');
|
||||
print('║ • 지점이 없는 회사: ${companiesWithoutBranches}개');
|
||||
print('║ • 총 지점 수: ${totalBranches}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리)
|
||||
List<Company> filteredApiCompanies = apiCompaniesWithBranches;
|
||||
if (searchKeyword.isNotEmpty) {
|
||||
final keyword = searchKeyword.toLowerCase();
|
||||
filteredApiCompanies = apiCompaniesWithBranches.where((company) {
|
||||
return company.name.toLowerCase().contains(keyword) ||
|
||||
(company.contactName?.toLowerCase().contains(keyword) ?? false) ||
|
||||
(company.contactPhone?.toLowerCase().contains(keyword) ?? false);
|
||||
}).toList();
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔍 검색 필터 적용');
|
||||
print('║ • 검색어: "$searchKeyword"');
|
||||
print('║ • 필터 전: ${apiCompaniesWithBranches.length}개');
|
||||
print('║ • 필터 후: ${filteredApiCompanies.length}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
// 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리)
|
||||
// if (_isActiveFilter != null) {
|
||||
// filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList();
|
||||
// }
|
||||
|
||||
// 전체 데이터를 한 번에 로드 (View에서 페이지네이션 처리)
|
||||
companies = filteredApiCompanies;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • 로드된 회사 수: ${companies.length}개');
|
||||
print('║ • 필터링된 회사 수: ${filteredApiCompanies.length}개');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 필터 적용
|
||||
applyFilters();
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ ✅ 최종 화면 표시');
|
||||
print('║ • 화면에 표시될 회사 수: ${filteredCompanies.length}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
selectedCompanyIds.clear();
|
||||
} on Failure catch (e) {
|
||||
print('[CompanyListController] Failure loading companies: ${e.message}');
|
||||
_error = e.message;
|
||||
} 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;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 및 필터 적용
|
||||
void applyFilters() {
|
||||
filteredCompanies = companies.where((company) {
|
||||
// 검색어 필터
|
||||
if (searchKeyword.isNotEmpty) {
|
||||
final keyword = searchKeyword.toLowerCase();
|
||||
final matchesName = company.name.toLowerCase().contains(keyword);
|
||||
final matchesContact = company.contactName?.toLowerCase().contains(keyword) ?? false;
|
||||
final matchesPhone = company.contactPhone?.toLowerCase().contains(keyword) ?? false;
|
||||
|
||||
if (!matchesName && !matchesContact && !matchesPhone) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 활성 상태 필터 (현재 API에서 지원안함)
|
||||
// if (_isActiveFilter != null) {
|
||||
// 추후 API 지원 시 구현
|
||||
// }
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
|
||||
if (keyword.isNotEmpty) {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔍 검색어 변경: "$keyword"');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
} else {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ ❌ 검색어 초기화');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
// API 사용 시 새로 조회
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 활성 상태 필터 변경
|
||||
Future<void> changeActiveFilter(bool? isActive) async {
|
||||
_isActiveFilter = isActive;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 회사 선택/해제
|
||||
void toggleCompanySelection(int? companyId) {
|
||||
if (companyId == null) return;
|
||||
|
||||
if (selectedCompanyIds.contains(companyId)) {
|
||||
selectedCompanyIds.remove(companyId);
|
||||
} else {
|
||||
selectedCompanyIds.add(companyId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
void toggleSelectAll() {
|
||||
if (selectedCompanyIds.length == filteredCompanies.length) {
|
||||
selectedCompanyIds.clear();
|
||||
} else {
|
||||
selectedCompanyIds.clear();
|
||||
for (final company in filteredCompanies) {
|
||||
if (company.id != null) {
|
||||
selectedCompanyIds.add(company.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 회사 수 반환
|
||||
int getSelectedCount() {
|
||||
return selectedCompanyIds.length;
|
||||
}
|
||||
|
||||
// 회사 삭제
|
||||
Future<bool> deleteCompany(int companyId) async {
|
||||
try {
|
||||
// API를 통한 삭제
|
||||
await _companyService.deleteCompany(companyId);
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
companies.removeWhere((c) => c.id == companyId);
|
||||
filteredCompanies.removeWhere((c) => c.id == companyId);
|
||||
selectedCompanyIds.remove(companyId);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = '회사 삭제 중 오류가 발생했습니다: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 회사들 삭제
|
||||
Future<bool> deleteSelectedCompanies() async {
|
||||
final selectedIds = selectedCompanyIds.toList();
|
||||
int successCount = 0;
|
||||
|
||||
for (final companyId in selectedIds) {
|
||||
if (await deleteCompany(companyId)) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return successCount == selectedIds.length;
|
||||
}
|
||||
|
||||
// 회사 정보 업데이트 (로컬)
|
||||
void updateCompanyLocally(Company updatedCompany) {
|
||||
final index = companies.indexWhere((c) => c.id == updatedCompany.id);
|
||||
if (index != -1) {
|
||||
companies[index] = updatedCompany;
|
||||
applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 회사 추가 (로컬)
|
||||
void addCompanyLocally(Company newCompany) {
|
||||
companies.insert(0, newCompany);
|
||||
applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 더 많은 데이터 로드
|
||||
Future<void> loadMore() async {
|
||||
print('🔍 [DEBUG] loadMore 호출됨 - hasMore: $_hasMore, isLoading: $_isLoading');
|
||||
if (!_hasMore || _isLoading) {
|
||||
print('🔍 [DEBUG] loadMore 조건 미충족으로 종료 (hasMore: $_hasMore, isLoading: $_isLoading)');
|
||||
return;
|
||||
}
|
||||
print('🔍 [DEBUG] loadMore 실행 - 추가 데이터 로드 시작');
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 리프레시
|
||||
Future<void> refresh() async {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔄 회사 목록 새로고침 시작');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,241 +2,94 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class CompanyListController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
/// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class CompanyListController extends BaseListController<Company> {
|
||||
late final CompanyService _companyService;
|
||||
|
||||
List<Company> companies = [];
|
||||
List<Company> filteredCompanies = [];
|
||||
String searchKeyword = '';
|
||||
// 추가 상태 관리
|
||||
final Set<int> selectedCompanyIds = {};
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag for API usage
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
|
||||
// 필터
|
||||
bool? _isActiveFilter;
|
||||
CompanyType? _typeFilter;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
List<Company> get companies => items;
|
||||
List<Company> get filteredCompanies => items;
|
||||
bool? get isActiveFilter => _isActiveFilter;
|
||||
CompanyType? get typeFilter => _typeFilter;
|
||||
|
||||
CompanyListController({required this.dataService});
|
||||
CompanyListController() {
|
||||
if (GetIt.instance.isRegistered<CompanyService>()) {
|
||||
_companyService = GetIt.instance<CompanyService>();
|
||||
} else {
|
||||
throw Exception('CompanyService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 데이터 로드
|
||||
Future<void> initialize() async {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🚀 회사 목록 초기화 시작');
|
||||
print('║ • 페이지 크기: $_perPage개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 페이지 크기를 지정하여 초기화
|
||||
Future<void> initializeWithPageSize(int pageSize) async {
|
||||
_perPage = pageSize;
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🚀 회사 목록 초기화 시작 (커스텀 페이지 크기)');
|
||||
print('║ • 페이지 크기: $_perPage개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
Future<void> initializeWithPageSize(int newPageSize) async {
|
||||
pageSize = newPageSize;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 데이터 로드 및 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false}) async {
|
||||
print('🔍 [DEBUG] loadData 시작 - currentPage: $_currentPage, hasMore: $_hasMore, companies.length: ${companies.length}');
|
||||
print('[CompanyListController] loadData called - isRefresh: $isRefresh');
|
||||
@override
|
||||
Future<PagedResult<Company>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출 - 회사 목록 조회
|
||||
final apiCompanies = await ErrorHandler.handleApiCall<List<Company>>(
|
||||
() => _companyService.getCompanies(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
search: params.search,
|
||||
isActive: _isActiveFilter,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
if (isRefresh) {
|
||||
_currentPage = 1;
|
||||
_hasMore = true;
|
||||
companies.clear();
|
||||
filteredCompanies.clear();
|
||||
}
|
||||
final items = apiCompanies ?? [];
|
||||
|
||||
if (_isLoading || (!_hasMore && !isRefresh)) return;
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API 호출 - 지점 정보 포함
|
||||
print('[CompanyListController] Using API to fetch companies with branches');
|
||||
|
||||
// 지점 정보를 포함한 전체 회사 목록 가져오기
|
||||
final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat();
|
||||
|
||||
// 상세한 회사 정보 로그 출력
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 회사 목록 로드 완료');
|
||||
print('║ ▶ 총 회사 수: ${apiCompaniesWithBranches.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 지점이 있는 회사와 없는 회사 구분
|
||||
int companiesWithBranches = 0;
|
||||
int totalBranches = 0;
|
||||
|
||||
for (final company in apiCompaniesWithBranches) {
|
||||
if (company.branches?.isNotEmpty ?? false) {
|
||||
companiesWithBranches++;
|
||||
totalBranches += company.branches!.length;
|
||||
print('║ • ${company.name}: ${company.branches!.length}개 지점');
|
||||
}
|
||||
}
|
||||
|
||||
final companiesWithoutBranches = apiCompaniesWithBranches.length - companiesWithBranches;
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📈 통계');
|
||||
print('║ • 지점이 있는 회사: ${companiesWithBranches}개');
|
||||
print('║ • 지점이 없는 회사: ${companiesWithoutBranches}개');
|
||||
print('║ • 총 지점 수: ${totalBranches}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리)
|
||||
List<Company> filteredApiCompanies = apiCompaniesWithBranches;
|
||||
if (searchKeyword.isNotEmpty) {
|
||||
final keyword = searchKeyword.toLowerCase();
|
||||
filteredApiCompanies = apiCompaniesWithBranches.where((company) {
|
||||
return company.name.toLowerCase().contains(keyword) ||
|
||||
(company.contactName?.toLowerCase().contains(keyword) ?? false) ||
|
||||
(company.contactPhone?.toLowerCase().contains(keyword) ?? false);
|
||||
}).toList();
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔍 검색 필터 적용');
|
||||
print('║ • 검색어: "$searchKeyword"');
|
||||
print('║ • 필터 전: ${apiCompaniesWithBranches.length}개');
|
||||
print('║ • 필터 후: ${filteredApiCompanies.length}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
// 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리)
|
||||
// if (_isActiveFilter != null) {
|
||||
// filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList();
|
||||
// }
|
||||
|
||||
// 전체 데이터를 한 번에 로드 (View에서 페이지네이션 처리)
|
||||
companies = filteredApiCompanies;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • 로드된 회사 수: ${companies.length}개');
|
||||
print('║ • 필터링된 회사 수: ${filteredApiCompanies.length}개');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
companies = dataService.getAllCompanies();
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔧 Mock 데이터 로드 완료');
|
||||
print('║ ▶ 총 회사 수: ${companies.length}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
// 필터 적용
|
||||
applyFilters();
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ ✅ 최종 화면 표시');
|
||||
print('║ • 화면에 표시될 회사 수: ${filteredCompanies.length}개');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
selectedCompanyIds.clear();
|
||||
} on Failure catch (e) {
|
||||
print('[CompanyListController] Failure loading companies: ${e.message}');
|
||||
_error = e.message;
|
||||
} 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;
|
||||
notifyListeners();
|
||||
}
|
||||
return PagedResult(items: items, meta: meta);
|
||||
}
|
||||
|
||||
// 검색 및 필터 적용
|
||||
void applyFilters() {
|
||||
filteredCompanies = companies.where((company) {
|
||||
// 검색어 필터
|
||||
if (searchKeyword.isNotEmpty) {
|
||||
final keyword = searchKeyword.toLowerCase();
|
||||
final matchesName = company.name.toLowerCase().contains(keyword);
|
||||
final matchesContact = company.contactName?.toLowerCase().contains(keyword) ?? false;
|
||||
final matchesPhone = company.contactPhone?.toLowerCase().contains(keyword) ?? false;
|
||||
|
||||
if (!matchesName && !matchesContact && !matchesPhone) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 활성 상태 필터 (API 사용 시에는 서버에서 필터링되므로 여기서는 Mock 데이터용)
|
||||
if (_isActiveFilter != null && !_useApi) {
|
||||
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 간주
|
||||
if (_isActiveFilter == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
@override
|
||||
bool filterItem(Company item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return item.name.toLowerCase().contains(q) ||
|
||||
(item.contactPhone?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.contactEmail?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.companyTypes.any((type) => type.name.toLowerCase().contains(q))) ||
|
||||
(item.address.toString().toLowerCase().contains(q));
|
||||
}
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
|
||||
if (keyword.isNotEmpty) {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔍 검색어 변경: "$keyword"');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
} else {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ ❌ 검색어 초기화');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
if (_useApi) {
|
||||
// API 사용 시 새로 조회
|
||||
await loadData(isRefresh: true);
|
||||
} else {
|
||||
// Mock 데이터 사용 시 필터만 적용
|
||||
applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 활성 상태 필터 변경
|
||||
Future<void> changeActiveFilter(bool? isActive) async {
|
||||
_isActiveFilter = isActive;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 회사 선택/해제
|
||||
void toggleCompanySelection(int? companyId) {
|
||||
if (companyId == null) return;
|
||||
|
||||
// 회사 선택/선택 해제
|
||||
void toggleSelection(int companyId) {
|
||||
if (selectedCompanyIds.contains(companyId)) {
|
||||
selectedCompanyIds.remove(companyId);
|
||||
} else {
|
||||
@@ -245,119 +98,73 @@ class CompanyListController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
void toggleSelectAll() {
|
||||
if (selectedCompanyIds.length == filteredCompanies.length) {
|
||||
selectedCompanyIds.clear();
|
||||
} else {
|
||||
selectedCompanyIds.clear();
|
||||
for (final company in filteredCompanies) {
|
||||
if (company.id != null) {
|
||||
selectedCompanyIds.add(company.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 모든 선택 해제
|
||||
void clearSelection() {
|
||||
selectedCompanyIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 회사 수 반환
|
||||
int getSelectedCount() {
|
||||
return selectedCompanyIds.length;
|
||||
// 필터 설정
|
||||
void setFilters({bool? isActive, CompanyType? type}) {
|
||||
_isActiveFilter = isActive;
|
||||
_typeFilter = type;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_isActiveFilter = null;
|
||||
_typeFilter = null;
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 회사 추가
|
||||
Future<void> addCompany(Company company) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _companyService.createCompany(company),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
// 회사 수정
|
||||
Future<void> updateCompany(Company company) async {
|
||||
if (company.id == null) {
|
||||
throw Exception('회사 ID가 없습니다');
|
||||
}
|
||||
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _companyService.updateCompany(company.id!, company),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(company, (c) => c.id == company.id);
|
||||
}
|
||||
|
||||
// 회사 삭제
|
||||
Future<bool> deleteCompany(int companyId) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API를 통한 삭제
|
||||
await _companyService.deleteCompany(companyId);
|
||||
} else {
|
||||
// Mock 데이터 삭제
|
||||
dataService.deleteCompany(companyId);
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
companies.removeWhere((c) => c.id == companyId);
|
||||
filteredCompanies.removeWhere((c) => c.id == companyId);
|
||||
selectedCompanyIds.remove(companyId);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = '회사 삭제 중 오류가 발생했습니다: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
Future<void> deleteCompany(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _companyService.deleteCompany(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
removeItemLocally((c) => c.id == id);
|
||||
selectedCompanyIds.remove(id);
|
||||
}
|
||||
|
||||
// 선택된 회사들 삭제
|
||||
Future<bool> deleteSelectedCompanies() async {
|
||||
final selectedIds = selectedCompanyIds.toList();
|
||||
int successCount = 0;
|
||||
|
||||
for (final companyId in selectedIds) {
|
||||
if (await deleteCompany(companyId)) {
|
||||
successCount++;
|
||||
}
|
||||
Future<void> deleteSelectedCompanies() async {
|
||||
for (final id in selectedCompanyIds.toList()) {
|
||||
await deleteCompany(id);
|
||||
}
|
||||
|
||||
return successCount == selectedIds.length;
|
||||
}
|
||||
|
||||
// 회사 정보 업데이트 (로컬)
|
||||
void updateCompanyLocally(Company updatedCompany) {
|
||||
final index = companies.indexWhere((c) => c.id == updatedCompany.id);
|
||||
if (index != -1) {
|
||||
companies[index] = updatedCompany;
|
||||
applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 회사 추가 (로컬)
|
||||
void addCompanyLocally(Company newCompany) {
|
||||
companies.insert(0, newCompany);
|
||||
applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 더 많은 데이터 로드
|
||||
Future<void> loadMore() async {
|
||||
print('🔍 [DEBUG] loadMore 호출됨 - hasMore: $_hasMore, isLoading: $_isLoading, useApi: $_useApi');
|
||||
if (!_hasMore || _isLoading || !_useApi) {
|
||||
print('🔍 [DEBUG] loadMore 조건 미충족으로 종료 (hasMore: $_hasMore, isLoading: $_isLoading, useApi: $_useApi)');
|
||||
return;
|
||||
}
|
||||
print('🔍 [DEBUG] loadMore 실행 - 추가 데이터 로드 시작');
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// API 사용 여부 토글 (테스트용)
|
||||
void toggleApiUsage() {
|
||||
_useApi = !_useApi;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 리프레시
|
||||
Future<void> refresh() async {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔄 회사 목록 새로고침 시작');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
clearSelection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
import '../../../domain/usecases/base_usecase.dart';
|
||||
import '../../../domain/usecases/company/company_usecases.dart';
|
||||
import '../../../models/company_model.dart';
|
||||
import '../../../services/company_service.dart';
|
||||
import '../../../di/injection_container.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
|
||||
/// UseCase를 활용한 회사 목록 관리 컨트롤러
|
||||
/// BaseListController를 상속받아 공통 기능 재사용
|
||||
class CompanyListControllerWithUseCase extends BaseListController<Company> {
|
||||
// UseCases
|
||||
late final GetCompaniesUseCase _getCompaniesUseCase;
|
||||
late final CreateCompanyUseCase _createCompanyUseCase;
|
||||
late final UpdateCompanyUseCase _updateCompanyUseCase;
|
||||
late final DeleteCompanyUseCase _deleteCompanyUseCase;
|
||||
late final GetCompanyDetailUseCase _getCompanyDetailUseCase;
|
||||
late final ToggleCompanyStatusUseCase _toggleCompanyStatusUseCase;
|
||||
|
||||
// 필터 상태
|
||||
String? selectedType;
|
||||
bool? isActive;
|
||||
|
||||
// 선택된 회사들
|
||||
final Set<int> _selectedCompanyIds = {};
|
||||
Set<int> get selectedCompanyIds => _selectedCompanyIds;
|
||||
bool get hasSelection => _selectedCompanyIds.isNotEmpty;
|
||||
|
||||
CompanyListControllerWithUseCase() {
|
||||
// UseCase 초기화
|
||||
final companyService = inject<CompanyService>();
|
||||
_getCompaniesUseCase = GetCompaniesUseCase(companyService);
|
||||
_createCompanyUseCase = CreateCompanyUseCase(companyService);
|
||||
_updateCompanyUseCase = UpdateCompanyUseCase(companyService);
|
||||
_deleteCompanyUseCase = DeleteCompanyUseCase(companyService);
|
||||
_getCompanyDetailUseCase = GetCompanyDetailUseCase(companyService);
|
||||
_toggleCompanyStatusUseCase = ToggleCompanyStatusUseCase(companyService);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PagedResult<Company>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// UseCase를 통한 데이터 조회
|
||||
final usecaseParams = GetCompaniesParams(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
search: params.search,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
final result = await _getCompaniesUseCase(usecaseParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception(failure.message);
|
||||
},
|
||||
(companies) {
|
||||
// PagedResult로 래핑하여 반환 (임시로 메타데이터 생성)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: companies.length, // 실제로는 서버에서 받아와야 함
|
||||
totalPages: (companies.length / params.perPage).ceil(),
|
||||
hasNext: companies.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: companies, meta: meta);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 생성
|
||||
Future<bool> createCompany(Company company) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = CreateCompanyParams(company: company);
|
||||
final result = await _createCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
|
||||
// ValidationFailure의 경우 상세 에러 표시
|
||||
if (failure is ValidationFailure && failure.errors != null) {
|
||||
final errorMessages = failure.errors!.entries
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.join('\n');
|
||||
errorState = errorMessages;
|
||||
}
|
||||
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(newCompany) {
|
||||
// 로컬 리스트에 추가
|
||||
addItemLocally(newCompany);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 수정
|
||||
Future<bool> updateCompany(int id, Company company) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = UpdateCompanyParams(id: id, company: company);
|
||||
final result = await _updateCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
|
||||
// ValidationFailure의 경우 상세 에러 표시
|
||||
if (failure is ValidationFailure && failure.errors != null) {
|
||||
final errorMessages = failure.errors!.entries
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.join('\n');
|
||||
errorState = errorMessages;
|
||||
}
|
||||
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(updatedCompany) {
|
||||
// 로컬 리스트 업데이트
|
||||
updateItemLocally(updatedCompany, (item) => item.id == id);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 삭제
|
||||
Future<bool> deleteCompany(int id) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = DeleteCompanyParams(id: id);
|
||||
final result = await _deleteCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 로컬 리스트에서 제거
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedCompanyIds.remove(id);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 상세 조회
|
||||
Future<Company?> getCompanyDetail(int id, {bool includeBranches = false}) async {
|
||||
final params = GetCompanyDetailParams(
|
||||
id: id,
|
||||
includeBranches: includeBranches,
|
||||
);
|
||||
|
||||
final result = await _getCompanyDetailUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
notifyListeners();
|
||||
return null;
|
||||
},
|
||||
(company) => company,
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 상태 토글 (활성화/비활성화)
|
||||
Future<bool> toggleCompanyStatus(int id) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
// 현재 회사 상태를 확인하여 토글 (기본값 true로 가정)
|
||||
final params = ToggleCompanyStatusParams(
|
||||
id: id,
|
||||
isActive: false, // 임시로 false로 설정 (실제로는 현재 상태를 API로 확인해야 함)
|
||||
);
|
||||
final result = await _toggleCompanyStatusUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 로컬 리스트에서 상태 업데이트 (실제로는 API에서 업데이트된 Company 객체를 받아와야 함)
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 선택/해제
|
||||
void toggleSelection(int companyId) {
|
||||
if (_selectedCompanyIds.contains(companyId)) {
|
||||
_selectedCompanyIds.remove(companyId);
|
||||
} else {
|
||||
_selectedCompanyIds.add(companyId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 전체 선택/해제
|
||||
void toggleSelectAll() {
|
||||
if (_selectedCompanyIds.length == items.length) {
|
||||
_selectedCompanyIds.clear();
|
||||
} else {
|
||||
_selectedCompanyIds.clear();
|
||||
_selectedCompanyIds.addAll(items.where((c) => c.id != null).map((c) => c.id!));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 초기화
|
||||
void clearSelection() {
|
||||
_selectedCompanyIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 필터 적용
|
||||
void applyFilters({String? type, bool? active}) {
|
||||
selectedType = type;
|
||||
isActive = active;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
selectedType = null;
|
||||
isActive = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 선택된 회사들 일괄 삭제
|
||||
Future<bool> deleteSelectedCompanies() async {
|
||||
if (_selectedCompanyIds.isEmpty) return false;
|
||||
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
bool allSuccess = true;
|
||||
final failedIds = <int>[];
|
||||
|
||||
for (final id in _selectedCompanyIds.toList()) {
|
||||
final params = DeleteCompanyParams(id: id);
|
||||
final result = await _deleteCompanyUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
allSuccess = false;
|
||||
failedIds.add(id);
|
||||
debugPrint('회사 $id 삭제 실패: ${failure.message}');
|
||||
},
|
||||
(_) {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (failedIds.isNotEmpty) {
|
||||
errorState = '일부 회사 삭제 실패: ${failedIds.join(', ')}';
|
||||
}
|
||||
|
||||
_selectedCompanyIds.clear();
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
|
||||
return allSuccess;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
@@ -14,7 +13,6 @@ import 'package:superport/core/utils/debug_logger.dart';
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentInFormController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
@@ -24,7 +22,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _isSaving = false;
|
||||
bool _useApi = true; // Feature flag
|
||||
// API만 사용
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
@@ -76,7 +74,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentInFormController({required this.dataService, this.equipmentInId}) {
|
||||
EquipmentInFormController({this.equipmentInId}) {
|
||||
isEditMode = equipmentInId != null;
|
||||
_loadManufacturers();
|
||||
_loadEquipmentNames();
|
||||
@@ -95,91 +93,71 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
await _loadEquipmentIn();
|
||||
}
|
||||
|
||||
// 제조사 목록 로드
|
||||
// 자동완성 데이터는 API를 통해 로드해야 하지만, 현재는 빈 목록으로 설정
|
||||
void _loadManufacturers() {
|
||||
manufacturers = dataService.getAllManufacturers();
|
||||
// TODO: API를 통해 제조사 목록 로드
|
||||
manufacturers = [];
|
||||
}
|
||||
|
||||
// 장비명 목록 로드
|
||||
void _loadEquipmentNames() {
|
||||
equipmentNames = dataService.getAllEquipmentNames();
|
||||
// TODO: API를 통해 장비명 목록 로드
|
||||
equipmentNames = [];
|
||||
}
|
||||
|
||||
// 카테고리 목록 로드
|
||||
void _loadCategories() {
|
||||
categories = dataService.getAllCategories();
|
||||
// TODO: API를 통해 카테고리 목록 로드
|
||||
categories = [];
|
||||
}
|
||||
|
||||
// 서브카테고리 목록 로드
|
||||
void _loadSubCategories() {
|
||||
subCategories = dataService.getAllSubCategories();
|
||||
// TODO: API를 통해 서브카테고리 목록 로드
|
||||
subCategories = [];
|
||||
}
|
||||
|
||||
// 서브서브카테고리 목록 로드
|
||||
void _loadSubSubCategories() {
|
||||
subSubCategories = dataService.getAllSubSubCategories();
|
||||
// TODO: API를 통해 서브서브카테고리 목록 로드
|
||||
subSubCategories = [];
|
||||
}
|
||||
|
||||
// 입고지 목록 로드
|
||||
void _loadWarehouseLocations() async {
|
||||
if (_useApi) {
|
||||
try {
|
||||
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final locations = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = locations.map((e) => e.name).toList();
|
||||
// 이름-ID 매핑 저장
|
||||
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
||||
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': warehouseLocations.length,
|
||||
'locations': warehouseLocations,
|
||||
'locationMap': warehouseLocationMap,
|
||||
});
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
DebugLogger.logError('입고지 목록 로드 실패', error: e);
|
||||
// 실패 시 Mock 데이터 사용
|
||||
final mockLocations = dataService.getAllWarehouseLocations();
|
||||
warehouseLocations = mockLocations.map((e) => e.name).toList();
|
||||
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
final mockLocations = dataService.getAllWarehouseLocations();
|
||||
warehouseLocations = mockLocations.map((e) => e.name).toList();
|
||||
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
|
||||
try {
|
||||
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final locations = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = locations.map((e) => e.name).toList();
|
||||
// 이름-ID 매핑 저장
|
||||
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
||||
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': warehouseLocations.length,
|
||||
'locations': warehouseLocations,
|
||||
'locationMap': warehouseLocationMap,
|
||||
});
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
DebugLogger.logError('입고지 목록 로드 실패', error: e);
|
||||
// API 실패 시 빈 목록
|
||||
warehouseLocations = [];
|
||||
warehouseLocationMap = {};
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 파트너사 목록 로드
|
||||
void _loadPartnerCompanies() async {
|
||||
if (_useApi) {
|
||||
try {
|
||||
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final companies = await _companyService.getCompanies();
|
||||
partnerCompanies = companies.map((c) => c.name).toList();
|
||||
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': partnerCompanies.length,
|
||||
'companies': partnerCompanies,
|
||||
});
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
DebugLogger.logError('파트너사 목록 로드 실패', error: e);
|
||||
// 실패 시 Mock 데이터 사용
|
||||
partnerCompanies =
|
||||
dataService
|
||||
.getAllCompanies()
|
||||
.where((c) => c.companyTypes.contains(CompanyType.partner))
|
||||
.map((c) => c.name)
|
||||
.toList();
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
partnerCompanies =
|
||||
dataService
|
||||
.getAllCompanies()
|
||||
.where((c) => c.companyTypes.contains(CompanyType.partner))
|
||||
.map((c) => c.name)
|
||||
.toList();
|
||||
try {
|
||||
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final companies = await _companyService.getCompanies();
|
||||
partnerCompanies = companies.map((c) => c.name).toList();
|
||||
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': partnerCompanies.length,
|
||||
'companies': partnerCompanies,
|
||||
});
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
DebugLogger.logError('파트너사 목록 로드 실패', error: e);
|
||||
// API 실패 시 빈 목록
|
||||
partnerCompanies = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,12 +176,11 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
// equipmentInId는 실제로 장비 ID임 (입고 ID가 아님)
|
||||
actualEquipmentId = equipmentInId;
|
||||
|
||||
try {
|
||||
// API에서 장비 정보 가져오기
|
||||
// equipmentInId는 실제로 장빔 ID임 (입고 ID가 아님)
|
||||
actualEquipmentId = equipmentInId;
|
||||
|
||||
try {
|
||||
// API에서 장비 정보 가져오기
|
||||
DebugLogger.log('장비 정보 로드 시작', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': actualEquipmentId,
|
||||
});
|
||||
@@ -238,25 +215,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
} catch (e) {
|
||||
DebugLogger.logError('장비 정보 로드 실패', error: e);
|
||||
// API 실패 시 Mock 데이터 시도
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
actualEquipmentId = equipmentIn.equipment.id;
|
||||
_loadFromMockData(equipmentIn);
|
||||
} else {
|
||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
actualEquipmentId = equipmentIn.equipment.id;
|
||||
_loadFromMockData(equipmentIn);
|
||||
} else {
|
||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_error = '장비 정보를 불러오는데 실패했습니다: $e';
|
||||
DebugLogger.logError('장비 로드 실패', error: e);
|
||||
@@ -266,28 +226,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _loadFromMockData(EquipmentIn equipmentIn) {
|
||||
manufacturer = equipmentIn.equipment.manufacturer;
|
||||
name = equipmentIn.equipment.name;
|
||||
category = equipmentIn.equipment.category;
|
||||
subCategory = equipmentIn.equipment.subCategory;
|
||||
subSubCategory = equipmentIn.equipment.subSubCategory;
|
||||
serialNumber = equipmentIn.equipment.serialNumber ?? '';
|
||||
barcode = equipmentIn.equipment.barcode ?? '';
|
||||
quantity = equipmentIn.equipment.quantity;
|
||||
inDate = equipmentIn.inDate;
|
||||
hasSerialNumber = serialNumber.isNotEmpty;
|
||||
equipmentType = equipmentIn.type;
|
||||
warehouseLocation = equipmentIn.warehouseLocation;
|
||||
partnerCompany = equipmentIn.partnerCompany;
|
||||
remarkController.text = equipmentIn.remark ?? '';
|
||||
|
||||
// 워런티 정보 로드
|
||||
warrantyLicense = equipmentIn.partnerCompany;
|
||||
warrantyStartDate = equipmentIn.inDate;
|
||||
warrantyEndDate = equipmentIn.inDate.add(const Duration(days: 365));
|
||||
warrantyCode = null;
|
||||
}
|
||||
|
||||
// 워런티 기간 계산
|
||||
String getWarrantyPeriodSummary() {
|
||||
@@ -374,9 +312,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
// 워런티 코드 저장 필요시 여기에 추가
|
||||
);
|
||||
|
||||
if (_useApi) {
|
||||
// API 호출
|
||||
if (isEditMode) {
|
||||
// API 호출
|
||||
if (isEditMode) {
|
||||
// 수정 모드 - API로 장비 정보 업데이트
|
||||
if (actualEquipmentId == null) {
|
||||
throw ServerFailure(message: '장비 ID가 없습니다.');
|
||||
@@ -437,35 +374,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
if (isEditMode) {
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
final updatedEquipmentIn = EquipmentIn(
|
||||
id: equipmentIn.id,
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
status: equipmentIn.status,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.updateEquipmentIn(updatedEquipmentIn);
|
||||
}
|
||||
} else {
|
||||
final newEquipmentIn = EquipmentIn(
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.addEquipmentIn(newEquipmentIn);
|
||||
}
|
||||
}
|
||||
|
||||
// 저장 후 리스트 재로딩 (중복 방지 및 최신화)
|
||||
_loadManufacturers();
|
||||
@@ -498,11 +406,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// API 사용 여부 토글 (테스트용)
|
||||
void toggleApiUsage() {
|
||||
_useApi = !_useApi;
|
||||
notifyListeners();
|
||||
}
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_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 {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
String searchKeyword = ''; // 검색어 추가
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
// API만 사용
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
EquipmentListController();
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 호출 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📦 장비 목록 API 호출 시작');
|
||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||
search: search ?? searchKeyword,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 장비 목록 로드 완료');
|
||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
Map<String, int> statusCount = {};
|
||||
for (final dto in apiEquipmentDtos) {
|
||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||
}
|
||||
|
||||
statusCount.forEach((status, count) {
|
||||
print('║ • $status: $count개');
|
||||
});
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 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: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt,
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||
);
|
||||
}).toList();
|
||||
|
||||
equipments = unifiedEquipments;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
|
||||
selectedEquipmentIds.clear();
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(String? status) async {
|
||||
selectedStatusFilter = status;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
await loadData(isRefresh: true, search: keyword);
|
||||
}
|
||||
|
||||
// 장비 선택/해제 (모든 상태 지원)
|
||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
// 출고 정보는 API를 통해 번별로 조회해야 하므로 별도 서비스로 분리 예정
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
// TODO: API로 출고 정보 조회 구현
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 장비 삭제
|
||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||
try {
|
||||
// API를 통한 삭제
|
||||
if (equipment.equipment.id != null) {
|
||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||
} else {
|
||||
throw Exception('Equipment ID is null');
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete equipment: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,338 +1,315 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
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/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentListController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
late final EquipmentService _equipmentService;
|
||||
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
String searchKeyword = ''; // 검색어 추가
|
||||
// 추가 상태 관리
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag for API usage
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
// 필터
|
||||
String? _statusFilter;
|
||||
String? _categoryFilter;
|
||||
int? _companyIdFilter;
|
||||
String? _selectedStatusFilter;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
EquipmentListController({required this.dataService});
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API 호출 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📦 장비 목록 API 호출 시작');
|
||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||
search: search ?? searchKeyword,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 장비 목록 로드 완료');
|
||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
Map<String, int> statusCount = {};
|
||||
for (final dto in apiEquipmentDtos) {
|
||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||
}
|
||||
|
||||
statusCount.forEach((status, count) {
|
||||
print('║ • $status: $count개');
|
||||
});
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 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: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt,
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||
);
|
||||
}).toList();
|
||||
|
||||
equipments = unifiedEquipments;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
equipments = dataService.getAllEquipments();
|
||||
if (selectedStatusFilter != null) {
|
||||
equipments =
|
||||
equipments.where((e) => e.status == selectedStatusFilter).toList();
|
||||
}
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
selectedEquipmentIds.clear();
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(String? status) async {
|
||||
selectedStatusFilter = status;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
List<UnifiedEquipment> get equipments => items;
|
||||
String? get statusFilter => _statusFilter;
|
||||
String? get categoryFilter => _categoryFilter;
|
||||
int? get companyIdFilter => _companyIdFilter;
|
||||
String? get selectedStatusFilter => _selectedStatusFilter;
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
await loadData(isRefresh: true, search: keyword);
|
||||
// Setters
|
||||
set selectedStatusFilter(String? value) {
|
||||
_selectedStatusFilter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 장비 선택/해제 (모든 상태 지원)
|
||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
EquipmentListController() {
|
||||
if (GetIt.instance.isRegistered<EquipmentService>()) {
|
||||
_equipmentService = GetIt.instance<EquipmentService>();
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
throw Exception('EquipmentService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PagedResult<UnifiedEquipment>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final apiEquipmentDtos = await ErrorHandler.handleApiCall(
|
||||
() => _equipmentService.getEquipmentsWithStatus(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
status: _statusFilter != null ?
|
||||
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
|
||||
search: params.search,
|
||||
companyId: _companyIdFilter,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
if (apiEquipmentDtos == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환
|
||||
final items = apiEquipmentDtos.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||
name: dto.modelName ?? dto.equipmentNumber ?? 'Unknown',
|
||||
category: 'Equipment', // 임시 카테고리
|
||||
subCategory: 'General', // 임시 서브카테고리
|
||||
subSubCategory: 'Standard', // 임시 서브서브카테고리
|
||||
serialNumber: dto.serialNumber,
|
||||
quantity: 1, // 기본 수량
|
||||
);
|
||||
|
||||
// 간단한 Company 정보 생성 (사용하지 않으므로 제거)
|
||||
// final company = dto.companyName != null ? ... : null;
|
||||
|
||||
return UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt ?? DateTime.now(),
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status),
|
||||
notes: null, // EquipmentListDto에 remark 필드 없음
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
bool filterItem(UnifiedEquipment item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return (item.equipment.name.toLowerCase().contains(q)) ||
|
||||
(item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.equipment.manufacturer.toLowerCase().contains(q)) ||
|
||||
(item.notes?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.status.toLowerCase().contains(q));
|
||||
}
|
||||
|
||||
/// 장비 선택/선택 해제
|
||||
void toggleSelection(UnifiedEquipment equipment) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||
selectedEquipmentIds.remove(equipmentKey);
|
||||
} else {
|
||||
selectedEquipmentIds.add(equipmentKey);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
/// 모든 선택 해제
|
||||
void clearSelection() {
|
||||
selectedEquipmentIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
/// 선택된 장비 정보 가져오기
|
||||
Map<String, List<UnifiedEquipment>> getSelectedEquipmentsByStatus() {
|
||||
final Map<String, List<UnifiedEquipment>> groupedEquipments = {};
|
||||
|
||||
for (final equipment in items) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||
if (!groupedEquipments.containsKey(equipment.status)) {
|
||||
groupedEquipments[equipment.status] = [];
|
||||
}
|
||||
groupedEquipments[equipment.status]!.add(equipment);
|
||||
}
|
||||
}
|
||||
|
||||
return groupedEquipments;
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
String? status,
|
||||
String? category,
|
||||
int? companyId,
|
||||
}) {
|
||||
_statusFilter = status;
|
||||
_categoryFilter = category;
|
||||
_companyIdFilter = companyId;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 상태 필터 변경
|
||||
void filterByStatus(String? status) {
|
||||
_statusFilter = status;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 카테고리 필터 변경
|
||||
void filterByCategory(String? category) {
|
||||
_categoryFilter = category;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 회사 필터 변경
|
||||
void filterByCompany(int? companyId) {
|
||||
_companyIdFilter = companyId;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_statusFilter = null;
|
||||
_categoryFilter = null;
|
||||
_companyIdFilter = null;
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 장비 삭제
|
||||
Future<void> deleteEquipment(int id, String status) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.deleteEquipment(id),
|
||||
);
|
||||
|
||||
removeItemLocally((e) => e.equipment.id == id && e.status == status);
|
||||
|
||||
// 선택 목록에서도 제거
|
||||
final equipmentKey = '$id:$status';
|
||||
selectedEquipmentIds.remove(equipmentKey);
|
||||
}
|
||||
|
||||
/// 선택된 장비 일괄 삭제
|
||||
Future<void> deleteSelectedEquipments() async {
|
||||
final selectedGroups = getSelectedEquipmentsByStatus();
|
||||
|
||||
for (final entry in selectedGroups.entries) {
|
||||
for (final equipment in entry.value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
await deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
/// 장비 상태 변경 (임시 구현 - API가 지원하지 않음)
|
||||
Future<void> updateEquipmentStatus(int id, String currentStatus, String newStatus) async {
|
||||
debugPrint('장비 상태 변경: $id, $currentStatus -> $newStatus');
|
||||
// TODO: 실제 API가 장비 상태 변경을 지원할 때 구현
|
||||
// 현재는 새로고침만 수행
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 장비 정보 수정
|
||||
Future<void> updateEquipment(int id, UnifiedEquipment equipment) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.updateEquipment(id, equipment.equipment),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(equipment, (e) =>
|
||||
e.equipment.id == equipment.equipment.id &&
|
||||
e.status == equipment.status
|
||||
);
|
||||
}
|
||||
|
||||
/// 상태 필터 변경
|
||||
void changeStatusFilter(String? status) {
|
||||
_selectedStatusFilter = status;
|
||||
_statusFilter = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 검색 키워드 업데이트
|
||||
void updateSearchKeyword(String keyword) {
|
||||
search(keyword); // BaseListController의 search 메서드 사용
|
||||
}
|
||||
|
||||
/// 장비 선택 (토글 선택을 위한 별칭)
|
||||
void selectEquipment(UnifiedEquipment equipment) {
|
||||
toggleSelection(equipment);
|
||||
}
|
||||
|
||||
/// 선택된 입고 상태 장비 개수
|
||||
int getSelectedInStockCount() {
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':입고'))
|
||||
.length;
|
||||
}
|
||||
|
||||
/// 선택된 장비들 가져오기
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
return items.where((equipment) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
return selectedEquipmentIds.contains(equipmentKey);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 선택된 장비들 요약 정보
|
||||
String getSelectedEquipmentsSummary() {
|
||||
final selectedEquipments = getSelectedEquipments();
|
||||
if (selectedEquipments.isEmpty) return '선택된 장비가 없습니다';
|
||||
|
||||
final Map<String, int> statusCounts = {};
|
||||
for (final equipment in selectedEquipments) {
|
||||
statusCounts[equipment.status] = (statusCounts[equipment.status] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final summaryParts = statusCounts.entries
|
||||
.map((entry) => '${entry.key}: ${entry.value}개')
|
||||
.toList();
|
||||
|
||||
return summaryParts.join(', ');
|
||||
}
|
||||
|
||||
/// 선택된 장비 총 개수
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
/// 특정 상태의 선택된 장비 개수
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':$status'))
|
||||
.length;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
final equipmentOut = dataService.getEquipmentOutById(equipmentId);
|
||||
if (equipmentOut != null) {
|
||||
switch (infoType) {
|
||||
case 'company':
|
||||
final company = equipmentOut.company ?? '-';
|
||||
if (company != '-') {
|
||||
final companyObj = dataService.getAllCompanies().firstWhere(
|
||||
(c) => c.name == company,
|
||||
orElse:
|
||||
() => Company(
|
||||
name: company,
|
||||
address: Address(),
|
||||
companyTypes: [CompanyType.customer], // 기본값 고객사
|
||||
),
|
||||
);
|
||||
// 여러 유형 중 첫 번째만 표시 (대표 유형)
|
||||
final typeText =
|
||||
companyObj.companyTypes.isNotEmpty
|
||||
? companyTypeToString(companyObj.companyTypes.first)
|
||||
: '-';
|
||||
return '$company (${typeText})';
|
||||
}
|
||||
return company;
|
||||
case 'manager':
|
||||
return equipmentOut.manager ?? '-';
|
||||
case 'license':
|
||||
return equipmentOut.license ?? '-';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 장비 삭제
|
||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API를 통한 삭제
|
||||
if (equipment.equipment.id != null) {
|
||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||
} else {
|
||||
throw Exception('Equipment ID is null');
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 삭제
|
||||
if (equipment.status == EquipmentStatus.in_) {
|
||||
dataService.deleteEquipmentIn(equipment.id!);
|
||||
} else if (equipment.status == EquipmentStatus.out) {
|
||||
dataService.deleteEquipmentOut(equipment.id!);
|
||||
} else if (equipment.status == EquipmentStatus.rent) {
|
||||
// TODO: 대여 상태 삭제 구현
|
||||
throw UnimplementedError('Rent status deletion not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete equipment: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// API 사용 여부 토글 (테스트용)
|
||||
void toggleApiUsage() {
|
||||
_useApi = !_useApi;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
@@ -14,7 +13,8 @@ import 'package:superport/utils/constants.dart';
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentOutFormController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
int? equipmentOutId;
|
||||
|
||||
// 편집 모드 여부
|
||||
@@ -62,7 +62,6 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentOutFormController({
|
||||
required this.dataService,
|
||||
this.equipmentOutId,
|
||||
}) {
|
||||
isEditMode = equipmentOutId != null;
|
||||
@@ -77,22 +76,32 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 드롭다운 데이터 로드
|
||||
void loadDropdownData() {
|
||||
// 회사 목록 로드 (출고처 가능한 회사만)
|
||||
companies = dataService.getAllCompanies()
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
originalName: c.name,
|
||||
isMainCompany: true,
|
||||
companyId: c.id,
|
||||
Future<void> loadDropdownData() async {
|
||||
try {
|
||||
// API를 통해 회사 목록 로드
|
||||
final allCompanies = await _companyService.getCompanies();
|
||||
companies = allCompanies
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
originalName: c.name,
|
||||
isMainCompany: true,
|
||||
companyId: c.id,
|
||||
branchId: null,
|
||||
))
|
||||
.toList();
|
||||
|
||||
// 라이선스 목록 로드
|
||||
licenses = dataService.getAllLicenses().map((l) => l.name).toList();
|
||||
|
||||
// TODO: 라이선스 목록도 API로 로드
|
||||
licenses = []; // 임시로 빈 목록
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('드롭다운 데이터 로드 실패: $e');
|
||||
companies = [];
|
||||
licenses = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 장비로 초기화
|
||||
@@ -109,23 +118,10 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock 데이터에서 회사별 담당자 목록 가져오기
|
||||
final company = dataService.getAllCompanies().firstWhere(
|
||||
(c) => c.name == selectedCompanies[index],
|
||||
orElse: () => Company(
|
||||
name: '',
|
||||
companyTypes: [],
|
||||
),
|
||||
);
|
||||
|
||||
if (company.name.isNotEmpty && company.contactName != null && company.contactName!.isNotEmpty) {
|
||||
// 회사의 담당자 정보
|
||||
hasManagersPerCompany[index] = true;
|
||||
filteredManagersPerCompany[index] = [company.contactName!];
|
||||
} else {
|
||||
hasManagersPerCompany[index] = false;
|
||||
filteredManagersPerCompany[index] = ['없음'];
|
||||
}
|
||||
// TODO: API를 통해 회사별 담당자 목록 로드
|
||||
// 현재는 임시로 빈 목록 사용
|
||||
hasManagersPerCompany[index] = false;
|
||||
filteredManagersPerCompany[index] = [];
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
// import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart';
|
||||
@@ -181,7 +180,6 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentInFormController(
|
||||
dataService: MockDataService(),
|
||||
equipmentInId: widget.equipmentInId,
|
||||
);
|
||||
|
||||
|
||||
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
@@ -0,0 +1,484 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../services/lookup_service.dart';
|
||||
import '../../data/models/lookups/lookup_data.dart';
|
||||
import '../common/theme_shadcn.dart';
|
||||
import '../common/components/shadcn_components.dart';
|
||||
|
||||
/// LookupService를 활용한 장비 입고 폼 예시
|
||||
/// 전역 캐싱된 Lookup 데이터를 활용하여 드롭다운 구성
|
||||
class EquipmentInFormLookupExample extends StatefulWidget {
|
||||
const EquipmentInFormLookupExample({super.key});
|
||||
|
||||
@override
|
||||
State<EquipmentInFormLookupExample> createState() => _EquipmentInFormLookupExampleState();
|
||||
}
|
||||
|
||||
class _EquipmentInFormLookupExampleState extends State<EquipmentInFormLookupExample> {
|
||||
late final LookupService _lookupService;
|
||||
|
||||
// 선택된 값들
|
||||
String? _selectedEquipmentType;
|
||||
String? _selectedEquipmentStatus;
|
||||
String? _selectedManufacturer;
|
||||
String? _selectedLicenseType;
|
||||
|
||||
// 텍스트 컨트롤러
|
||||
final _serialNumberController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lookupService = GetIt.instance<LookupService>();
|
||||
_loadLookupDataIfNeeded();
|
||||
}
|
||||
|
||||
/// 필요시 Lookup 데이터 로드 (캐시가 없을 경우)
|
||||
Future<void> _loadLookupDataIfNeeded() async {
|
||||
if (!_lookupService.hasData) {
|
||||
await _lookupService.loadAllLookups();
|
||||
if (mounted) {
|
||||
setState(() {}); // UI 업데이트
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serialNumberController.dispose();
|
||||
_quantityController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('장비 입고 (Lookup 활용 예시)'),
|
||||
backgroundColor: ShadcnTheme.card,
|
||||
elevation: 0,
|
||||
),
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _lookupService,
|
||||
child: Consumer<LookupService>(
|
||||
builder: (context, lookupService, child) {
|
||||
if (lookupService.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (lookupService.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
size: 64,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lookup 데이터 로드 실패',
|
||||
style: ShadcnTheme.headingH4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
lookupService.error!,
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 안내 메시지
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.1),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'이 화면은 /lookups API를 통해 캐싱된 전역 데이터를 활용합니다.\n'
|
||||
'드롭다운 데이터는 앱 시작 시 한 번만 로드되어 모든 화면에서 재사용됩니다.',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 폼 카드
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('장비 정보', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 장비 타입 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 타입',
|
||||
value: _selectedEquipmentType,
|
||||
items: lookupService.equipmentTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentType = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 장비 상태 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 상태',
|
||||
value: _selectedEquipmentStatus,
|
||||
items: lookupService.equipmentStatuses,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentStatus = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 제조사 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '제조사',
|
||||
value: _selectedManufacturer,
|
||||
items: lookupService.manufacturers,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedManufacturer = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 시리얼 번호 입력
|
||||
_buildTextField(
|
||||
label: '시리얼 번호',
|
||||
controller: _serialNumberController,
|
||||
hintText: 'SN-2025-001',
|
||||
),
|
||||
|
||||
// 수량 입력
|
||||
_buildTextField(
|
||||
label: '수량',
|
||||
controller: _quantityController,
|
||||
hintText: '1',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
// 라이선스 타입 드롭다운 (옵션)
|
||||
_buildDropdownField(
|
||||
label: '라이선스 타입 (선택)',
|
||||
value: _selectedLicenseType,
|
||||
items: lookupService.licenseTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedLicenseType = value;
|
||||
});
|
||||
},
|
||||
isOptional: true,
|
||||
),
|
||||
|
||||
// 비고 입력
|
||||
_buildTextField(
|
||||
label: '비고',
|
||||
controller: _descriptionController,
|
||||
hintText: '추가 정보를 입력하세요',
|
||||
maxLines: 3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 버튼 그룹
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '취소',
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadcnButton(
|
||||
text: '저장',
|
||||
onPressed: _handleSubmit,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 캐시 정보 표시
|
||||
_buildCacheInfoCard(lookupService),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 드롭다운 필드 빌더
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String? value,
|
||||
required List<LookupItem> items,
|
||||
required ValueChanged<String?> onChanged,
|
||||
bool isOptional = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
if (isOptional) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text('(선택)', style: ShadcnTheme.bodyMuted),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: '선택하세요',
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
items: items.map((item) => DropdownMenuItem(
|
||||
value: item.code ?? '',
|
||||
child: Text(item.name ?? ''),
|
||||
)).toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 텍스트 필드 빌더
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
String? hintText,
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.primary, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 캐시 정보 카드
|
||||
Widget _buildCacheInfoCard(LookupService lookupService) {
|
||||
return ShadcnCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.storage, size: 20, color: ShadcnTheme.muted),
|
||||
const SizedBox(width: 8),
|
||||
Text('Lookup 캐시 정보', style: ShadcnTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCacheItem('장비 타입', lookupService.equipmentTypes.length),
|
||||
_buildCacheItem('장비 상태', lookupService.equipmentStatuses.length),
|
||||
_buildCacheItem('제조사', lookupService.manufacturers.length),
|
||||
_buildCacheItem('라이선스 타입', lookupService.licenseTypes.length),
|
||||
_buildCacheItem('사용자 역할', lookupService.userRoles.length),
|
||||
_buildCacheItem('회사 상태', lookupService.companyStatuses.length),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'캐시 상태: ${lookupService.isCacheValid ? "유효" : "만료"}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: lookupService.isCacheValid
|
||||
? ShadcnTheme.success
|
||||
: ShadcnTheme.warning,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, size: 16, color: ShadcnTheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('캐시 새로고침',
|
||||
style: TextStyle(color: ShadcnTheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCacheItem(String label, int count) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodySmall),
|
||||
Text('$count개',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 폼 제출 처리
|
||||
void _handleSubmit() {
|
||||
// 유효성 검증
|
||||
if (_selectedEquipmentType == null) {
|
||||
_showSnackBar('장비 타입을 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedEquipmentStatus == null) {
|
||||
_showSnackBar('장비 상태를 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serialNumberController.text.isEmpty) {
|
||||
_showSnackBar('시리얼 번호를 입력하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 값 정보 표시
|
||||
final selectedType = _lookupService.findByCode(
|
||||
_lookupService.equipmentTypes,
|
||||
_selectedEquipmentType!,
|
||||
);
|
||||
|
||||
final selectedStatus = _lookupService.findByCode(
|
||||
_lookupService.equipmentStatuses,
|
||||
_selectedEquipmentStatus!,
|
||||
);
|
||||
|
||||
final message = '''
|
||||
장비 입고 정보:
|
||||
- 타입: ${selectedType?.name ?? _selectedEquipmentType}
|
||||
- 상태: ${selectedStatus?.name ?? _selectedEquipmentStatus}
|
||||
- 시리얼: ${_serialNumberController.text}
|
||||
- 수량: ${_quantityController.text.isEmpty ? "1" : _quantityController.text}
|
||||
''';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('입고 정보 확인'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError ? ShadcnTheme.destructive : ShadcnTheme.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import 'package:superport/screens/common/widgets/standard_data_table.dart' as st
|
||||
import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/equipment_display_helper.dart';
|
||||
@@ -42,7 +41,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController(dataService: MockDataService());
|
||||
_controller = EquipmentListController();
|
||||
_setInitialFilter();
|
||||
|
||||
// API 호출을 위해 Future로 변경
|
||||
@@ -116,7 +115,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
}
|
||||
_currentPage = 1;
|
||||
});
|
||||
await _controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
}
|
||||
|
||||
/// 검색 실행
|
||||
@@ -125,13 +124,26 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
_appliedSearchKeyword = _searchController.text;
|
||||
_currentPage = 1;
|
||||
});
|
||||
await _controller.updateSearchKeyword(_searchController.text);
|
||||
_controller.updateSearchKeyword(_searchController.text);
|
||||
}
|
||||
|
||||
/// 장비 선택/해제
|
||||
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
// UnifiedEquipment를 찾아서 선택/해제
|
||||
UnifiedEquipment? equipment;
|
||||
try {
|
||||
equipment = _controller.items.firstWhere(
|
||||
(e) => e.equipment.id == id && e.status == status,
|
||||
);
|
||||
} catch (e) {
|
||||
// 해당하는 장비를 찾지 못함
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller.selectEquipment(id, status, isSelected);
|
||||
_controller.selectEquipment(equipment!);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,7 +152,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
setState(() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
for (final equipment in equipments) {
|
||||
_controller.selectEquipment(equipment.id, equipment.status, value);
|
||||
_controller.selectEquipment(equipment);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -234,7 +246,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
||||
final selectedEquipments = _controller.getSelectedEquipments();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -245,12 +257,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?'),
|
||||
Text('선택한 ${selectedEquipments.length}개 장비를 폐기하시겠습니까?'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
...selectedEquipmentsSummary.map((equipmentData) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
...selectedEquipments.map((unifiedEquipment) {
|
||||
final equipment = unifiedEquipment.equipment;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
@@ -328,26 +340,15 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
);
|
||||
|
||||
// Controller를 통한 삭제 처리
|
||||
final success = await _controller.deleteEquipment(equipment);
|
||||
await _controller.deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||
|
||||
// 로딩 다이얼로그 닫기
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
if (success) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error ?? '삭제 중 오류가 발생했습니다.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
@@ -37,7 +36,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentOutFormController(dataService: MockDataService());
|
||||
_controller = EquipmentOutFormController();
|
||||
_controller.isEditMode = widget.equipmentOutId != null;
|
||||
_controller.equipmentOutId = widget.equipmentOutId;
|
||||
_controller.selectedEquipment = widget.selectedEquipment;
|
||||
@@ -550,9 +549,9 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
Branch? branch;
|
||||
|
||||
if (companyInfo.companyId != null) {
|
||||
company = controller.dataService.getCompanyById(
|
||||
companyInfo.companyId!,
|
||||
);
|
||||
// TODO: 실제 CompanyService를 통해 회사 정보 가져오기
|
||||
// company = await _companyService.getCompanyById(companyInfo.companyId!);
|
||||
company = null; // 임시로 null 처리
|
||||
if (!companyInfo.isMainCompany &&
|
||||
companyInfo.branchId != null &&
|
||||
company != null) {
|
||||
|
||||
@@ -2,13 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
// 라이센스 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseFormController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
late final LicenseService _licenseService;
|
||||
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
bool _isEditMode = false;
|
||||
@@ -59,15 +56,9 @@ class LicenseFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
LicenseFormController({
|
||||
this.useApi = false,
|
||||
MockDataService? dataService,
|
||||
int? licenseId,
|
||||
bool isExtension = false,
|
||||
}) : mockDataService = dataService ?? MockDataService() {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
}
|
||||
|
||||
}) {
|
||||
if (licenseId != null && !isExtension) {
|
||||
_licenseId = licenseId;
|
||||
_isEditMode = true;
|
||||
@@ -122,13 +113,8 @@ class LicenseFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
debugPrint('📝 API에서 라이센스 로드 중...');
|
||||
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
} else {
|
||||
debugPrint('📝 Mock에서 라이센스 로드 중...');
|
||||
_originalLicense = mockDataService?.getLicenseById(_licenseId!);
|
||||
}
|
||||
debugPrint('📝 API에서 라이센스 로드 중...');
|
||||
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
|
||||
debugPrint('📝 로드된 라이센스: $_originalLicense');
|
||||
|
||||
@@ -182,14 +168,8 @@ class LicenseFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
License? sourceLicense;
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
||||
sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
} else {
|
||||
debugPrint('📝 Mock에서 라이센스 로드 중 (연장용)...');
|
||||
sourceLicense = mockDataService?.getLicenseById(_licenseId!);
|
||||
}
|
||||
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
||||
final sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
|
||||
debugPrint('📝 로드된 소스 라이센스: $sourceLicense');
|
||||
|
||||
@@ -263,18 +243,10 @@ class LicenseFormController extends ChangeNotifier {
|
||||
remark: '${_durationMonths}개월,${_visitCycle},방문',
|
||||
);
|
||||
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
if (_isEditMode) {
|
||||
await _licenseService.updateLicense(license);
|
||||
} else {
|
||||
await _licenseService.createLicense(license);
|
||||
}
|
||||
if (_isEditMode) {
|
||||
await _licenseService.updateLicense(license);
|
||||
} else {
|
||||
if (_isEditMode) {
|
||||
mockDataService?.updateLicense(license);
|
||||
} else {
|
||||
mockDataService?.addLicense(license);
|
||||
}
|
||||
await _licenseService.createLicense(license);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
|
||||
// 라이센스 상태 필터
|
||||
enum LicenseStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
expiringSoon, // 30일 이내
|
||||
expired,
|
||||
}
|
||||
|
||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseListController extends ChangeNotifier {
|
||||
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||
|
||||
List<License> _licenses = [];
|
||||
List<License> _filteredLicenses = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
int? _selectedCompanyId;
|
||||
bool? _isActive;
|
||||
String? _licenseType;
|
||||
LicenseStatusFilter _statusFilter = LicenseStatusFilter.all;
|
||||
String _sortBy = 'expiry_date';
|
||||
String _sortOrder = 'asc';
|
||||
|
||||
// 선택된 라이선스 관리
|
||||
final Set<int> _selectedLicenseIds = {};
|
||||
|
||||
// 통계 데이터
|
||||
Map<String, int> _statistics = {
|
||||
'total': 0,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
|
||||
// 검색 디바운스를 위한 타이머
|
||||
Timer? _debounceTimer;
|
||||
|
||||
LicenseListController();
|
||||
|
||||
// Getters
|
||||
List<License> get licenses => _filteredLicenses;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
bool? get isActive => _isActive;
|
||||
String? get licenseType => _licenseType;
|
||||
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
Map<String, int> get statistics => _statistics;
|
||||
|
||||
// 선택된 라이선스 개수
|
||||
int get selectedCount => _selectedLicenseIds.length;
|
||||
|
||||
// 전체 선택 여부 확인
|
||||
bool get isAllSelected =>
|
||||
_filteredLicenses.isNotEmpty &&
|
||||
_filteredLicenses.where((l) => l.id != null)
|
||||
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||
|
||||
// 데이터 로드
|
||||
Future<void> loadData({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔧 유지보수 목록 API 호출 시작');
|
||||
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLicenses = await _licenseService.getLicenses(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 유지보수 목록 로드 완료');
|
||||
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
int activeCount = 0;
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
final now = DateTime.now();
|
||||
|
||||
for (final license in fetchedLicenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntil = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntil < 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntil <= 30) {
|
||||
expiringSoonCount++;
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
print('║ • 활성: $activeCount개');
|
||||
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
||||
print('║ • 만료됨: $expiredCount개');
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_licenses = fetchedLicenses;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLicenses.length;
|
||||
|
||||
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
||||
_applySearchFilter();
|
||||
_applyStatusFilter();
|
||||
await _updateStatistics();
|
||||
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
} catch (e) {
|
||||
debugPrint('❌ loadData 에러 발생: $e');
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
_currentPage++;
|
||||
await loadData(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색 (디바운싱 적용)
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
|
||||
// 기존 타이머 취소
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// API 검색은 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
||||
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLicenses = List.from(_licenses);
|
||||
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
||||
} else {
|
||||
_filteredLicenses = _licenses.where((license) {
|
||||
final productName = license.productName?.toLowerCase() ?? '';
|
||||
final licenseKey = license.licenseKey.toLowerCase();
|
||||
final vendor = license.vendor?.toLowerCase() ?? '';
|
||||
final companyName = license.companyName?.toLowerCase() ?? '';
|
||||
final searchLower = _searchQuery.toLowerCase();
|
||||
|
||||
return productName.contains(searchLower) ||
|
||||
licenseKey.contains(searchLower) ||
|
||||
vendor.contains(searchLower) ||
|
||||
companyName.contains(searchLower);
|
||||
}).toList();
|
||||
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 적용
|
||||
void _applyStatusFilter() {
|
||||
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
_filteredLicenses = _filteredLicenses.where((license) {
|
||||
switch (_statusFilter) {
|
||||
case LicenseStatusFilter.active:
|
||||
return license.isActive;
|
||||
case LicenseStatusFilter.inactive:
|
||||
return !license.isActive;
|
||||
case LicenseStatusFilter.expiringSoon:
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
return days > 0 && days <= 30;
|
||||
}
|
||||
return false;
|
||||
case LicenseStatusFilter.expired:
|
||||
if (license.expiryDate != null) {
|
||||
return license.expiryDate!.isBefore(now);
|
||||
}
|
||||
return false;
|
||||
case LicenseStatusFilter.all:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
bool? isActive,
|
||||
String? licenseType,
|
||||
}) {
|
||||
_selectedCompanyId = companyId;
|
||||
_isActive = isActive;
|
||||
_licenseType = licenseType;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_selectedCompanyId = null;
|
||||
_isActive = null;
|
||||
_licenseType = null;
|
||||
_searchQuery = '';
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 라이센스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
await _licenseService.deleteLicense(id);
|
||||
|
||||
// 목록에서 제거
|
||||
_licenses.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 만료 예정 라이선스 조회
|
||||
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
||||
try {
|
||||
return await _licenseService.getExpiringLicenses(days: days);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 상태별 라이선스 개수 조회
|
||||
Future<Map<String, int>> getLicenseStatusCounts() async {
|
||||
try {
|
||||
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
||||
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
||||
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
||||
final expiringLicenses = await getExpiringLicenses(days: 30);
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': inactiveCount,
|
||||
'expiring': expiringLicenses.length,
|
||||
'total': activeCount + inactiveCount,
|
||||
};
|
||||
} catch (e) {
|
||||
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬 변경
|
||||
void sortBy(String field, String order) {
|
||||
_sortBy = field;
|
||||
_sortOrder = order;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 라이선스 선택/해제
|
||||
void selectLicense(int? id, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
if (isSelected == true) {
|
||||
_selectedLicenseIds.add(id);
|
||||
} else {
|
||||
_selectedLicenseIds.remove(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
void selectAll(bool? isSelected) {
|
||||
if (isSelected == true) {
|
||||
// 현재 필터링된 라이선스 모두 선택
|
||||
for (var license in _filteredLicenses) {
|
||||
if (license.id != null) {
|
||||
_selectedLicenseIds.add(license.id!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 모두 해제
|
||||
_selectedLicenseIds.clear();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 라이선스 목록 반환
|
||||
List<License> getSelectedLicenses() {
|
||||
return _filteredLicenses
|
||||
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 선택 초기화
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 라이선스 할당
|
||||
Future<bool> assignLicense(int licenseId, int userId) async {
|
||||
try {
|
||||
await _licenseService.assignLicense(licenseId, userId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 라이선스 할당 해제
|
||||
Future<bool> unassignLicense(int licenseId) async {
|
||||
try {
|
||||
await _licenseService.unassignLicense(licenseId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
final selectedIds = List<int>.from(_selectedLicenseIds);
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (var id in selectedIds) {
|
||||
try {
|
||||
await deleteLicense(id);
|
||||
successCount++;
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
debugPrint('라이선스 $id 삭제 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await loadData();
|
||||
|
||||
if (successCount > 0) {
|
||||
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
||||
}
|
||||
if (failCount > 0) {
|
||||
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
Future<void> _updateStatistics() async {
|
||||
try {
|
||||
final counts = await getLicenseStatusCounts();
|
||||
|
||||
final now = DateTime.now();
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
|
||||
for (var license in _licenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
if (days <= 0) {
|
||||
expiredCount++;
|
||||
} else if (days <= 30) {
|
||||
expiringSoonCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_statistics = {
|
||||
'total': counts['total'] ?? 0,
|
||||
'active': counts['active'] ?? 0,
|
||||
'inactive': counts['inactive'] ?? 0,
|
||||
'expiringSoon': expiringSoonCount,
|
||||
'expired': expiredCount,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ 통계 업데이트 오류: $e');
|
||||
// 오류 발생 시 기본값 사용
|
||||
_statistics = {
|
||||
'total': _licenses.length,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 만료일까지 남은 일수 계산
|
||||
int? getDaysUntilExpiry(License license) {
|
||||
if (license.expiryDate == null) return null;
|
||||
return license.expiryDate!.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
// 라이센스 상태 필터
|
||||
/// 라이센스 상태 필터
|
||||
enum LicenseStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
expiringSoon, // 30일 이내
|
||||
expiringSoon, // ${AppConstants.licenseExpiryWarningDays}일 이내
|
||||
expired,
|
||||
}
|
||||
|
||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseListController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
/// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class LicenseListController extends BaseListController<License> {
|
||||
late final LicenseService _licenseService;
|
||||
|
||||
List<License> _licenses = [];
|
||||
List<License> _filteredLicenses = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
// 라이선스 특화 필터 상태
|
||||
int? _selectedCompanyId;
|
||||
bool? _isActive;
|
||||
String? _licenseType;
|
||||
@@ -54,207 +45,112 @@ class LicenseListController extends ChangeNotifier {
|
||||
// 검색 디바운스를 위한 타이머
|
||||
Timer? _debounceTimer;
|
||||
|
||||
LicenseListController({this.useApi = false, this.mockDataService}) {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<License> get licenses => _filteredLicenses;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
// Getters for license-specific properties
|
||||
List<License> get licenses => items;
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
bool? get isActive => _isActive;
|
||||
String? get licenseType => _licenseType;
|
||||
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
Map<String, int> get statistics => _statistics;
|
||||
|
||||
// 선택된 라이선스 개수
|
||||
int get selectedCount => _selectedLicenseIds.length;
|
||||
|
||||
// 전체 선택 여부 확인
|
||||
bool get isAllSelected =>
|
||||
_filteredLicenses.isNotEmpty &&
|
||||
_filteredLicenses.where((l) => l.id != null)
|
||||
items.isNotEmpty &&
|
||||
items.where((l) => l.id != null)
|
||||
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||
|
||||
// 데이터 로드
|
||||
Future<void> loadData({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔧 유지보수 목록 API 호출 시작');
|
||||
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLicenses = await _licenseService.getLicenses(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 유지보수 목록 로드 완료');
|
||||
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
int activeCount = 0;
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
final now = DateTime.now();
|
||||
|
||||
for (final license in fetchedLicenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntil = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntil < 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntil <= 30) {
|
||||
expiringSoonCount++;
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
print('║ • 활성: $activeCount개');
|
||||
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
||||
print('║ • 만료됨: $expiredCount개');
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_licenses = fetchedLicenses;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLicenses.length;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
|
||||
// 필터링 적용
|
||||
var filtered = allLicenses;
|
||||
if (_selectedCompanyId != null) {
|
||||
filtered = filtered.where((l) => l.companyId == _selectedCompanyId).toList();
|
||||
}
|
||||
|
||||
// 페이지네이션 적용
|
||||
final startIndex = (_currentPage - 1) * _pageSize;
|
||||
final endIndex = startIndex + _pageSize;
|
||||
|
||||
if (startIndex < filtered.length) {
|
||||
final pageLicenses = filtered.sublist(
|
||||
startIndex,
|
||||
endIndex > filtered.length ? filtered.length : endIndex,
|
||||
);
|
||||
|
||||
if (isInitialLoad) {
|
||||
_licenses = pageLicenses;
|
||||
} else {
|
||||
_licenses.addAll(pageLicenses);
|
||||
}
|
||||
|
||||
_hasMore = endIndex < filtered.length;
|
||||
} else {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
_total = filtered.length;
|
||||
}
|
||||
|
||||
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
||||
_applySearchFilter();
|
||||
_applyStatusFilter();
|
||||
await _updateStatistics();
|
||||
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
} catch (e) {
|
||||
debugPrint('❌ loadData 에러 발생: $e');
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
notifyListeners();
|
||||
LicenseListController() {
|
||||
if (GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
} else {
|
||||
throw Exception('LicenseService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
_currentPage++;
|
||||
await loadData(isInitialLoad: false);
|
||||
@override
|
||||
Future<PagedResult<License>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final fetchedLicenses = await ErrorHandler.handleApiCall(
|
||||
() => _licenseService.getLicenses(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
if (fetchedLicenses == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
await _updateStatistics(fetchedLicenses);
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: fetchedLicenses.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + fetchedLicenses.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: fetchedLicenses.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: fetchedLicenses.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: fetchedLicenses, meta: meta);
|
||||
}
|
||||
|
||||
// 검색 (디바운싱 적용)
|
||||
@override
|
||||
bool filterItem(License item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return (item.productName?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.licenseKey.toLowerCase().contains(q)) ||
|
||||
(item.vendor?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.companyName?.toLowerCase().contains(q) ?? false);
|
||||
}
|
||||
|
||||
/// BaseListController의 검색을 오버라이드하여 디바운싱 적용
|
||||
@override
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
|
||||
// 기존 타이머 취소
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// Mock 데이터는 즉시 검색
|
||||
if (!useApi) {
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// API 검색은 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
loadData();
|
||||
// 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(AppConstants.licenseSearchDebounce, () {
|
||||
super.search(query);
|
||||
_applyStatusFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
||||
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLicenses = List.from(_licenses);
|
||||
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
||||
} else {
|
||||
_filteredLicenses = _licenses.where((license) {
|
||||
final productName = license.productName?.toLowerCase() ?? '';
|
||||
final licenseKey = license.licenseKey.toLowerCase();
|
||||
final vendor = license.vendor?.toLowerCase() ?? '';
|
||||
final companyName = license.companyName?.toLowerCase() ?? '';
|
||||
final searchLower = _searchQuery.toLowerCase();
|
||||
|
||||
return productName.contains(searchLower) ||
|
||||
licenseKey.contains(searchLower) ||
|
||||
vendor.contains(searchLower) ||
|
||||
companyName.contains(searchLower);
|
||||
}).toList();
|
||||
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 적용
|
||||
/// 상태 필터 적용 (BaseListController의 filtering과 추가로 동작)
|
||||
void _applyStatusFilter() {
|
||||
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
_filteredLicenses = _filteredLicenses.where((license) {
|
||||
final currentItems = List<License>.from(items);
|
||||
|
||||
// 상태 필터 적용
|
||||
final filteredByStatus = currentItems.where((license) {
|
||||
switch (_statusFilter) {
|
||||
case LicenseStatusFilter.active:
|
||||
return license.isActive;
|
||||
@@ -276,9 +172,13 @@ class LicenseListController extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// 직접 필터링된 결과를 적용 (BaseListController의 private 필드에 접근할 수 없으므로)
|
||||
// 대신 notifyListeners를 통해 UI 업데이트
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
bool? isActive,
|
||||
@@ -287,135 +187,48 @@ class LicenseListController extends ChangeNotifier {
|
||||
_selectedCompanyId = companyId;
|
||||
_isActive = isActive;
|
||||
_licenseType = licenseType;
|
||||
loadData();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_selectedCompanyId = null;
|
||||
_isActive = null;
|
||||
_licenseType = null;
|
||||
_searchQuery = '';
|
||||
loadData();
|
||||
_statusFilter = LicenseStatusFilter.all;
|
||||
search(''); // BaseListController의 search 호출
|
||||
}
|
||||
|
||||
// 라이센스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.deleteLicense(id);
|
||||
} else {
|
||||
mockDataService?.deleteLicense(id);
|
||||
}
|
||||
|
||||
// 목록에서 제거
|
||||
_licenses.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
/// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
_applyStatusFilter();
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 만료 예정 라이선스 조회
|
||||
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
return await _licenseService.getExpiringLicenses(days: days);
|
||||
} else {
|
||||
// Mock 데이터에서 만료 예정 라이선스 필터링
|
||||
final now = DateTime.now();
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
|
||||
return allLicenses.where((license) {
|
||||
// 실제 License 모델에서 만료일 확인
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
||||
return daysUntilExpiry > 0 && daysUntilExpiry <= days;
|
||||
}
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 상태별 라이선스 개수 조회
|
||||
Future<Map<String, int>> getLicenseStatusCounts() async {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
try {
|
||||
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
||||
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
||||
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
||||
final expiringLicenses = await getExpiringLicenses(days: 30);
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': inactiveCount,
|
||||
'expiring': expiringLicenses.length,
|
||||
'total': activeCount + inactiveCount,
|
||||
};
|
||||
} catch (e) {
|
||||
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터에서 계산
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
final now = DateTime.now();
|
||||
|
||||
int activeCount = 0;
|
||||
int expiredCount = 0;
|
||||
int expiringCount = 0;
|
||||
|
||||
for (var license in allLicenses) {
|
||||
if (license.isActive) {
|
||||
activeCount++;
|
||||
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntilExpiry <= 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntilExpiry <= 30) {
|
||||
expiringCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': allLicenses.length - activeCount,
|
||||
'expiring': expiringCount,
|
||||
'expired': expiredCount,
|
||||
'total': allLicenses.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬 변경
|
||||
/// 정렬 변경
|
||||
void sortBy(String field, String order) {
|
||||
_sortBy = field;
|
||||
_sortOrder = order;
|
||||
loadData();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
await loadData();
|
||||
|
||||
/// 라이선스 삭제 (BaseListController의 기본 기능 활용)
|
||||
Future<void> deleteLicense(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.deleteLicense(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// BaseListController의 removeItemLocally 활용
|
||||
removeItemLocally((l) => l.id == id);
|
||||
|
||||
// 선택 목록에서도 제거
|
||||
_selectedLicenseIds.remove(id);
|
||||
}
|
||||
|
||||
// 라이선스 선택/해제
|
||||
|
||||
/// 라이선스 선택/해제
|
||||
void selectLicense(int? id, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
@@ -427,11 +240,11 @@ class LicenseListController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
/// 전체 선택/해제
|
||||
void selectAll(bool? isSelected) {
|
||||
if (isSelected == true) {
|
||||
// 현재 필터링된 라이선스 모두 선택
|
||||
for (var license in _filteredLicenses) {
|
||||
for (var license in items) {
|
||||
if (license.id != null) {
|
||||
_selectedLicenseIds.add(license.id!);
|
||||
}
|
||||
@@ -443,131 +256,126 @@ class LicenseListController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 라이선스 목록 반환
|
||||
/// 선택된 라이선스 목록 반환
|
||||
List<License> getSelectedLicenses() {
|
||||
return _filteredLicenses
|
||||
return items
|
||||
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 선택 초기화
|
||||
/// 선택 초기화 (BaseListController에도 있지만 라이선스 특화)
|
||||
@override
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 라이선스 할당
|
||||
Future<bool> assignLicense(int licenseId, int userId) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.assignLicense(licenseId, userId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 라이선스 할당 해제
|
||||
Future<bool> unassignLicense(int licenseId) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.unassignLicense(licenseId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 라이선스 일괄 삭제
|
||||
|
||||
/// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
final selectedIds = List<int>.from(_selectedLicenseIds);
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (var id in selectedIds) {
|
||||
try {
|
||||
await deleteLicense(id);
|
||||
successCount++;
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
debugPrint('라이선스 $id 삭제 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await loadData();
|
||||
|
||||
if (successCount > 0) {
|
||||
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
||||
}
|
||||
if (failCount > 0) {
|
||||
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
||||
for (final id in _selectedLicenseIds.toList()) {
|
||||
await deleteLicense(id);
|
||||
}
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
Future<void> _updateStatistics() async {
|
||||
try {
|
||||
final counts = await getLicenseStatusCounts();
|
||||
|
||||
final now = DateTime.now();
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
|
||||
for (var license in _licenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
if (days <= 0) {
|
||||
expiredCount++;
|
||||
} else if (days <= 30) {
|
||||
expiringSoonCount++;
|
||||
}
|
||||
|
||||
/// 라이선스 생성
|
||||
Future<void> createLicense(License license) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.createLicense(license),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 라이선스 수정
|
||||
Future<void> updateLicense(License license) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.updateLicense(license),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(license, (l) => l.id == license.id);
|
||||
}
|
||||
|
||||
/// 라이선스 활성화/비활성화 토글
|
||||
Future<void> toggleLicenseStatus(int id) async {
|
||||
final license = items.firstWhere((l) => l.id == id);
|
||||
final updatedLicense = license.copyWith(isActive: !license.isActive);
|
||||
|
||||
await updateLicense(updatedLicense);
|
||||
}
|
||||
|
||||
/// 통계 데이터 업데이트
|
||||
Future<void> _updateStatistics(List<License> licenses) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
_statistics = {
|
||||
'total': licenses.length,
|
||||
'active': licenses.where((l) => l.isActive).length,
|
||||
'inactive': licenses.where((l) => !l.isActive).length,
|
||||
'expiringSoon': licenses.where((l) {
|
||||
if (l.expiryDate != null) {
|
||||
final days = l.expiryDate!.difference(now).inDays;
|
||||
return days > 0 && days <= 30;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}).length,
|
||||
'expired': licenses.where((l) {
|
||||
if (l.expiryDate != null) {
|
||||
return l.expiryDate!.isBefore(now);
|
||||
}
|
||||
return false;
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
/// 라이선스 만료일별 그룹핑
|
||||
Map<String, List<License>> getLicensesByExpiryPeriod() {
|
||||
final now = DateTime.now();
|
||||
final Map<String, List<License>> grouped = {
|
||||
'이미 만료': [],
|
||||
'${AppConstants.licenseExpiryWarningDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryCautionDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryInfoDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryInfoDays}일 이후': [],
|
||||
};
|
||||
|
||||
for (final license in items) {
|
||||
if (license.expiryDate == null) continue;
|
||||
|
||||
_statistics = {
|
||||
'total': counts['total'] ?? 0,
|
||||
'active': counts['active'] ?? 0,
|
||||
'inactive': counts['inactive'] ?? 0,
|
||||
'expiringSoon': expiringSoonCount,
|
||||
'expired': expiredCount,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ 통계 업데이트 오류: $e');
|
||||
// 오류 발생 시 기본값 사용
|
||||
_statistics = {
|
||||
'total': _licenses.length,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
|
||||
if (days < 0) {
|
||||
grouped['이미 만료']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryWarningDays) {
|
||||
grouped['${AppConstants.licenseExpiryWarningDays}일 이내']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryCautionDays) {
|
||||
grouped['${AppConstants.licenseExpiryCautionDays}일 이내']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryInfoDays) {
|
||||
grouped['${AppConstants.licenseExpiryInfoDays}일 이내']!.add(license);
|
||||
} else {
|
||||
grouped['${AppConstants.licenseExpiryInfoDays}일 이후']!.add(license);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// 만료일까지 남은 일수 계산
|
||||
int? getDaysUntilExpiry(License license) {
|
||||
if (license.expiryDate == null) return null;
|
||||
return license.expiryDate!.difference(DateTime.now()).inDays;
|
||||
|
||||
/// 만료까지 남은 날짜 계산
|
||||
int getDaysUntilExpiry(DateTime? expiryDate) {
|
||||
if (expiryDate == null) return 999; // 만료일이 없으면 큰 숫자 반환
|
||||
final now = DateTime.now();
|
||||
return expiryDate.difference(now).inDays;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/utils/error_handler.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
import '../../../data/models/license/license_dto.dart';
|
||||
import '../../../domain/usecases/license/license_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 라이선스 목록 컨트롤러
|
||||
class LicenseListControllerWithUseCase extends BaseListController<LicenseDto> {
|
||||
final GetLicensesUseCase getLicensesUseCase;
|
||||
final CreateLicenseUseCase createLicenseUseCase;
|
||||
final UpdateLicenseUseCase updateLicenseUseCase;
|
||||
final DeleteLicenseUseCase deleteLicenseUseCase;
|
||||
final CheckLicenseExpiryUseCase checkLicenseExpiryUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLicenseIds = {};
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
|
||||
// 필터 옵션
|
||||
String? _filterByCompany;
|
||||
String? _filterByExpiry;
|
||||
DateTime? _filterStartDate;
|
||||
DateTime? _filterEndDate;
|
||||
|
||||
String? get filterByCompany => _filterByCompany;
|
||||
String? get filterByExpiry => _filterByExpiry;
|
||||
DateTime? get filterStartDate => _filterStartDate;
|
||||
DateTime? get filterEndDate => _filterEndDate;
|
||||
|
||||
// 만료 임박 라이선스 정보
|
||||
LicenseExpiryResult? _expiryResult;
|
||||
LicenseExpiryResult? get expiryResult => _expiryResult;
|
||||
|
||||
LicenseListControllerWithUseCase({
|
||||
required this.getLicensesUseCase,
|
||||
required this.createLicenseUseCase,
|
||||
required this.updateLicenseUseCase,
|
||||
required this.deleteLicenseUseCase,
|
||||
required this.checkLicenseExpiryUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<LicenseDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_filterByCompany != null) filters['company_id'] = _filterByCompany;
|
||||
if (_filterByExpiry != null) filters['expiry'] = _filterByExpiry;
|
||||
if (_filterStartDate != null) filters['start_date'] = _filterStartDate!.toIso8601String();
|
||||
if (_filterEndDate != null) filters['end_date'] = _filterEndDate!.toIso8601String();
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetLicensesParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getLicensesUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(licenseResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: licenseResponse.items.length, // 실제로는 서버에서 받아와야 함
|
||||
totalPages: (licenseResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: licenseResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: licenseResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 만료 임박 라이선스 체크
|
||||
Future<void> checkExpiringLicenses() async {
|
||||
try {
|
||||
final params = CheckLicenseExpiryParams(
|
||||
companyId: _filterByCompany != null ? int.tryParse(_filterByCompany!) : null,
|
||||
);
|
||||
|
||||
final result = await checkLicenseExpiryUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) => errorState = failure.message,
|
||||
(expiryResult) {
|
||||
_expiryResult = expiryResult;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '라이선스 만료 체크 실패: $e';
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 생성
|
||||
Future<void> createLicense({
|
||||
required int equipmentId,
|
||||
required int companyId,
|
||||
required String licenseType,
|
||||
required DateTime startDate,
|
||||
required DateTime expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateLicenseParams(
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
);
|
||||
|
||||
final result = await createLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
await refresh();
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 수정
|
||||
Future<void> updateLicense({
|
||||
required int id,
|
||||
int? equipmentId,
|
||||
int? companyId,
|
||||
String? licenseType,
|
||||
DateTime? startDate,
|
||||
DateTime? expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
String? status,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateLicenseParams(
|
||||
id: id,
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
status: status,
|
||||
);
|
||||
|
||||
final result = await updateLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
updateItemLocally(license, (item) => item.id == license.id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLicenseIds.remove(id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
String? company,
|
||||
String? expiry,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) {
|
||||
_filterByCompany = company;
|
||||
_filterByExpiry = expiry;
|
||||
_filterStartDate = startDate;
|
||||
_filterEndDate = endDate;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_filterByCompany = null;
|
||||
_filterByExpiry = null;
|
||||
_filterStartDate = null;
|
||||
_filterEndDate = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 라이선스 선택 토글
|
||||
void toggleLicenseSelection(int id) {
|
||||
if (_selectedLicenseIds.contains(id)) {
|
||||
_selectedLicenseIds.remove(id);
|
||||
} else {
|
||||
_selectedLicenseIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 라이선스 선택
|
||||
void selectAll() {
|
||||
_selectedLicenseIds.clear();
|
||||
_selectedLicenseIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLicenseIds.toList()) {
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
result.fold(
|
||||
(failure) => print('Failed to delete license $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await checkExpiringLicenses();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLicenseIds.clear();
|
||||
_expiryResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:superport/screens/license/controllers/license_form_controller.da
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:superport/core/config/environment.dart' as env;
|
||||
@@ -51,8 +50,6 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
|
||||
debugPrint('📌 라이선스 폼 초기화 - API 모드: $useApi');
|
||||
|
||||
_controller = LicenseFormController(
|
||||
useApi: useApi,
|
||||
dataService: useApi ? null : MockDataService(),
|
||||
licenseId: widget.maintenanceId,
|
||||
isExtension: widget.isExtension,
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/license/controllers/license_list_controller.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/core/config/environment.dart' as env;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -25,7 +24,7 @@ class LicenseListRedesign extends StatefulWidget {
|
||||
|
||||
class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
late final LicenseListController _controller;
|
||||
final MockDataService _dataService = MockDataService();
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _horizontalScrollController = ScrollController();
|
||||
int _currentPage = 1;
|
||||
@@ -45,10 +44,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
|
||||
// 실제 API 사용 여부에 따라 컨트롤러 초기화
|
||||
final useApi = env.Environment.useApi;
|
||||
_controller = LicenseListController(
|
||||
useApi: useApi,
|
||||
mockDataService: useApi ? null : _dataService,
|
||||
);
|
||||
_controller = LicenseListController();
|
||||
|
||||
debugPrint('📌 Controller 모드: ${useApi ? "Real API" : "Mock Data"}');
|
||||
debugPrint('==========================================\n');
|
||||
@@ -611,7 +607,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
final displayIndex = entry.key;
|
||||
final license = entry.value;
|
||||
final index = (_currentPage - 1) * _pageSize + displayIndex;
|
||||
final daysRemaining = _controller.getDaysUntilExpiry(license);
|
||||
final daysRemaining = _controller.getDaysUntilExpiry(license.expiryDate);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
||||
193
lib/screens/login/controllers/login_controller_with_usecase.dart
Normal file
193
lib/screens/login/controllers/login_controller_with_usecase.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
import '../../../domain/usecases/base_usecase.dart';
|
||||
import '../../../domain/usecases/auth/login_usecase.dart';
|
||||
import '../../../domain/usecases/auth/check_auth_status_usecase.dart';
|
||||
import '../../../services/auth_service.dart';
|
||||
import '../../../services/health_check_service.dart';
|
||||
import '../../../di/injection_container.dart';
|
||||
|
||||
/// UseCase를 활용한 로그인 화면 컨트롤러
|
||||
/// 비즈니스 로직을 UseCase로 분리하여 테스트 용이성과 재사용성 향상
|
||||
class LoginControllerWithUseCase extends ChangeNotifier {
|
||||
// UseCases
|
||||
late final LoginUseCase _loginUseCase;
|
||||
late final CheckAuthStatusUseCase _checkAuthStatusUseCase;
|
||||
|
||||
// Services
|
||||
final HealthCheckService _healthCheckService = HealthCheckService();
|
||||
|
||||
// UI Controllers
|
||||
final TextEditingController idController = TextEditingController();
|
||||
final TextEditingController pwController = TextEditingController();
|
||||
|
||||
// Focus Nodes
|
||||
final FocusNode idFocus = FocusNode();
|
||||
final FocusNode pwFocus = FocusNode();
|
||||
|
||||
// State
|
||||
bool saveId = false;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
LoginControllerWithUseCase() {
|
||||
// UseCase 초기화
|
||||
final authService = inject<AuthService>();
|
||||
_loginUseCase = LoginUseCase(authService);
|
||||
_checkAuthStatusUseCase = CheckAuthStatusUseCase(authService);
|
||||
|
||||
// 초기 인증 상태 확인
|
||||
_checkInitialAuthStatus();
|
||||
}
|
||||
|
||||
/// 초기 인증 상태 확인
|
||||
Future<void> _checkInitialAuthStatus() async {
|
||||
final result = await _checkAuthStatusUseCase(const NoParams());
|
||||
result.fold(
|
||||
(failure) => debugPrint('인증 상태 확인 실패: ${failure.message}'),
|
||||
(isAuthenticated) {
|
||||
if (isAuthenticated) {
|
||||
debugPrint('이미 로그인된 상태입니다.');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 아이디 저장 체크박스 상태 변경
|
||||
void setSaveId(bool value) {
|
||||
saveId = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 에러 메시지 초기화
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 로그인 처리
|
||||
Future<bool> login() async {
|
||||
// 입력값 검증 (UI 레벨)
|
||||
if (idController.text.trim().isEmpty) {
|
||||
_errorMessage = '아이디 또는 이메일을 입력해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pwController.text.isEmpty) {
|
||||
_errorMessage = '비밀번호를 입력해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 로딩 시작
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
// 입력값이 이메일인지 username인지 판단
|
||||
final inputValue = idController.text.trim();
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
final isEmail = emailRegex.hasMatch(inputValue);
|
||||
|
||||
try {
|
||||
// UseCase 실행
|
||||
final params = LoginParams(
|
||||
email: isEmail ? inputValue : '$inputValue@superport.kr', // username인 경우 임시 도메인 추가
|
||||
password: pwController.text,
|
||||
);
|
||||
|
||||
debugPrint('[LoginController] 로그인 시도: ${params.email}');
|
||||
|
||||
final result = await _loginUseCase(params).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () async {
|
||||
debugPrint('[LoginController] 로그인 요청 타임아웃');
|
||||
return Left(NetworkFailure(
|
||||
message: '요청 시간이 초과되었습니다. 네트워크 연결을 확인해주세요.',
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
debugPrint('[LoginController] 로그인 실패: ${failure.message}');
|
||||
|
||||
// 실패 타입에 따른 메시지 처리
|
||||
if (failure is ValidationFailure) {
|
||||
_errorMessage = failure.message;
|
||||
} else if (failure is AuthenticationFailure) {
|
||||
_errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
|
||||
} else if (failure is NetworkFailure) {
|
||||
_errorMessage = '네트워크 연결을 확인해주세요.';
|
||||
} else if (failure is ServerFailure) {
|
||||
_errorMessage = '서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.';
|
||||
} else {
|
||||
_errorMessage = failure.message;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(loginResponse) {
|
||||
debugPrint('[LoginController] 로그인 성공');
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[LoginController] 예상치 못한 에러: $e');
|
||||
_errorMessage = '로그인 중 오류가 발생했습니다.';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 헬스체크 실행
|
||||
Future<bool> performHealthCheck() async {
|
||||
debugPrint('[LoginController] 헬스체크 시작');
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final healthResult = await _healthCheckService.checkHealth();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
// HealthCheckService가 Map을 반환하는 경우 적절히 변환
|
||||
final isHealthy = healthResult is bool ? healthResult :
|
||||
(healthResult is Map && healthResult['status'] == 'healthy');
|
||||
|
||||
if (isHealthy == false) {
|
||||
_errorMessage = '서버와 연결할 수 없습니다.\n잠시 후 다시 시도해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[LoginController] 헬스체크 실패: $e');
|
||||
_errorMessage = '서버 상태 확인 중 오류가 발생했습니다.';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
idController.dispose();
|
||||
pwController.dispose();
|
||||
idFocus.dispose();
|
||||
pwFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import 'package:intl/intl.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/overview/controllers/overview_controller.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/services/health_check_service.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 대시보드 화면
|
||||
class OverviewScreenRedesign extends StatefulWidget {
|
||||
const OverviewScreenRedesign({Key? key}) : super(key: key);
|
||||
const OverviewScreenRedesign({super.key});
|
||||
|
||||
@override
|
||||
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
|
||||
@@ -16,21 +20,44 @@ class OverviewScreenRedesign extends StatefulWidget {
|
||||
|
||||
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
late final OverviewController _controller;
|
||||
late final HealthCheckService _healthCheckService;
|
||||
Map<String, dynamic>? _healthStatus;
|
||||
bool _isHealthCheckLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = OverviewController();
|
||||
_healthCheckService = HealthCheckService();
|
||||
_loadData();
|
||||
_checkHealthStatus();
|
||||
// 주기적인 헬스체크 시작 (30초마다)
|
||||
_healthCheckService.startPeriodicHealthCheck();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await _controller.loadDashboardData();
|
||||
}
|
||||
|
||||
Future<void> _checkHealthStatus() async {
|
||||
setState(() {
|
||||
_isHealthCheckLoading = true;
|
||||
});
|
||||
|
||||
final result = await _healthCheckService.checkHealth();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_healthStatus = result;
|
||||
_isHealthCheckLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_healthCheckService.stopPeriodicHealthCheck();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -70,7 +97,13 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('안녕하세요, 관리자님! 👋', style: ShadcnTheme.headingH3),
|
||||
FutureBuilder<AuthUser?>(
|
||||
future: context.read<AuthService>().getCurrentUser(),
|
||||
builder: (context, snapshot) {
|
||||
final userName = snapshot.data?.name ?? '사용자';
|
||||
return Text('안녕하세요, $userName님! 👋', style: ShadcnTheme.headingH3);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'오늘의 포트 운영 현황을 확인해보세요.',
|
||||
@@ -333,51 +366,79 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
}
|
||||
|
||||
Widget _buildRightColumn() {
|
||||
return Column(
|
||||
children: [
|
||||
// 빠른 작업
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.local_shipping,
|
||||
'장비 출고',
|
||||
'장비 대여 처리',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.business_center,
|
||||
'회사 등록',
|
||||
'새 회사 추가',
|
||||
),
|
||||
],
|
||||
return FutureBuilder<AuthUser?>(
|
||||
future: context.read<AuthService>().getCurrentUser(),
|
||||
builder: (context, snapshot) {
|
||||
final userRole = snapshot.data?.role?.toLowerCase() ?? '';
|
||||
final isAdminOrManager = userRole == 'admin' || userRole == 'manager';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 빠른 작업 (Admin과 Manager만 표시)
|
||||
if (isAdminOrManager) ...[
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.local_shipping,
|
||||
'장비 출고',
|
||||
'장비 대여 처리',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.business_center,
|
||||
'회사 등록',
|
||||
'새 회사 추가',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 시스템 상태
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusItem('서버 상태', '정상'),
|
||||
_buildStatusItem('데이터베이스', '정상'),
|
||||
_buildStatusItem('네트워크', '정상'),
|
||||
_buildStatusItem('백업', '완료'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// 시스템 상태 (실시간 헬스체크)
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
||||
IconButton(
|
||||
icon: _isHealthCheckLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(ShadcnTheme.primary),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.refresh, size: 20, color: ShadcnTheme.muted),
|
||||
onPressed: _isHealthCheckLoading ? null : _checkHealthStatus,
|
||||
tooltip: '새로고침',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHealthStatusItem('서버 상태', _getServerStatus()),
|
||||
_buildHealthStatusItem('데이터베이스', _getDatabaseStatus()),
|
||||
_buildHealthStatusItem('API 응답', _getApiResponseTime()),
|
||||
_buildHealthStatusItem('최종 체크', _getLastCheckTime()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -664,4 +725,151 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 헬스 상태 아이템 빌더
|
||||
Widget _buildHealthStatusItem(String label, Map<String, dynamic> statusInfo) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
Row(
|
||||
children: [
|
||||
if (statusInfo['icon'] != null) ...[
|
||||
Icon(
|
||||
statusInfo['icon'] as IconData,
|
||||
size: 16,
|
||||
color: statusInfo['color'] as Color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
ShadcnBadge(
|
||||
text: statusInfo['text'] as String,
|
||||
variant: statusInfo['variant'] as ShadcnBadgeVariant,
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 서버 상태 가져오기
|
||||
Map<String, dynamic> _getServerStatus() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '확인 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.pending,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final isHealthy = _healthStatus!['success'] == true &&
|
||||
_healthStatus!['data']?['status'] == 'healthy';
|
||||
|
||||
return {
|
||||
'text': isHealthy ? '정상' : '오류',
|
||||
'variant': isHealthy ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.destructive,
|
||||
'icon': isHealthy ? Icons.check_circle : Icons.error,
|
||||
'color': isHealthy ? ShadcnTheme.success : ShadcnTheme.destructive,
|
||||
};
|
||||
}
|
||||
|
||||
/// 데이터베이스 상태 가져오기
|
||||
Map<String, dynamic> _getDatabaseStatus() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '확인 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.pending,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final dbStatus = _healthStatus!['data']?['database']?['status'] ?? 'unknown';
|
||||
final isConnected = dbStatus == 'connected';
|
||||
|
||||
return {
|
||||
'text': isConnected ? '연결됨' : '연결 안됨',
|
||||
'variant': isConnected ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.warning,
|
||||
'icon': isConnected ? Icons.storage : Icons.cloud_off,
|
||||
'color': isConnected ? ShadcnTheme.success : ShadcnTheme.warning,
|
||||
};
|
||||
}
|
||||
|
||||
/// API 응답 시간 가져오기
|
||||
Map<String, dynamic> _getApiResponseTime() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '측정 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.timer,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final responseTime = _healthStatus!['data']?['responseTime'] ?? 0;
|
||||
final timeMs = responseTime is num ? responseTime : 0;
|
||||
|
||||
ShadcnBadgeVariant variant;
|
||||
Color color;
|
||||
if (timeMs < 100) {
|
||||
variant = ShadcnBadgeVariant.success;
|
||||
color = ShadcnTheme.success;
|
||||
} else if (timeMs < 500) {
|
||||
variant = ShadcnBadgeVariant.warning;
|
||||
color = ShadcnTheme.warning;
|
||||
} else {
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
color = ShadcnTheme.destructive;
|
||||
}
|
||||
|
||||
return {
|
||||
'text': '${timeMs}ms',
|
||||
'variant': variant,
|
||||
'icon': Icons.speed,
|
||||
'color': color,
|
||||
};
|
||||
}
|
||||
|
||||
/// 마지막 체크 시간 가져오기
|
||||
Map<String, dynamic> _getLastCheckTime() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '없음',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final timestamp = _healthStatus!['data']?['timestamp'];
|
||||
if (timestamp != null) {
|
||||
try {
|
||||
final date = DateTime.parse(timestamp);
|
||||
final formatter = DateFormat('HH:mm:ss');
|
||||
return {
|
||||
'text': formatter.format(date),
|
||||
'variant': ShadcnBadgeVariant.outline,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.foreground,
|
||||
};
|
||||
} catch (e) {
|
||||
// 파싱 실패
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 시간 사용
|
||||
final now = DateTime.now();
|
||||
final formatter = DateFormat('HH:mm:ss');
|
||||
return {
|
||||
'text': formatter.format(now),
|
||||
'variant': ShadcnBadgeVariant.outline,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.foreground,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/user_service.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/models/user_phone_field.dart';
|
||||
|
||||
// 사용자 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class UserFormController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final UserService _userService = GetIt.instance<UserService>();
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
// 상태 변수
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag
|
||||
// API만 사용
|
||||
|
||||
// 폼 필드
|
||||
bool isEditMode = false;
|
||||
@@ -50,7 +50,7 @@ class UserFormController extends ChangeNotifier {
|
||||
bool get isCheckingUsername => _isCheckingUsername;
|
||||
bool? get isUsernameAvailable => _isUsernameAvailable;
|
||||
|
||||
UserFormController({required this.dataService, this.userId}) {
|
||||
UserFormController({this.userId}) {
|
||||
isEditMode = userId != null;
|
||||
if (isEditMode) {
|
||||
loadUser();
|
||||
@@ -61,15 +61,29 @@ class UserFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 회사 목록 로드
|
||||
void loadCompanies() {
|
||||
companies = dataService.getAllCompanies();
|
||||
notifyListeners();
|
||||
Future<void> loadCompanies() async {
|
||||
try {
|
||||
final result = await _companyService.getCompanies();
|
||||
companies = result;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('회사 목록 로드 실패: $e');
|
||||
companies = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 회사 ID에 따라 지점 목록 로드
|
||||
void loadBranches(int companyId) {
|
||||
final company = dataService.getCompanyById(companyId);
|
||||
branches = company?.branches ?? [];
|
||||
final company = companies.firstWhere(
|
||||
(c) => c.id == companyId,
|
||||
orElse: () => Company(
|
||||
id: companyId,
|
||||
name: '알 수 없는 회사',
|
||||
branches: [],
|
||||
),
|
||||
);
|
||||
branches = company.branches ?? [];
|
||||
// 지점 변경 시 이전 선택 지점이 새 회사에 없으면 초기화
|
||||
if (branchId != null && !branches.any((b) => b.id == branchId)) {
|
||||
branchId = null;
|
||||
@@ -86,13 +100,7 @@ class UserFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
User? user;
|
||||
|
||||
if (_useApi) {
|
||||
user = await _userService.getUser(userId!);
|
||||
} else {
|
||||
user = dataService.getUserById(userId!);
|
||||
}
|
||||
final user = await _userService.getUser(userId!);
|
||||
|
||||
if (user != null) {
|
||||
name = user.name;
|
||||
@@ -155,15 +163,8 @@ class UserFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
||||
_isUsernameAvailable = !isDuplicate;
|
||||
} else {
|
||||
// Mock 데이터에서 중복 확인
|
||||
final users = dataService.getAllUsers();
|
||||
final exists = users.any((u) => u.username == value && u.id != userId);
|
||||
_isUsernameAvailable = !exists;
|
||||
}
|
||||
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
||||
_isUsernameAvailable = !isDuplicate;
|
||||
_lastCheckedUsername = value;
|
||||
} catch (e) {
|
||||
_isUsernameAvailable = null;
|
||||
@@ -217,81 +218,32 @@ class UserFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (_useApi) {
|
||||
if (isEditMode && userId != null) {
|
||||
// 사용자 수정
|
||||
await _userService.updateUser(
|
||||
userId!,
|
||||
name: name,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phone: phoneNumber,
|
||||
companyId: companyId,
|
||||
branchId: branchId,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
password: password.isNotEmpty ? password : null,
|
||||
);
|
||||
} else {
|
||||
// 사용자 생성
|
||||
await _userService.createUser(
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
name: name,
|
||||
role: role,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
phone: phoneNumber,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
);
|
||||
}
|
||||
if (isEditMode && userId != null) {
|
||||
// 사용자 수정
|
||||
await _userService.updateUser(
|
||||
userId!,
|
||||
name: name,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phone: phoneNumber,
|
||||
companyId: companyId,
|
||||
branchId: branchId,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
password: password.isNotEmpty ? password : null,
|
||||
);
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
List<Map<String, String>> phoneNumbersList = [];
|
||||
for (var phoneField in phoneFields) {
|
||||
if (phoneField.number.isNotEmpty) {
|
||||
phoneNumbersList.add({
|
||||
'type': phoneField.type,
|
||||
'number': phoneField.number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditMode && userId != null) {
|
||||
final user = dataService.getUserById(userId!);
|
||||
if (user != null) {
|
||||
final updatedUser = User(
|
||||
id: user.id,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
username: username.isNotEmpty ? username : null,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
dataService.updateUser(updatedUser);
|
||||
}
|
||||
} else {
|
||||
final newUser = User(
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
username: username,
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
dataService.addUser(newUser);
|
||||
}
|
||||
// 사용자 생성
|
||||
await _userService.createUser(
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
name: name,
|
||||
role: role,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
phone: phoneNumber,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
);
|
||||
}
|
||||
|
||||
onResult(null);
|
||||
@@ -314,9 +266,6 @@ class UserFormController extends ChangeNotifier {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// API/Mock 모드 전환
|
||||
void toggleApiMode() {
|
||||
_useApi = !_useApi;
|
||||
notifyListeners();
|
||||
}
|
||||
// API 모드만 사용 (Mock 데이터 제거됨)
|
||||
// void toggleApiMode() 메서드 제거
|
||||
}
|
||||
|
||||
172
lib/screens/user/controllers/user_list_controller.backup.dart
Normal file
172
lib/screens/user/controllers/user_list_controller.backup.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/user_service.dart';
|
||||
|
||||
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class UserListController extends ChangeNotifier {
|
||||
final UserService _userService = GetIt.instance<UserService>();
|
||||
|
||||
// 상태 변수
|
||||
List<User> _users = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
// API만 사용
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMoreData = true;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
// 검색/필터
|
||||
String _searchQuery = '';
|
||||
int? _filterCompanyId;
|
||||
String? _filterRole;
|
||||
bool? _filterIsActive;
|
||||
|
||||
// Getters
|
||||
List<User> get users => _users;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
String? get error => _error;
|
||||
bool get hasMoreData => _hasMoreData;
|
||||
String get searchQuery => _searchQuery;
|
||||
int? get filterCompanyId => _filterCompanyId;
|
||||
String? get filterRole => _filterRole;
|
||||
bool? get filterIsActive => _filterIsActive;
|
||||
|
||||
UserListController();
|
||||
|
||||
/// 사용자 목록 초기 로드
|
||||
Future<void> loadUsers({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
_users.clear();
|
||||
}
|
||||
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final newUsers = await _userService.getUsers(
|
||||
page: _currentPage,
|
||||
perPage: _perPage,
|
||||
isActive: _filterIsActive,
|
||||
companyId: _filterCompanyId,
|
||||
role: _filterRole,
|
||||
);
|
||||
|
||||
if (newUsers.isEmpty || newUsers.length < _perPage) {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
|
||||
if (_currentPage == 1) {
|
||||
_users = newUsers;
|
||||
} else {
|
||||
_users.addAll(newUsers);
|
||||
}
|
||||
|
||||
_currentPage++;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 다음 페이지 로드 (무한 스크롤용)
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMoreData || _isLoadingMore || _isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await loadUsers();
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 쿼리 설정
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
String? role,
|
||||
bool? isActive,
|
||||
}) {
|
||||
_filterCompanyId = companyId;
|
||||
_filterRole = role;
|
||||
_filterIsActive = isActive;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_filterCompanyId = null;
|
||||
_filterRole = null;
|
||||
_filterIsActive = null;
|
||||
_searchQuery = '';
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 사용자 삭제
|
||||
Future<void> deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async {
|
||||
try {
|
||||
await _userService.deleteUser(id);
|
||||
|
||||
// 목록에서 삭제된 사용자 제거
|
||||
_users.removeWhere((user) => user.id == id);
|
||||
notifyListeners();
|
||||
|
||||
onDeleted();
|
||||
} catch (e) {
|
||||
onError('사용자 삭제 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 상태 변경 (활성/비활성)
|
||||
Future<void> changeUserStatus(int id, bool isActive, Function(String) onError) async {
|
||||
try {
|
||||
final updatedUser = await _userService.changeUserStatus(id, isActive);
|
||||
// 목록에서 해당 사용자 업데이트
|
||||
final index = _users.indexWhere((u) => u.id == id);
|
||||
if (index != -1) {
|
||||
_users[index] = updatedUser;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
onError('상태 변경 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용
|
||||
|
||||
/// 회사 ID와 지점 ID로 지점명 조회
|
||||
// 지점명 조회는 별도 서비스로 이동 예정
|
||||
String getBranchName(int companyId, int? branchId) {
|
||||
// TODO: API를 통해 지점명 조회
|
||||
return '-';
|
||||
}
|
||||
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
}
|
||||
@@ -1,137 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/user_service.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class UserListController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final UserService _userService = GetIt.instance<UserService>();
|
||||
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class UserListController extends BaseListController<User> {
|
||||
late final UserService _userService;
|
||||
|
||||
// 상태 변수
|
||||
List<User> _users = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMoreData = true;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
// 검색/필터
|
||||
String _searchQuery = '';
|
||||
// 필터 옵션
|
||||
int? _filterCompanyId;
|
||||
String? _filterRole;
|
||||
bool? _filterIsActive;
|
||||
|
||||
// Getters
|
||||
List<User> get users => _users;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
String? get error => _error;
|
||||
bool get hasMoreData => _hasMoreData;
|
||||
String get searchQuery => _searchQuery;
|
||||
List<User> get users => items;
|
||||
int? get filterCompanyId => _filterCompanyId;
|
||||
String? get filterRole => _filterRole;
|
||||
bool? get filterIsActive => _filterIsActive;
|
||||
|
||||
UserListController({required this.dataService});
|
||||
UserListController() {
|
||||
if (GetIt.instance.isRegistered<UserService>()) {
|
||||
_userService = GetIt.instance<UserService>();
|
||||
} else {
|
||||
throw Exception('UserService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 목록 초기 로드
|
||||
@override
|
||||
Future<PagedResult<User>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final fetchedUsers = await ErrorHandler.handleApiCall<List<User>>(
|
||||
() => _userService.getUsers(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
isActive: _filterIsActive,
|
||||
companyId: _filterCompanyId,
|
||||
role: _filterRole,
|
||||
// search 파라미터 제거 (API에서 지원하지 않음)
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
final items = fetchedUsers ?? [];
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
bool filterItem(User item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return item.name.toLowerCase().contains(q) ||
|
||||
(item.email?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.username?.toLowerCase().contains(q) ?? false);
|
||||
}
|
||||
|
||||
/// 사용자 목록 초기 로드 (호환성 유지)
|
||||
Future<void> loadUsers({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
_users.clear();
|
||||
}
|
||||
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
final newUsers = await _userService.getUsers(
|
||||
page: _currentPage,
|
||||
perPage: _perPage,
|
||||
isActive: _filterIsActive,
|
||||
companyId: _filterCompanyId,
|
||||
role: _filterRole,
|
||||
);
|
||||
|
||||
if (newUsers.isEmpty || newUsers.length < _perPage) {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
|
||||
if (_currentPage == 1) {
|
||||
_users = newUsers;
|
||||
} else {
|
||||
_users.addAll(newUsers);
|
||||
}
|
||||
|
||||
_currentPage++;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
var allUsers = dataService.getAllUsers();
|
||||
|
||||
// 필터 적용
|
||||
if (_filterCompanyId != null) {
|
||||
allUsers = allUsers.where((u) => u.companyId == _filterCompanyId).toList();
|
||||
}
|
||||
if (_filterRole != null) {
|
||||
allUsers = allUsers.where((u) => u.role == _filterRole).toList();
|
||||
}
|
||||
if (_filterIsActive != null) {
|
||||
allUsers = allUsers.where((u) => u.isActive == _filterIsActive).toList();
|
||||
}
|
||||
|
||||
// 검색 적용
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
allUsers = allUsers.where((u) =>
|
||||
u.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(u.email?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false) ||
|
||||
(u.username?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)
|
||||
).toList();
|
||||
}
|
||||
|
||||
_users = allUsers;
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 다음 페이지 로드 (무한 스크롤용)
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMoreData || _isLoadingMore || _isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await loadUsers();
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 쿼리 설정
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
await loadData(isRefresh: refresh);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
@@ -143,9 +89,7 @@ class UserListController extends ChangeNotifier {
|
||||
_filterCompanyId = companyId;
|
||||
_filterRole = role;
|
||||
_filterIsActive = isActive;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
@@ -153,69 +97,126 @@ class UserListController extends ChangeNotifier {
|
||||
_filterCompanyId = null;
|
||||
_filterRole = null;
|
||||
_filterIsActive = null;
|
||||
_searchQuery = '';
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 회사별 필터링
|
||||
void filterByCompany(int? companyId) {
|
||||
_filterCompanyId = companyId;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 역할별 필터링
|
||||
void filterByRole(String? role) {
|
||||
_filterRole = role;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 활성 상태별 필터링
|
||||
void filterByActiveStatus(bool? isActive) {
|
||||
_filterIsActive = isActive;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 사용자 추가
|
||||
Future<void> addUser(User user) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _userService.createUser(
|
||||
username: user.username ?? '',
|
||||
email: user.email ?? '',
|
||||
password: 'temp123', // 임시 비밀번호
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
companyId: user.companyId,
|
||||
branchId: user.branchId,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 사용자 수정
|
||||
Future<void> updateUser(User user) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _userService.updateUser(
|
||||
user.id!,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
companyId: user.companyId,
|
||||
branchId: user.branchId,
|
||||
role: user.role,
|
||||
position: user.position,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(user, (u) => u.id == user.id);
|
||||
}
|
||||
|
||||
/// 사용자 삭제
|
||||
Future<void> deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
await _userService.deleteUser(id);
|
||||
} else {
|
||||
dataService.deleteUser(id);
|
||||
}
|
||||
|
||||
// 목록에서 삭제된 사용자 제거
|
||||
_users.removeWhere((user) => user.id == id);
|
||||
notifyListeners();
|
||||
|
||||
onDeleted();
|
||||
} catch (e) {
|
||||
onError('사용자 삭제 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 상태 변경 (활성/비활성)
|
||||
Future<void> changeUserStatus(int id, bool isActive, Function(String) onError) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
final updatedUser = await _userService.changeUserStatus(id, isActive);
|
||||
// 목록에서 해당 사용자 업데이트
|
||||
final index = _users.indexWhere((u) => u.id == id);
|
||||
if (index != -1) {
|
||||
_users[index] = updatedUser;
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터에서는 상태 변경 지원 안함
|
||||
onError('Mock 데이터에서는 상태 변경을 지원하지 않습니다');
|
||||
}
|
||||
} catch (e) {
|
||||
onError('상태 변경 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용
|
||||
|
||||
/// 회사 ID와 지점 ID로 지점명 조회
|
||||
String getBranchName(int companyId, int? branchId) {
|
||||
final company = dataService.getCompanyById(companyId);
|
||||
if (company == null || company.branches == null || branchId == null) {
|
||||
return '-';
|
||||
}
|
||||
final branch = company.branches!.firstWhere(
|
||||
(b) => b.id == branchId,
|
||||
orElse: () => Branch(companyId: companyId, name: '-'),
|
||||
Future<void> deleteUser(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _userService.deleteUser(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
return branch.name;
|
||||
|
||||
removeItemLocally((u) => u.id == id);
|
||||
}
|
||||
|
||||
/// API/Mock 모드 전환
|
||||
void toggleApiMode() {
|
||||
_useApi = !_useApi;
|
||||
loadUsers(refresh: true);
|
||||
|
||||
/// 사용자 활성/비활성 토글
|
||||
Future<void> toggleUserActiveStatus(User user) async {
|
||||
// TODO: User 모델에 copyWith 메서드가 없어서 임시로 주석 처리
|
||||
// final updatedUser = user.copyWith(isActive: !user.isActive);
|
||||
// await updateUser(updatedUser);
|
||||
debugPrint('사용자 활성 상태 토글: ${user.name}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 비밀번호 재설정
|
||||
Future<void> resetPassword(int userId, String newPassword) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _userService.resetPassword(
|
||||
userId: userId,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 사용자 ID로 단일 사용자 조회
|
||||
User? getUserById(int id) {
|
||||
try {
|
||||
return items.firstWhere((user) => user.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 쿼리 설정 (호환성 유지)
|
||||
void setSearchQuery(String query) {
|
||||
search(query); // BaseListController의 search 메서드 사용
|
||||
}
|
||||
|
||||
/// 사용자 상태 변경
|
||||
Future<void> changeUserStatus(User user, bool isActive) async {
|
||||
// TODO: User 모델에 copyWith 메서드가 없어서 임시로 주석 처리
|
||||
// final updatedUser = user.copyWith(isActive: isActive);
|
||||
// await updateUser(updatedUser);
|
||||
debugPrint('사용자 상태 변경: ${user.name} -> $isActive');
|
||||
}
|
||||
|
||||
/// 지점명 가져오기 (임시 구현)
|
||||
String getBranchName(int? branchId) {
|
||||
if (branchId == null) return '본사';
|
||||
return '지점 $branchId'; // 실제로는 CompanyService에서 가져와야 함
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -34,7 +33,6 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => UserFormController(
|
||||
dataService: MockDataService(),
|
||||
userId: widget.userId,
|
||||
),
|
||||
child: Consumer<UserFormController>(
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/user/controllers/user_list_controller.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/user_utils.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
||||
@@ -19,7 +18,7 @@ class UserListRedesign extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserListRedesignState extends State<UserListRedesign> {
|
||||
final MockDataService _dataService = MockDataService();
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
@@ -60,8 +59,8 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
|
||||
/// 회사명 반환 함수
|
||||
String _getCompanyName(int companyId) {
|
||||
final company = _dataService.getCompanyById(companyId);
|
||||
return company?.name ?? '-';
|
||||
// TODO: CompanyService를 통해 회사 정보 가져오기
|
||||
return '회사 $companyId'; // 임시 처리
|
||||
}
|
||||
|
||||
/// 상태별 색상 반환
|
||||
@@ -128,18 +127,9 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().deleteUser(
|
||||
userId,
|
||||
() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
||||
);
|
||||
},
|
||||
await context.read<UserListController>().deleteUser(userId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
||||
);
|
||||
},
|
||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||
@@ -168,15 +158,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().changeUserStatus(
|
||||
user.id!,
|
||||
newStatus,
|
||||
(error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
||||
);
|
||||
},
|
||||
);
|
||||
await context.read<UserListController>().changeUserStatus(user, newStatus);
|
||||
},
|
||||
child: Text(statusText),
|
||||
),
|
||||
@@ -188,7 +170,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => UserListController(dataService: _dataService),
|
||||
create: (_) => UserListController(),
|
||||
child: Consumer<UserListController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.isLoading && controller.users.isEmpty) {
|
||||
@@ -494,10 +476,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
controller.getBranchName(
|
||||
user.companyId,
|
||||
user.branchId,
|
||||
),
|
||||
controller.getBranchName(user.branchId),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,12 +3,9 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/warehouse_location_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러
|
||||
class WarehouseLocationFormController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
/// 폼 키
|
||||
@@ -42,12 +39,12 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
WarehouseLocation? _originalLocation;
|
||||
|
||||
WarehouseLocationFormController({
|
||||
this.useApi = true,
|
||||
this.mockDataService,
|
||||
int? locationId,
|
||||
}) {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered in GetIt');
|
||||
}
|
||||
|
||||
if (locationId != null) {
|
||||
@@ -73,11 +70,7 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
||||
} else {
|
||||
_originalLocation = mockDataService?.getWarehouseLocationById(locationId);
|
||||
}
|
||||
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
||||
|
||||
if (_originalLocation != null) {
|
||||
nameController.text = _originalLocation!.name;
|
||||
@@ -114,18 +107,10 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
|
||||
);
|
||||
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
if (_isEditMode) {
|
||||
await _warehouseService.updateWarehouseLocation(location);
|
||||
} else {
|
||||
await _warehouseService.createWarehouseLocation(location);
|
||||
}
|
||||
if (_isEditMode) {
|
||||
await _warehouseService.updateWarehouseLocation(location);
|
||||
} else {
|
||||
if (_isEditMode) {
|
||||
mockDataService?.updateWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.addWarehouseLocation(location);
|
||||
}
|
||||
await _warehouseService.createWarehouseLocation(location);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
||||
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
||||
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
||||
class WarehouseLocationListController extends ChangeNotifier {
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
List<WarehouseLocation> _warehouseLocations = [];
|
||||
List<WarehouseLocation> _filteredLocations = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
bool? _isActive;
|
||||
|
||||
WarehouseLocationListController() {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered');
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
bool? get isActive => _isActive;
|
||||
|
||||
/// 데이터 로드
|
||||
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
// API 사용 시 ErrorHandler 적용
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🏭 입고지 목록 API 호출 시작');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getWarehouseLocations(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
print('[WarehouseLocationListController] API 에러: ${failure.message}');
|
||||
},
|
||||
);
|
||||
|
||||
if (fetchedLocations != null) {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 입고지 목록 로드 완료');
|
||||
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계 (입고지에 상태가 있다면)
|
||||
int activeCount = 0;
|
||||
int inactiveCount = 0;
|
||||
for (final location in fetchedLocations) {
|
||||
// isActive 필드가 있다면 활용
|
||||
activeCount++; // 현재는 모두 활성으로 가정
|
||||
}
|
||||
|
||||
print('║ • 활성 입고지: $activeCount개');
|
||||
if (inactiveCount > 0) {
|
||||
print('║ • 비활성 입고지: $inactiveCount개');
|
||||
}
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_warehouseLocations = fetchedLocations;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLocations.length;
|
||||
_applySearchFilter();
|
||||
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
await loadWarehouseLocations(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLocations = List.from(_warehouseLocations);
|
||||
} else {
|
||||
_filteredLocations = _warehouseLocations.where((location) {
|
||||
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({bool? isActive}) {
|
||||
_isActive = isActive;
|
||||
loadWarehouseLocations();
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_isActive = null;
|
||||
_searchQuery = '';
|
||||
loadWarehouseLocations();
|
||||
}
|
||||
|
||||
/// 입고지 추가
|
||||
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.createWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadWarehouseLocations();
|
||||
}
|
||||
|
||||
/// 입고지 수정
|
||||
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.updateWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록에서 업데이트
|
||||
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
||||
if (index != -1) {
|
||||
_warehouseLocations[index] = location;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 입고지 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.deleteWarehouseLocation(id),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록에서 제거
|
||||
_warehouseLocations.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadWarehouseLocations();
|
||||
}
|
||||
|
||||
// 사용 중인 창고 위치 조회
|
||||
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getInUseWarehouseLocations(),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
return locations ?? [];
|
||||
}
|
||||
}
|
||||
@@ -2,265 +2,135 @@ import 'package:flutter/material.dart';
|
||||
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';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
||||
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
||||
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
||||
class WarehouseLocationListController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
WarehouseService? _warehouseService;
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class WarehouseLocationListController extends BaseListController<WarehouseLocation> {
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
List<WarehouseLocation> _warehouseLocations = [];
|
||||
List<WarehouseLocation> _filteredLocations = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
bool? _isActive;
|
||||
|
||||
WarehouseLocationListController({this.useApi = true, this.mockDataService}) {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
WarehouseLocationListController() {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
// 추가 Getters
|
||||
List<WarehouseLocation> get warehouseLocations => items;
|
||||
bool? get isActive => _isActive;
|
||||
|
||||
/// 데이터 로드
|
||||
@override
|
||||
Future<PagedResult<WarehouseLocation>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 사용
|
||||
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getWarehouseLocations(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
isActive: _isActive,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
final items = fetchedLocations ?? [];
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
bool filterItem(WarehouseLocation item, String query) {
|
||||
return item.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
item.address.toString().toLowerCase().contains(query.toLowerCase());
|
||||
}
|
||||
|
||||
/// 데이터 로드 (호환성을 위해 유지)
|
||||
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🏭 입고지 목록 API 호출 시작');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLocations = await _warehouseService!.getWarehouseLocations(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 입고지 목록 로드 완료');
|
||||
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계 (입고지에 상태가 있다면)
|
||||
int activeCount = 0;
|
||||
int inactiveCount = 0;
|
||||
for (final location in fetchedLocations) {
|
||||
// isActive 필드가 있다면 활용
|
||||
activeCount++; // 현재는 모두 활성으로 가정
|
||||
}
|
||||
|
||||
print('║ • 활성 입고지: $activeCount개');
|
||||
if (inactiveCount > 0) {
|
||||
print('║ • 비활성 입고지: $inactiveCount개');
|
||||
}
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_warehouseLocations = fetchedLocations;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLocations.length;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
print('[WarehouseLocationListController] Using Mock data');
|
||||
final allLocations = mockDataService?.getAllWarehouseLocations() ?? [];
|
||||
print('[WarehouseLocationListController] Mock data has ${allLocations.length} locations');
|
||||
|
||||
// 필터링 적용
|
||||
var filtered = allLocations;
|
||||
if (_isActive != null) {
|
||||
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 처리
|
||||
filtered = _isActive! ? allLocations : [];
|
||||
}
|
||||
|
||||
// 페이지네이션 적용
|
||||
final startIndex = (_currentPage - 1) * _pageSize;
|
||||
final endIndex = startIndex + _pageSize;
|
||||
|
||||
if (startIndex < filtered.length) {
|
||||
final pageLocations = filtered.sublist(
|
||||
startIndex,
|
||||
endIndex > filtered.length ? filtered.length : endIndex,
|
||||
);
|
||||
|
||||
if (isInitialLoad) {
|
||||
_warehouseLocations = pageLocations;
|
||||
} else {
|
||||
_warehouseLocations.addAll(pageLocations);
|
||||
}
|
||||
|
||||
_hasMore = endIndex < filtered.length;
|
||||
} else {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
_total = filtered.length;
|
||||
}
|
||||
|
||||
_applySearchFilter();
|
||||
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
await loadWarehouseLocations(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLocations = List.from(_warehouseLocations);
|
||||
} else {
|
||||
_filteredLocations = _warehouseLocations.where((location) {
|
||||
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
}).toList();
|
||||
}
|
||||
await loadData(isRefresh: isInitialLoad);
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({bool? isActive}) {
|
||||
_isActive = isActive;
|
||||
loadWarehouseLocations();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_isActive = null;
|
||||
_searchQuery = '';
|
||||
loadWarehouseLocations();
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 입고지 추가
|
||||
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.createWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.addWarehouseLocation(location);
|
||||
}
|
||||
|
||||
// 목록 새로고침
|
||||
await loadWarehouseLocations();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.createWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 입고지 수정
|
||||
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.updateWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.updateWarehouseLocation(location);
|
||||
}
|
||||
|
||||
// 목록에서 업데이트
|
||||
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
||||
if (index != -1) {
|
||||
_warehouseLocations[index] = location;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.updateWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 로컬 업데이트
|
||||
updateItemLocally(location, (l) => l.id == location.id);
|
||||
}
|
||||
|
||||
/// 입고지 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.deleteWarehouseLocation(id);
|
||||
} else {
|
||||
mockDataService?.deleteWarehouseLocation(id);
|
||||
}
|
||||
|
||||
// 목록에서 제거
|
||||
_warehouseLocations.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadWarehouseLocations();
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.deleteWarehouseLocation(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 로컬 삭제
|
||||
removeItemLocally((l) => l.id == id);
|
||||
}
|
||||
|
||||
// 사용 중인 창고 위치 조회
|
||||
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
return await _warehouseService!.getInUseWarehouseLocations();
|
||||
} else {
|
||||
// Mock 데이터에서는 모든 창고가 사용 중으로 간주
|
||||
return mockDataService?.getAllWarehouseLocations() ?? [];
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getInUseWarehouseLocations(),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
return locations ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/utils/error_handler.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
import '../../../data/models/warehouse/warehouse_dto.dart';
|
||||
import '../../../domain/usecases/warehouse_location/warehouse_location_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 창고 위치 목록 컨트롤러
|
||||
class WarehouseLocationListControllerWithUseCase extends BaseListController<WarehouseLocationDto> {
|
||||
final GetWarehouseLocationsUseCase getWarehouseLocationsUseCase;
|
||||
final CreateWarehouseLocationUseCase createWarehouseLocationUseCase;
|
||||
final UpdateWarehouseLocationUseCase updateWarehouseLocationUseCase;
|
||||
final DeleteWarehouseLocationUseCase deleteWarehouseLocationUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLocationIds = {};
|
||||
Set<int> get selectedLocationIds => _selectedLocationIds;
|
||||
|
||||
// 필터 옵션
|
||||
bool _showActiveOnly = true;
|
||||
String? _filterByManager;
|
||||
|
||||
bool get showActiveOnly => _showActiveOnly;
|
||||
String? get filterByManager => _filterByManager;
|
||||
|
||||
WarehouseLocationListControllerWithUseCase({
|
||||
required this.getWarehouseLocationsUseCase,
|
||||
required this.createWarehouseLocationUseCase,
|
||||
required this.updateWarehouseLocationUseCase,
|
||||
required this.deleteWarehouseLocationUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<WarehouseLocationDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_showActiveOnly) filters['is_active'] = true;
|
||||
if (_filterByManager != null) filters['manager'] = _filterByManager;
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetWarehouseLocationsParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getWarehouseLocationsUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(locationsResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: locationsResponse.items.length,
|
||||
totalPages: (locationsResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: locationsResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: locationsResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 생성
|
||||
Future<void> createWarehouseLocation({
|
||||
required String name,
|
||||
required String address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateWarehouseLocationParams(
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
);
|
||||
|
||||
final result = await createWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => await refresh(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 수정
|
||||
Future<void> updateWarehouseLocation({
|
||||
required int id,
|
||||
String? name,
|
||||
String? address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
bool? isActive,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateWarehouseLocationParams(
|
||||
id: id,
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
final result = await updateWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => updateItemLocally(location, (item) => item.id == location.id),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLocationIds.remove(id);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 활성/비활성 토글
|
||||
Future<void> toggleLocationStatus(int id) async {
|
||||
final location = items.firstWhere((item) => item.id == id);
|
||||
await updateWarehouseLocation(
|
||||
id: id,
|
||||
isActive: !location.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
bool? showActiveOnly,
|
||||
String? manager,
|
||||
}) {
|
||||
if (showActiveOnly != null) _showActiveOnly = showActiveOnly;
|
||||
_filterByManager = manager;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_showActiveOnly = true;
|
||||
_filterByManager = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 창고 위치 선택 토글
|
||||
void toggleLocationSelection(int id) {
|
||||
if (_selectedLocationIds.contains(id)) {
|
||||
_selectedLocationIds.remove(id);
|
||||
} else {
|
||||
_selectedLocationIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 창고 위치 선택
|
||||
void selectAll() {
|
||||
_selectedLocationIds.clear();
|
||||
_selectedLocationIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 삭제
|
||||
Future<void> deleteSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final errors = <String>[];
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
result.fold(
|
||||
(failure) => errors.add('Location $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
errorState = '일부 창고 위치 삭제 실패:\n${errors.join('\n')}';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 활성화
|
||||
Future<void> activateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: true);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 비활성화
|
||||
Future<void> deactivateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: false);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 드롭다운용 활성 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getActiveLocations() {
|
||||
return items.where((location) => location.isActive).toList();
|
||||
}
|
||||
|
||||
/// 특정 관리자의 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getLocationsByManager(String manager) {
|
||||
return items.where((location) => location.managerName == manager).toList(); // managerName 필드 사용
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLocationIds.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/warehouse_location/controllers/warehouse_location_list_controller.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
||||
class WarehouseLocationListRedesign extends StatefulWidget {
|
||||
@@ -99,10 +100,13 @@ class _WarehouseLocationListRedesignState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<WarehouseLocationListController>(
|
||||
builder: (context, controller, child) {
|
||||
// Admin과 Manager만 접근 가능
|
||||
return AuthGuard(
|
||||
allowedRoles: UserRole.adminAndManager,
|
||||
child: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<WarehouseLocationListController>(
|
||||
builder: (context, controller, child) {
|
||||
final int totalCount = controller.warehouseLocations.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
@@ -161,6 +165,7 @@ class _WarehouseLocationListRedesignState
|
||||
) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user