refactor: UI 화면 통합 및 불필요한 파일 정리
- 모든 *_redesign.dart 파일을 기본 화면 파일로 통합 - 백업용 컨트롤러 파일들 제거 (*_controller.backup.dart) - 사용하지 않는 예제 및 테스트 파일 제거 - Clean Architecture 적용 후 남은 정리 작업 완료 - 테스트 코드 정리 및 구조 개선 준비 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,12 @@ 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';
|
||||
import 'package:superport/screens/equipment/equipment_list_redesign.dart';
|
||||
import 'package:superport/screens/company/company_list_redesign.dart';
|
||||
import 'package:superport/screens/user/user_list_redesign.dart';
|
||||
import 'package:superport/screens/license/license_list_redesign.dart';
|
||||
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
||||
import 'package:superport/screens/overview/overview_screen.dart';
|
||||
import 'package:superport/screens/equipment/equipment_list.dart';
|
||||
import 'package:superport/screens/company/company_list.dart';
|
||||
import 'package:superport/screens/user/user_list.dart';
|
||||
import 'package:superport/screens/license/license_list.dart';
|
||||
import 'package:superport/screens/warehouse_location/warehouse_location_list.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/services/dashboard_service.dart';
|
||||
import 'package:superport/services/lookup_service.dart';
|
||||
@@ -18,17 +18,17 @@ import 'package:superport/data/models/auth/auth_user.dart';
|
||||
/// ERP 시스템 최적화 메인 레이아웃
|
||||
/// F-Pattern 레이아웃 적용 (1920x1080 최적화)
|
||||
/// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조
|
||||
class AppLayoutRedesign extends StatefulWidget {
|
||||
class AppLayout extends StatefulWidget {
|
||||
final String initialRoute;
|
||||
|
||||
const AppLayoutRedesign({Key? key, this.initialRoute = Routes.home})
|
||||
const AppLayout({Key? key, this.initialRoute = Routes.home})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<AppLayoutRedesign> createState() => _AppLayoutRedesignState();
|
||||
State<AppLayout> createState() => _AppLayoutState();
|
||||
}
|
||||
|
||||
class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
class _AppLayoutState extends State<AppLayout>
|
||||
with TickerProviderStateMixin {
|
||||
late String _currentRoute;
|
||||
bool _sidebarCollapsed = false;
|
||||
@@ -156,20 +156,20 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
Widget _getContentForRoute(String route) {
|
||||
switch (route) {
|
||||
case Routes.home:
|
||||
return const OverviewScreenRedesign();
|
||||
return const OverviewScreen();
|
||||
case Routes.equipment:
|
||||
case Routes.equipmentInList:
|
||||
case Routes.equipmentOutList:
|
||||
case Routes.equipmentRentList:
|
||||
return EquipmentListRedesign(currentRoute: route);
|
||||
return EquipmentList(currentRoute: route);
|
||||
case Routes.company:
|
||||
return const CompanyListRedesign();
|
||||
return const CompanyList();
|
||||
case Routes.user:
|
||||
return const UserListRedesign();
|
||||
return const UserList();
|
||||
case Routes.license:
|
||||
return const LicenseListRedesign();
|
||||
return const LicenseList();
|
||||
case Routes.warehouseLocation:
|
||||
return const WarehouseLocationListRedesign();
|
||||
return const WarehouseLocationList();
|
||||
case '/test/api':
|
||||
// Navigator를 사용하여 별도 화면으로 이동
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -177,7 +177,7 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
});
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
default:
|
||||
return const OverviewScreenRedesign();
|
||||
return const OverviewScreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
|
||||
/// 사이드바 빌드
|
||||
Widget _buildSidebar() {
|
||||
return SidebarMenuRedesign(
|
||||
return SidebarMenu(
|
||||
currentRoute: _currentRoute,
|
||||
onRouteChanged: _navigateTo,
|
||||
collapsed: _sidebarCollapsed,
|
||||
@@ -881,13 +881,13 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
}
|
||||
|
||||
/// 재설계된 사이드바 메뉴 (접기/펼치기 지원)
|
||||
class SidebarMenuRedesign extends StatelessWidget {
|
||||
class SidebarMenu extends StatelessWidget {
|
||||
final String currentRoute;
|
||||
final Function(String) onRouteChanged;
|
||||
final bool collapsed;
|
||||
final int expiringLicenseCount;
|
||||
|
||||
const SidebarMenuRedesign({
|
||||
const SidebarMenu({
|
||||
Key? key,
|
||||
required this.currentRoute,
|
||||
required this.onRouteChanged,
|
||||
@@ -359,7 +359,6 @@ class ShadcnInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ShadcnInputState extends State<ShadcnInput> {
|
||||
bool _isFocused = false;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
@@ -384,9 +383,7 @@ class _ShadcnInputState extends State<ShadcnInput> {
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: Focus(
|
||||
onFocusChange: (focused) => setState(() => _isFocused = focused),
|
||||
child: TextFormField(
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
obscureText: widget.obscureText,
|
||||
keyboardType: widget.keyboardType,
|
||||
@@ -470,7 +467,6 @@ class _ShadcnInputState extends State<ShadcnInput> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'form_field_wrapper.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
|
||||
// 날짜 선택 필드
|
||||
class DatePickerField extends StatelessWidget {
|
||||
@@ -45,7 +45,7 @@ class DatePickerField extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Metronic Admin 테일윈드 테마 (데모6 스타일)
|
||||
class AppThemeTailwind {
|
||||
// 메인 컬러 팔레트
|
||||
static const Color primary = Color(0xFF5867DD);
|
||||
static const Color secondary = Color(0xFF34BFA3);
|
||||
static const Color success = Color(0xFF1BC5BD);
|
||||
static const Color info = Color(0xFF8950FC);
|
||||
static const Color warning = Color(0xFFFFA800);
|
||||
static const Color danger = Color(0xFFF64E60);
|
||||
static const Color light = Color(0xFFF3F6F9);
|
||||
static const Color dark = Color(0xFF181C32);
|
||||
static const Color muted = Color(0xFFB5B5C3);
|
||||
|
||||
// 배경 컬러
|
||||
static const Color surface = Color(0xFFF7F8FA);
|
||||
static const Color cardBackground = Colors.white;
|
||||
|
||||
// 테마 데이터
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
primaryColor: primary,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
surface: surface,
|
||||
error: danger,
|
||||
),
|
||||
scaffoldBackgroundColor: surface,
|
||||
fontFamily: 'Poppins',
|
||||
|
||||
// AppBar 테마
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: dark,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
color: dark,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
iconTheme: IconThemeData(color: dark),
|
||||
),
|
||||
|
||||
// 버튼 테마
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
),
|
||||
|
||||
// 카드 테마
|
||||
cardTheme: CardThemeData(
|
||||
color: Colors.white,
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
|
||||
// 입력 폼 테마
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: primary),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: danger),
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
|
||||
// 데이터 테이블 테마
|
||||
dataTableTheme: const DataTableThemeData(
|
||||
headingRowColor: WidgetStatePropertyAll(light),
|
||||
dividerThickness: 1,
|
||||
columnSpacing: 24,
|
||||
headingTextStyle: TextStyle(
|
||||
color: dark,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
dataTextStyle: TextStyle(color: Color(0xFF6C7293), fontSize: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 스타일 - 헤딩 및 텍스트
|
||||
static const TextStyle headingStyle = TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: dark,
|
||||
);
|
||||
|
||||
static const TextStyle subheadingStyle = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: dark,
|
||||
);
|
||||
|
||||
static const TextStyle bodyStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF6C7293),
|
||||
);
|
||||
|
||||
// 굵은 본문 텍스트
|
||||
static const TextStyle bodyBoldStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: dark,
|
||||
);
|
||||
|
||||
static const TextStyle smallText = TextStyle(fontSize: 12, color: muted);
|
||||
|
||||
// 버튼 스타일
|
||||
static final ButtonStyle primaryButtonStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
);
|
||||
|
||||
// 라벨 스타일
|
||||
static const TextStyle formLabelStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: dark,
|
||||
);
|
||||
|
||||
static final ButtonStyle secondaryButtonStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: secondary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
);
|
||||
|
||||
static final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom(
|
||||
foregroundColor: primary,
|
||||
side: const BorderSide(color: primary),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
);
|
||||
|
||||
// 카드 장식
|
||||
static final BoxDecoration cardDecoration = BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(13),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// 기타 장식
|
||||
static final BoxDecoration containerDecoration = BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||||
);
|
||||
|
||||
static const EdgeInsets cardPadding = EdgeInsets.all(20);
|
||||
static const EdgeInsets listPadding = EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/utils/address_constants.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
|
||||
@@ -41,7 +41,7 @@ class AddressInput extends StatefulWidget {
|
||||
/// Address 객체를 받아 읽기 전용으로 표시하는 위젯
|
||||
static Widget readonly({required Address address}) {
|
||||
// 회사 리스트와 동일하게 address.toString() 사용, 스타일도 bodyStyle로 통일
|
||||
return Text(address.toString(), style: AppThemeTailwind.bodyStyle);
|
||||
return Text(address.toString(), style: ShadcnTheme.bodyMedium);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ class _AddressInputState extends State<AddressInput> {
|
||||
height: 48,
|
||||
child: Text(
|
||||
region,
|
||||
style: AppThemeTailwind.bodyStyle.copyWith(
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ import 'package:flutter/material.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
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||
// import 'package:superport/screens/company/widgets/branch_card.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
||||
@@ -48,7 +48,7 @@ class CompanyTypeSelector extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('회사 유형', style: AppThemeTailwind.formLabelStyle),
|
||||
Text('회사 유형', style: ShadcnTheme.labelMedium),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
@@ -357,7 +357,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppThemeTailwind.primary,
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -463,7 +463,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'지점 정보',
|
||||
style: AppThemeTailwind.subheadingStyle,
|
||||
style: ShadcnTheme.headingH6,
|
||||
),
|
||||
),
|
||||
if (_controller.branchControllers.isNotEmpty)
|
||||
@@ -507,7 +507,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppThemeTailwind.primary,
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
@@ -17,25 +17,23 @@ import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
|
||||
import 'package:superport/screens/company/controllers/company_list_controller.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 회사 관리 화면 (통일된 UI 컴포넌트 사용)
|
||||
class CompanyListRedesign extends StatefulWidget {
|
||||
const CompanyListRedesign({super.key});
|
||||
class CompanyList extends StatefulWidget {
|
||||
const CompanyList({super.key});
|
||||
|
||||
@override
|
||||
State<CompanyListRedesign> createState() => _CompanyListRedesignState();
|
||||
State<CompanyList> createState() => _CompanyListState();
|
||||
}
|
||||
|
||||
class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
class _CompanyListState extends State<CompanyList> {
|
||||
late CompanyListController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
Timer? _debounceTimer;
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CompanyListController();
|
||||
_controller.initializeWithPageSize(_pageSize);
|
||||
_controller.initializeWithPageSize(10); // 페이지 크기 설정
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -50,10 +48,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
void _onSearchChanged(String value) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(AppConstants.searchDebounce, () {
|
||||
setState(() {
|
||||
_currentPage = 1;
|
||||
});
|
||||
_controller.search(value);
|
||||
_controller.search(value); // Controller가 페이지 리셋 처리
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,37 +223,15 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
}
|
||||
}
|
||||
|
||||
final int totalCount = displayCompanies.length;
|
||||
|
||||
// 페이지네이션을 위한 데이터 처리
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex = startIndex + _pageSize;
|
||||
// Controller가 이미 페이지크된 데이터를 제공
|
||||
final List<Map<String, dynamic>> pagedCompanies = displayCompanies;
|
||||
final int totalCount = controller.total; // 실제 전체 개수 사용
|
||||
|
||||
// 디버그 로그 추가
|
||||
print('🔍 [VIEW DEBUG] 화면 페이지네이션 상태');
|
||||
print(' • filteredCompanies 수: ${controller.filteredCompanies.length}개');
|
||||
print(' • displayCompanies 수: ${displayCompanies.length}개 (지점 포함)');
|
||||
print(' • 현재 페이지: $_currentPage');
|
||||
print(' • 페이지 크기: $_pageSize');
|
||||
print(' • startIndex: $startIndex, endIndex: $endIndex');
|
||||
|
||||
// startIndex가 displayCompanies.length보다 크거나 같으면 첫 페이지로 리셋
|
||||
if (startIndex >= displayCompanies.length && displayCompanies.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_currentPage = 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> pagedCompanies = displayCompanies.isEmpty
|
||||
? []
|
||||
: displayCompanies.sublist(
|
||||
startIndex.clamp(0, displayCompanies.length),
|
||||
endIndex.clamp(0, displayCompanies.length),
|
||||
);
|
||||
|
||||
print(' • 화면에 표시될 항목 수: ${pagedCompanies.length}개');
|
||||
print('🔍 [VIEW DEBUG] 페이지네이션 상태');
|
||||
print(' • Controller items: ${controller.companies.length}개');
|
||||
print(' • 전체 개수: ${controller.total}개');
|
||||
print(' • 현재 페이지: ${controller.currentPage}');
|
||||
print(' • 페이지 크기: ${controller.pageSize}');
|
||||
|
||||
// 로딩 상태
|
||||
if (controller.isLoading && controller.companies.isEmpty) {
|
||||
@@ -344,7 +317,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
],
|
||||
rows: [
|
||||
...pagedCompanies.asMap().entries.map((entry) {
|
||||
final int index = startIndex + entry.key;
|
||||
final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key;
|
||||
final companyData = entry.value;
|
||||
final bool isBranch = companyData['isBranch'] as bool;
|
||||
final Company company =
|
||||
@@ -457,15 +430,18 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
],
|
||||
),
|
||||
|
||||
// 페이지네이션 (항상 표시)
|
||||
// 페이지네이션 (Controller 상태 사용)
|
||||
pagination: Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
// 다음 페이지 로드
|
||||
if (page > controller.currentPage) {
|
||||
controller.loadNextPage();
|
||||
} else if (page == 1) {
|
||||
controller.refresh();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -473,4 +449,4 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,8 +96,9 @@ class CompanyFormController {
|
||||
try {
|
||||
List<Company> companies;
|
||||
|
||||
// API만 사용
|
||||
companies = await _companyService.getCompanies();
|
||||
// API만 사용 (PaginatedResponse에서 items 추출)
|
||||
final response = await _companyService.getCompanies();
|
||||
companies = response.items;
|
||||
|
||||
companyNames = companies.map((c) => c.name).toList();
|
||||
filteredCompanyNames = companyNames;
|
||||
@@ -347,9 +348,9 @@ class CompanyFormController {
|
||||
if (_useApi) {
|
||||
try {
|
||||
// 회사명 목록을 조회하여 중복 확인
|
||||
final companies = await _companyService.getCompanies(search: name);
|
||||
final response = await _companyService.getCompanies(search: name);
|
||||
// 정확히 일치하는 회사명이 있는지 확인
|
||||
for (final company in companies) {
|
||||
for (final company in response.items) {
|
||||
if (company.name.toLowerCase() == name.toLowerCase()) {
|
||||
return company;
|
||||
}
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@ class CompanyListController extends BaseListController<Company> {
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출 - 회사 목록 조회
|
||||
final apiCompanies = await ErrorHandler.handleApiCall<List<Company>>(
|
||||
// API 호출 - 회사 목록 조회 (이제 PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall<dynamic>(
|
||||
() => _companyService.getCompanies(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
@@ -61,21 +61,17 @@ class CompanyListController extends BaseListController<Company> {
|
||||
},
|
||||
);
|
||||
|
||||
final items = apiCompanies ?? [];
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
// PaginatedResponse를 PagedResult로 변환
|
||||
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,
|
||||
currentPage: response.page,
|
||||
perPage: response.size,
|
||||
total: response.totalElements,
|
||||
totalPages: response.totalPages,
|
||||
hasNext: !response.last,
|
||||
hasPrevious: !response.first,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
return PagedResult(items: response.items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
@@ -81,7 +81,7 @@ class _BranchCardState extends State<BranchCard> {
|
||||
children: [
|
||||
Text(
|
||||
'지점 #${widget.index + 1}',
|
||||
style: AppThemeTailwind.subheadingStyle,
|
||||
style: ShadcnTheme.headingH6,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.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/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/screens/company/widgets/company_name_autocomplete.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
|
||||
/// 주소에 대한 지도 대화상자를 표시합니다.
|
||||
class MapDialog extends StatelessWidget {
|
||||
@@ -68,7 +68,7 @@ class MapDialog extends StatelessWidget {
|
||||
Icon(
|
||||
Icons.map,
|
||||
size: 64,
|
||||
color: AppThemeTailwind.primary,
|
||||
color: ShadcnTheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
|
||||
@@ -123,10 +123,10 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
void _loadWarehouseLocations() async {
|
||||
try {
|
||||
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final locations = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = locations.map((e) => e.name).toList();
|
||||
final response = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = response.items.map((e) => e.name).toList();
|
||||
// 이름-ID 매핑 저장
|
||||
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
||||
warehouseLocationMap = {for (var loc in response.items) loc.name: loc.id};
|
||||
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': warehouseLocations.length,
|
||||
'locations': warehouseLocations,
|
||||
@@ -146,8 +146,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
void _loadPartnerCompanies() async {
|
||||
try {
|
||||
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final companies = await _companyService.getCompanies();
|
||||
partnerCompanies = companies.map((c) => c.name).toList();
|
||||
final response = await _companyService.getCompanies();
|
||||
partnerCompanies = response.items.map((c) => c.name).toList();
|
||||
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': partnerCompanies.length,
|
||||
'companies': partnerCompanies,
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
}
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환
|
||||
final items = apiEquipmentDtos.map((dto) {
|
||||
final items = apiEquipmentDtos.items.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||
@@ -109,7 +109,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
(params.page * params.perPage) + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
|
||||
@@ -79,8 +79,8 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
Future<void> loadDropdownData() async {
|
||||
try {
|
||||
// API를 통해 회사 목록 로드
|
||||
final allCompanies = await _companyService.getCompanies();
|
||||
companies = allCompanies
|
||||
final response = await _companyService.getCompanies();
|
||||
companies = response.items
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
@@ -204,9 +204,9 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
|
||||
// 선택된 회사 정보에서 ID 추출
|
||||
if (selectedCompanies[0] != null) {
|
||||
final companies = await companyService.getCompanies(search: selectedCompanies[0]);
|
||||
if (companies.isNotEmpty) {
|
||||
companyId = companies.first.id;
|
||||
final response = await companyService.getCompanies(search: selectedCompanies[0]);
|
||||
if (response.items.isNotEmpty) {
|
||||
companyId = response.items.first.id;
|
||||
// TODO: 지점 ID 처리 로직 추가
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
// import 'package:superport/models/equipment_unified_model.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -2163,7 +2163,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
children: [
|
||||
Text(
|
||||
'${_controller.inDate.year}-${_controller.inDate.month.toString().padLeft(2, '0')}-${_controller.inDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
@@ -2258,7 +2258,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -2308,7 +2308,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,17 @@ import 'package:superport/utils/equipment_display_helper.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
||||
class EquipmentListRedesign extends StatefulWidget {
|
||||
class EquipmentList extends StatefulWidget {
|
||||
final String currentRoute;
|
||||
|
||||
const EquipmentListRedesign({Key? key, this.currentRoute = Routes.equipment})
|
||||
const EquipmentList({Key? key, this.currentRoute = Routes.equipment})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<EquipmentListRedesign> createState() => _EquipmentListRedesignState();
|
||||
State<EquipmentList> createState() => _EquipmentListState();
|
||||
}
|
||||
|
||||
class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
class _EquipmentListState extends State<EquipmentList> {
|
||||
late final EquipmentListController _controller;
|
||||
bool _showDetailedColumns = true;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
@@ -34,14 +34,14 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
String _selectedStatus = 'all';
|
||||
// String _searchKeyword = ''; // Removed - unused field
|
||||
String _appliedSearchKeyword = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
// 페이지 상태는 이제 Controller에서 관리
|
||||
final Set<int> _selectedItems = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController();
|
||||
_controller.pageSize = 10; // 페이지 크기 설정
|
||||
_setInitialFilter();
|
||||
|
||||
// API 호출을 위해 Future로 변경
|
||||
@@ -113,7 +113,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
} else if (status == 'rent') {
|
||||
_controller.selectedStatusFilter = EquipmentStatus.rent;
|
||||
}
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
void _onSearch() async {
|
||||
setState(() {
|
||||
_appliedSearchKeyword = _searchController.text;
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
_controller.updateSearchKeyword(_searchController.text);
|
||||
}
|
||||
@@ -414,14 +414,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
dataTable: _buildDataTable(filteredEquipments),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: totalCount > _pageSize ? Pagination(
|
||||
pagination: totalCount > controller.pageSize ? Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
controller.goToPage(page);
|
||||
},
|
||||
) : null,
|
||||
);
|
||||
@@ -515,7 +513,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
onRefresh: () {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
},
|
||||
statusMessage:
|
||||
@@ -548,7 +546,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -623,7 +621,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -829,7 +827,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_currentPage - 1) * _pageSize) + index + 1}',
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
@@ -946,11 +944,11 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<UnifiedEquipment> filteredEquipments) {
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > filteredEquipments.length
|
||||
(startIndex + _controller.pageSize) > filteredEquipments.length
|
||||
? filteredEquipments.length
|
||||
: (startIndex + _pageSize);
|
||||
: (startIndex + _controller.pageSize);
|
||||
final List<UnifiedEquipment> pagedEquipments = filteredEquipments.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
@@ -975,7 +973,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1176,8 +1174,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
/// 페이지 데이터 가져오기
|
||||
List<UnifiedEquipment> _getPagedEquipments() {
|
||||
final filteredEquipments = _getFilteredEquipments();
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex = startIndex + _pageSize;
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex = startIndex + _controller.pageSize;
|
||||
|
||||
if (startIndex >= filteredEquipments.length) {
|
||||
return [];
|
||||
@@ -6,7 +6,7 @@ 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/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.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';
|
||||
@@ -421,7 +421,10 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
: null,
|
||||
style:
|
||||
canSubmit
|
||||
? AppThemeTailwind.primaryButtonStyle
|
||||
? ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
)
|
||||
: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
@@ -600,7 +603,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
company.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${company.contactName} ${company.contactPosition ?? ""} ${company.contactPhone ?? ""} ${company.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
if (!companyInfo.isMainCompany &&
|
||||
branch != null &&
|
||||
@@ -608,7 +611,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
branch.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${branch.contactName} ${branch.contactPosition ?? ""} ${branch.contactPhone ?? ""} ${branch.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 담당자 목록에서 실제 담당자 정보만 표시하는 부분은 제거
|
||||
@@ -686,7 +689,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
children: [
|
||||
Text(
|
||||
controller.formatDate(date),
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
@@ -817,7 +820,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
userWidgets.add(
|
||||
Text(
|
||||
'정수진 사원 010-4567-8901 jung.soojin@lg.com',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -74,8 +74,8 @@ class LicenseListController extends BaseListController<License> {
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final fetchedLicenses = await ErrorHandler.handleApiCall(
|
||||
// API 호출 (PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall(
|
||||
() => _licenseService.getLicenses(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
@@ -88,7 +88,7 @@ class LicenseListController extends BaseListController<License> {
|
||||
},
|
||||
);
|
||||
|
||||
if (fetchedLicenses == null) {
|
||||
if (response == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
@@ -103,21 +103,19 @@ class LicenseListController extends BaseListController<License> {
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
await _updateStatistics(fetchedLicenses);
|
||||
await _updateStatistics(response.items);
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
// PaginatedResponse를 PagedResult로 변환
|
||||
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,
|
||||
currentPage: response.page,
|
||||
perPage: response.size,
|
||||
total: response.totalElements,
|
||||
totalPages: response.totalPages,
|
||||
hasNext: !response.last,
|
||||
hasPrevious: !response.first,
|
||||
);
|
||||
|
||||
return PagedResult(items: fetchedLicenses, meta: meta);
|
||||
return PagedResult(items: response.items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/screens/license/controllers/license_form_controller.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/utils/validators.dart';
|
||||
@@ -109,7 +109,7 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: AppThemeTailwind.success,
|
||||
backgroundColor: ShadcnTheme.success,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
|
||||
@@ -15,20 +15,19 @@ import 'package:superport/core/config/environment.dart' as env;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 유지보수 라이선스 관리 화면
|
||||
class LicenseListRedesign extends StatefulWidget {
|
||||
const LicenseListRedesign({super.key});
|
||||
class LicenseList extends StatefulWidget {
|
||||
const LicenseList({super.key});
|
||||
|
||||
@override
|
||||
State<LicenseListRedesign> createState() => _LicenseListRedesignState();
|
||||
State<LicenseList> createState() => _LicenseListState();
|
||||
}
|
||||
|
||||
class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
class _LicenseListState extends State<LicenseList> {
|
||||
late final LicenseListController _controller;
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _horizontalScrollController = ScrollController();
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
// 페이지 상태는 이제 Controller에서 관리
|
||||
|
||||
// 날짜 포맷터
|
||||
final DateFormat _dateFormat = DateFormat('yyyy-MM-dd');
|
||||
@@ -69,15 +68,9 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 페이지네이션용 라이선스 가져오기
|
||||
/// 페이지네이션용 라이선스 가져오기 (Controller가 이미 페이징된 데이터 제공)
|
||||
List<License> _getPagedLicenses() {
|
||||
final licenses = _controller.licenses;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex = startIndex + _pageSize;
|
||||
return licenses.sublist(
|
||||
startIndex,
|
||||
endIndex > licenses.length ? licenses.length : endIndex,
|
||||
);
|
||||
return _controller.licenses; // 이미 페이징된 데이터
|
||||
}
|
||||
|
||||
/// 검색 실행
|
||||
@@ -253,15 +246,13 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
searchBar: _buildSearchBar(),
|
||||
actionBar: _buildActionBar(),
|
||||
dataTable: _buildDataTable(),
|
||||
pagination: totalCount > _pageSize
|
||||
pagination: totalCount > controller.pageSize
|
||||
? Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
controller.goToPage(page);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
@@ -606,7 +597,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||
...pagedLicenses.asMap().entries.map((entry) {
|
||||
final displayIndex = entry.key;
|
||||
final license = entry.value;
|
||||
final index = (_currentPage - 1) * _pageSize + displayIndex;
|
||||
final index = (_controller.currentPage - 1) * _controller.pageSize + displayIndex;
|
||||
final daysRemaining = _controller.getDaysUntilExpiry(license.expiryDate);
|
||||
|
||||
return Container(
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/login/controllers/login_controller.dart';
|
||||
import 'package:superport/screens/login/widgets/login_view_redesign.dart';
|
||||
import 'package:superport/screens/login/widgets/login_view.dart';
|
||||
|
||||
/// 로그인 화면 진입점 (상태/로직은 controller, UI는 LoginView 위젯에 위임)
|
||||
class LoginScreen extends StatefulWidget {
|
||||
@@ -31,7 +31,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LoginViewRedesign(
|
||||
return LoginView(
|
||||
controller: _controller,
|
||||
onLoginSuccess: _onLoginSuccess,
|
||||
);
|
||||
|
||||
@@ -5,21 +5,21 @@ import 'package:superport/screens/login/controllers/login_controller.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 로그인 화면
|
||||
class LoginViewRedesign extends StatefulWidget {
|
||||
class LoginView extends StatefulWidget {
|
||||
final LoginController controller;
|
||||
final VoidCallback onLoginSuccess;
|
||||
|
||||
const LoginViewRedesign({
|
||||
const LoginView({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.onLoginSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LoginViewRedesign> createState() => _LoginViewRedesignState();
|
||||
State<LoginView> createState() => _LoginViewState();
|
||||
}
|
||||
|
||||
class _LoginViewRedesignState extends State<LoginViewRedesign>
|
||||
class _LoginViewState extends State<LoginView>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _fadeController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
@@ -335,4 +335,4 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import 'package:superport/data/models/dashboard/license_expiry_summary.dart';
|
||||
import 'package:superport/data/models/dashboard/overview_stats.dart';
|
||||
import 'package:superport/data/models/dashboard/recent_activity.dart';
|
||||
import 'package:superport/services/dashboard_service.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
|
||||
// 대시보드(Overview) 화면의 상태 및 비즈니스 로직을 담답하는 컨트롤러
|
||||
@@ -309,18 +309,18 @@ class OverviewController extends ChangeNotifier {
|
||||
switch (activityType.toLowerCase()) {
|
||||
case 'equipment_in':
|
||||
case '장비 입고':
|
||||
return AppThemeTailwind.success;
|
||||
return ShadcnTheme.success;
|
||||
case 'equipment_out':
|
||||
case '장비 출고':
|
||||
return AppThemeTailwind.warning;
|
||||
return ShadcnTheme.warning;
|
||||
case 'user_create':
|
||||
case '사용자 추가':
|
||||
return AppThemeTailwind.primary;
|
||||
return ShadcnTheme.primary;
|
||||
case 'license_create':
|
||||
case '라이선스 등록':
|
||||
return AppThemeTailwind.info;
|
||||
return ShadcnTheme.info;
|
||||
default:
|
||||
return AppThemeTailwind.muted;
|
||||
return ShadcnTheme.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ 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({super.key});
|
||||
class OverviewScreen extends StatefulWidget {
|
||||
const OverviewScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
|
||||
State<OverviewScreen> createState() => _OverviewScreenState();
|
||||
}
|
||||
|
||||
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
class _OverviewScreenState extends State<OverviewScreen> {
|
||||
late final OverviewController _controller;
|
||||
late final HealthCheckService _healthCheckService;
|
||||
Map<String, dynamic>? _healthStatus;
|
||||
@@ -872,4 +872,4 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
'color': ShadcnTheme.foreground,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class UserFormController extends ChangeNotifier {
|
||||
Future<void> loadCompanies() async {
|
||||
try {
|
||||
final result = await _companyService.getCompanies();
|
||||
companies = result;
|
||||
companies = result.items; // PaginatedResponse에서 items 추출
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('회사 목록 로드 실패: $e');
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
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만 사용하므로 토글 기능 제거
|
||||
}
|
||||
@@ -35,8 +35,8 @@ class UserListController extends BaseListController<User> {
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final fetchedUsers = await ErrorHandler.handleApiCall<List<User>>(
|
||||
// API 호출 (이제 PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall<dynamic>(
|
||||
() => _userService.getUsers(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
@@ -50,21 +50,17 @@ class UserListController extends BaseListController<User> {
|
||||
},
|
||||
);
|
||||
|
||||
final items = fetchedUsers ?? [];
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
// PaginatedResponse를 PagedResult로 변환
|
||||
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,
|
||||
currentPage: response.page,
|
||||
perPage: response.size,
|
||||
total: response.totalElements,
|
||||
totalPages: response.totalPages,
|
||||
hasNext: !response.last,
|
||||
hasPrevious: !response.first,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
return PagedResult(items: response.items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -272,7 +272,10 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: () => _onSaveUser(controller),
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: controller.isLoading
|
||||
|
||||
@@ -10,18 +10,16 @@ import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/user_utils.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
||||
class UserListRedesign extends StatefulWidget {
|
||||
const UserListRedesign({super.key});
|
||||
class UserList extends StatefulWidget {
|
||||
const UserList({super.key});
|
||||
|
||||
@override
|
||||
State<UserListRedesign> createState() => _UserListRedesignState();
|
||||
State<UserList> createState() => _UserListState();
|
||||
}
|
||||
|
||||
class _UserListRedesignState extends State<UserListRedesign> {
|
||||
class _UserListState extends State<UserList> {
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -29,7 +27,9 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
|
||||
// 초기 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<UserListController>().loadUsers();
|
||||
final controller = context.read<UserListController>();
|
||||
controller.pageSize = 10; // 페이지 크기 설정
|
||||
controller.loadUsers();
|
||||
});
|
||||
|
||||
// 검색 디바운싱
|
||||
@@ -50,10 +50,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_currentPage = 1;
|
||||
});
|
||||
context.read<UserListController>().setSearchQuery(query);
|
||||
context.read<UserListController>().setSearchQuery(query); // Controller가 페이지 리셋 처리
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,14 +204,9 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
);
|
||||
}
|
||||
|
||||
// 페이지네이션을 위한 데이터 처리
|
||||
final int totalUsers = controller.users.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex = startIndex + _pageSize;
|
||||
final List<User> pagedUsers = controller.users.sublist(
|
||||
startIndex,
|
||||
endIndex > totalUsers ? totalUsers : endIndex,
|
||||
);
|
||||
// Controller가 이미 페이징된 데이터를 제공
|
||||
final List<User> pagedUsers = controller.users; // 이미 페이징됨
|
||||
final int totalUsers = controller.total; // 실제 전체 개수
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
@@ -415,7 +407,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
)
|
||||
else
|
||||
...pagedUsers.asMap().entries.map((entry) {
|
||||
final int index = startIndex + entry.key;
|
||||
final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key;
|
||||
final User user = entry.value;
|
||||
|
||||
return Container(
|
||||
@@ -576,16 +568,19 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
),
|
||||
),
|
||||
|
||||
// 페이지네이션 컴포넌트
|
||||
if (totalUsers > _pageSize)
|
||||
// 페이지네이션 컴포넌트 (Controller 상태 사용)
|
||||
if (controller.total > controller.pageSize)
|
||||
Pagination(
|
||||
totalCount: totalUsers,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
// 다음 페이지 로드
|
||||
if (page > controller.currentPage) {
|
||||
controller.loadNextPage();
|
||||
} else if (page == 1) {
|
||||
controller.refresh();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -1,210 +0,0 @@
|
||||
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 ?? [];
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ class WarehouseLocationListController extends BaseListController<WarehouseLocati
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 사용
|
||||
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
// API 사용 (PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall(
|
||||
() => _warehouseService.getWarehouseLocations(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
@@ -43,21 +43,31 @@ class WarehouseLocationListController extends BaseListController<WarehouseLocati
|
||||
},
|
||||
);
|
||||
|
||||
final items = fetchedLocations ?? [];
|
||||
if (response == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
// PaginatedResponse를 PagedResult로 변환
|
||||
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,
|
||||
currentPage: response.page,
|
||||
perPage: response.size,
|
||||
total: response.totalElements,
|
||||
totalPages: response.totalPages,
|
||||
hasNext: !response.last,
|
||||
hasPrevious: !response.first,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
return PagedResult(items: response.items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'controllers/warehouse_location_form_controller.dart';
|
||||
|
||||
@@ -50,7 +50,7 @@ class _WarehouseLocationFormScreenState
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.isEditMode ? '입고지가 수정되었습니다' : '입고지가 추가되었습니다'),
|
||||
backgroundColor: AppThemeTailwind.success,
|
||||
backgroundColor: ShadcnTheme.success,
|
||||
),
|
||||
);
|
||||
// 리스트 화면으로 돌아가기
|
||||
@@ -62,7 +62,7 @@ class _WarehouseLocationFormScreenState
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error ?? '저장에 실패했습니다'),
|
||||
backgroundColor: AppThemeTailwind.danger,
|
||||
backgroundColor: ShadcnTheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,19 +14,18 @@ import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
||||
class WarehouseLocationListRedesign extends StatefulWidget {
|
||||
const WarehouseLocationListRedesign({Key? key}) : super(key: key);
|
||||
class WarehouseLocationList extends StatefulWidget {
|
||||
const WarehouseLocationList({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<WarehouseLocationListRedesign> createState() =>
|
||||
_WarehouseLocationListRedesignState();
|
||||
State<WarehouseLocationList> createState() =>
|
||||
_WarehouseLocationListState();
|
||||
}
|
||||
|
||||
class _WarehouseLocationListRedesignState
|
||||
extends State<WarehouseLocationListRedesign> {
|
||||
class _WarehouseLocationListState
|
||||
extends State<WarehouseLocationList> {
|
||||
late WarehouseLocationListController _controller;
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
// 페이지 상태는 이제 Controller에서 관리
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -46,8 +45,7 @@ class _WarehouseLocationListRedesignState
|
||||
|
||||
/// 리스트 새로고침
|
||||
void _reload() {
|
||||
_currentPage = 1;
|
||||
_controller.loadWarehouseLocations();
|
||||
_controller.refresh(); // Controller에서 페이지 리셋 처리
|
||||
}
|
||||
|
||||
/// 입고지 추가 폼으로 이동
|
||||
@@ -107,15 +105,9 @@ class _WarehouseLocationListRedesignState
|
||||
value: _controller,
|
||||
child: Consumer<WarehouseLocationListController>(
|
||||
builder: (context, controller, child) {
|
||||
final int totalCount = controller.warehouseLocations.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > totalCount
|
||||
? totalCount
|
||||
: (startIndex + _pageSize);
|
||||
final List<WarehouseLocation> pagedLocations = totalCount > 0 && startIndex < totalCount
|
||||
? controller.warehouseLocations.sublist(startIndex, endIndex)
|
||||
: [];
|
||||
// Controller가 이미 페이징된 데이터를 제공
|
||||
final List<WarehouseLocation> pagedLocations = controller.warehouseLocations;
|
||||
final int totalCount = controller.total; // 실제 전체 개수
|
||||
|
||||
return BaseListScreen(
|
||||
isLoading: controller.isLoading && controller.warehouseLocations.isEmpty,
|
||||
@@ -150,17 +142,15 @@ class _WarehouseLocationListRedesignState
|
||||
),
|
||||
|
||||
// 데이터 테이블
|
||||
dataTable: _buildDataTable(pagedLocations, startIndex),
|
||||
dataTable: _buildDataTable(pagedLocations),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: totalCount > _pageSize ? Pagination(
|
||||
pagination: totalCount > controller.pageSize ? Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
controller.goToPage(page);
|
||||
},
|
||||
) : null,
|
||||
);
|
||||
@@ -171,7 +161,7 @@ class _WarehouseLocationListRedesignState
|
||||
}
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<WarehouseLocation> pagedLocations, int startIndex) {
|
||||
Widget _buildDataTable(List<WarehouseLocation> pagedLocations) {
|
||||
if (pagedLocations.isEmpty) {
|
||||
return StandardEmptyState(
|
||||
title:
|
||||
@@ -257,7 +247,7 @@ class _WarehouseLocationListRedesignState
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${startIndex + index + 1}',
|
||||
'${(_controller.currentPage - 1) * _controller.pageSize + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Reference in New Issue
Block a user