refactor: UI 화면 통합 및 불필요한 파일 정리
Some checks failed
Flutter Test & Quality Check / Build APK (push) Has been cancelled
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled

- 모든 *_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:
JiWoong Sul
2025-08-11 14:00:44 +09:00
parent 162fe08618
commit 1e6da44917
103 changed files with 1224 additions and 2976 deletions

View File

@@ -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,

View File

@@ -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> {
),
),
),
),
],
);
}

View File

@@ -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),
],

View File

@@ -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,
);
}

View File

@@ -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,
),
),

View File

@@ -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),

View File

@@ -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> {
),
);
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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),

View File

@@ -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';

View File

@@ -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(

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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 처리 로직 추가
}
}

View File

@@ -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,
),
),

View File

@@ -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),
),
);
}
}

View File

@@ -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 [];

View File

@@ -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,
),
);
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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(

View File

@@ -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,
);

View File

@@ -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>
],
);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
};
}
}
}

View File

@@ -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');

View File

@@ -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만 사용하므로 토글 기능 제거
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}
},
),
],

View File

@@ -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 ?? [];
}
}

View File

@@ -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

View File

@@ -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,
),
);
}

View File

@@ -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,
),
),