refactor: UI 화면 통합 및 불필요한 파일 정리
- 모든 *_redesign.dart 파일을 기본 화면 파일로 통합 - 백업용 컨트롤러 파일들 제거 (*_controller.backup.dart) - 사용하지 않는 예제 및 테스트 파일 제거 - Clean Architecture 적용 후 남은 정리 작업 완료 - 테스트 코드 정리 및 구조 개선 준비 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -123,10 +123,10 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
void _loadWarehouseLocations() async {
|
||||
try {
|
||||
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final locations = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = locations.map((e) => e.name).toList();
|
||||
final response = await _warehouseService.getWarehouseLocations();
|
||||
warehouseLocations = response.items.map((e) => e.name).toList();
|
||||
// 이름-ID 매핑 저장
|
||||
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
||||
warehouseLocationMap = {for (var loc in response.items) loc.name: loc.id};
|
||||
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': warehouseLocations.length,
|
||||
'locations': warehouseLocations,
|
||||
@@ -146,8 +146,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
void _loadPartnerCompanies() async {
|
||||
try {
|
||||
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final companies = await _companyService.getCompanies();
|
||||
partnerCompanies = companies.map((c) => c.name).toList();
|
||||
final response = await _companyService.getCompanies();
|
||||
partnerCompanies = response.items.map((c) => c.name).toList();
|
||||
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'count': partnerCompanies.length,
|
||||
'companies': partnerCompanies,
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart' as legacy;
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
|
||||
// companyTypeToString 함수 import
|
||||
import 'package:superport/utils/constants.dart'
|
||||
show companyTypeToString, CompanyType;
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
|
||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentListController extends ChangeNotifier {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
String searchKeyword = ''; // 검색어 추가
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
// API만 사용
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
EquipmentListController();
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 호출 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📦 장비 목록 API 호출 시작');
|
||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||
search: search ?? searchKeyword,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 장비 목록 로드 완료');
|
||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
Map<String, int> statusCount = {};
|
||||
for (final dto in apiEquipmentDtos) {
|
||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||
}
|
||||
|
||||
statusCount.forEach((status, count) {
|
||||
print('║ • $status: $count개');
|
||||
});
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
|
||||
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer,
|
||||
name: dto.modelName ?? dto.equipmentNumber,
|
||||
category: '', // 세부 정보는 상세 조회에서 가져와야 함
|
||||
subCategory: '',
|
||||
subSubCategory: '',
|
||||
serialNumber: dto.serialNumber,
|
||||
quantity: 1,
|
||||
inDate: dto.createdAt,
|
||||
);
|
||||
|
||||
return UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt,
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||
);
|
||||
}).toList();
|
||||
|
||||
equipments = unifiedEquipments;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
|
||||
selectedEquipmentIds.clear();
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(String? status) async {
|
||||
selectedStatusFilter = status;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
await loadData(isRefresh: true, search: keyword);
|
||||
}
|
||||
|
||||
// 장비 선택/해제 (모든 상태 지원)
|
||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
// 출고 정보는 API를 통해 번별로 조회해야 하므로 별도 서비스로 분리 예정
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
// TODO: API로 출고 정보 조회 구현
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 장비 삭제
|
||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||
try {
|
||||
// API를 통한 삭제
|
||||
if (equipment.equipment.id != null) {
|
||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||
} else {
|
||||
throw Exception('Equipment ID is null');
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete equipment: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
}
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환
|
||||
final items = apiEquipmentDtos.map((dto) {
|
||||
final items = apiEquipmentDtos.items.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||
@@ -109,7 +109,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
(params.page * params.perPage) + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
|
||||
@@ -79,8 +79,8 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
Future<void> loadDropdownData() async {
|
||||
try {
|
||||
// API를 통해 회사 목록 로드
|
||||
final allCompanies = await _companyService.getCompanies();
|
||||
companies = allCompanies
|
||||
final response = await _companyService.getCompanies();
|
||||
companies = response.items
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
@@ -204,9 +204,9 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
|
||||
// 선택된 회사 정보에서 ID 추출
|
||||
if (selectedCompanies[0] != null) {
|
||||
final companies = await companyService.getCompanies(search: selectedCompanies[0]);
|
||||
if (companies.isNotEmpty) {
|
||||
companyId = companies.first.id;
|
||||
final response = await companyService.getCompanies(search: selectedCompanies[0]);
|
||||
if (response.items.isNotEmpty) {
|
||||
companyId = response.items.first.id;
|
||||
// TODO: 지점 ID 처리 로직 추가
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
// import 'package:superport/models/equipment_unified_model.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -2163,7 +2163,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
children: [
|
||||
Text(
|
||||
'${_controller.inDate.year}-${_controller.inDate.month.toString().padLeft(2, '0')}-${_controller.inDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
@@ -2258,7 +2258,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -2308,7 +2308,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../services/lookup_service.dart';
|
||||
import '../../data/models/lookups/lookup_data.dart';
|
||||
import '../common/theme_shadcn.dart';
|
||||
import '../common/components/shadcn_components.dart';
|
||||
|
||||
/// LookupService를 활용한 장비 입고 폼 예시
|
||||
/// 전역 캐싱된 Lookup 데이터를 활용하여 드롭다운 구성
|
||||
class EquipmentInFormLookupExample extends StatefulWidget {
|
||||
const EquipmentInFormLookupExample({super.key});
|
||||
|
||||
@override
|
||||
State<EquipmentInFormLookupExample> createState() => _EquipmentInFormLookupExampleState();
|
||||
}
|
||||
|
||||
class _EquipmentInFormLookupExampleState extends State<EquipmentInFormLookupExample> {
|
||||
late final LookupService _lookupService;
|
||||
|
||||
// 선택된 값들
|
||||
String? _selectedEquipmentType;
|
||||
String? _selectedEquipmentStatus;
|
||||
String? _selectedManufacturer;
|
||||
String? _selectedLicenseType;
|
||||
|
||||
// 텍스트 컨트롤러
|
||||
final _serialNumberController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lookupService = GetIt.instance<LookupService>();
|
||||
_loadLookupDataIfNeeded();
|
||||
}
|
||||
|
||||
/// 필요시 Lookup 데이터 로드 (캐시가 없을 경우)
|
||||
Future<void> _loadLookupDataIfNeeded() async {
|
||||
if (!_lookupService.hasData) {
|
||||
await _lookupService.loadAllLookups();
|
||||
if (mounted) {
|
||||
setState(() {}); // UI 업데이트
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serialNumberController.dispose();
|
||||
_quantityController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('장비 입고 (Lookup 활용 예시)'),
|
||||
backgroundColor: ShadcnTheme.card,
|
||||
elevation: 0,
|
||||
),
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _lookupService,
|
||||
child: Consumer<LookupService>(
|
||||
builder: (context, lookupService, child) {
|
||||
if (lookupService.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (lookupService.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
size: 64,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lookup 데이터 로드 실패',
|
||||
style: ShadcnTheme.headingH4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
lookupService.error!,
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 안내 메시지
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.1),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'이 화면은 /lookups API를 통해 캐싱된 전역 데이터를 활용합니다.\n'
|
||||
'드롭다운 데이터는 앱 시작 시 한 번만 로드되어 모든 화면에서 재사용됩니다.',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 폼 카드
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('장비 정보', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 장비 타입 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 타입',
|
||||
value: _selectedEquipmentType,
|
||||
items: lookupService.equipmentTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentType = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 장비 상태 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 상태',
|
||||
value: _selectedEquipmentStatus,
|
||||
items: lookupService.equipmentStatuses,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentStatus = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 제조사 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '제조사',
|
||||
value: _selectedManufacturer,
|
||||
items: lookupService.manufacturers,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedManufacturer = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 시리얼 번호 입력
|
||||
_buildTextField(
|
||||
label: '시리얼 번호',
|
||||
controller: _serialNumberController,
|
||||
hintText: 'SN-2025-001',
|
||||
),
|
||||
|
||||
// 수량 입력
|
||||
_buildTextField(
|
||||
label: '수량',
|
||||
controller: _quantityController,
|
||||
hintText: '1',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
// 라이선스 타입 드롭다운 (옵션)
|
||||
_buildDropdownField(
|
||||
label: '라이선스 타입 (선택)',
|
||||
value: _selectedLicenseType,
|
||||
items: lookupService.licenseTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedLicenseType = value;
|
||||
});
|
||||
},
|
||||
isOptional: true,
|
||||
),
|
||||
|
||||
// 비고 입력
|
||||
_buildTextField(
|
||||
label: '비고',
|
||||
controller: _descriptionController,
|
||||
hintText: '추가 정보를 입력하세요',
|
||||
maxLines: 3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 버튼 그룹
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '취소',
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadcnButton(
|
||||
text: '저장',
|
||||
onPressed: _handleSubmit,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 캐시 정보 표시
|
||||
_buildCacheInfoCard(lookupService),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 드롭다운 필드 빌더
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String? value,
|
||||
required List<LookupItem> items,
|
||||
required ValueChanged<String?> onChanged,
|
||||
bool isOptional = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
if (isOptional) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text('(선택)', style: ShadcnTheme.bodyMuted),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: '선택하세요',
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
items: items.map((item) => DropdownMenuItem(
|
||||
value: item.code ?? '',
|
||||
child: Text(item.name ?? ''),
|
||||
)).toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 텍스트 필드 빌더
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
String? hintText,
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.primary, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 캐시 정보 카드
|
||||
Widget _buildCacheInfoCard(LookupService lookupService) {
|
||||
return ShadcnCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.storage, size: 20, color: ShadcnTheme.muted),
|
||||
const SizedBox(width: 8),
|
||||
Text('Lookup 캐시 정보', style: ShadcnTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCacheItem('장비 타입', lookupService.equipmentTypes.length),
|
||||
_buildCacheItem('장비 상태', lookupService.equipmentStatuses.length),
|
||||
_buildCacheItem('제조사', lookupService.manufacturers.length),
|
||||
_buildCacheItem('라이선스 타입', lookupService.licenseTypes.length),
|
||||
_buildCacheItem('사용자 역할', lookupService.userRoles.length),
|
||||
_buildCacheItem('회사 상태', lookupService.companyStatuses.length),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'캐시 상태: ${lookupService.isCacheValid ? "유효" : "만료"}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: lookupService.isCacheValid
|
||||
? ShadcnTheme.success
|
||||
: ShadcnTheme.warning,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, size: 16, color: ShadcnTheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('캐시 새로고침',
|
||||
style: TextStyle(color: ShadcnTheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCacheItem(String label, int count) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodySmall),
|
||||
Text('$count개',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 폼 제출 처리
|
||||
void _handleSubmit() {
|
||||
// 유효성 검증
|
||||
if (_selectedEquipmentType == null) {
|
||||
_showSnackBar('장비 타입을 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedEquipmentStatus == null) {
|
||||
_showSnackBar('장비 상태를 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serialNumberController.text.isEmpty) {
|
||||
_showSnackBar('시리얼 번호를 입력하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 값 정보 표시
|
||||
final selectedType = _lookupService.findByCode(
|
||||
_lookupService.equipmentTypes,
|
||||
_selectedEquipmentType!,
|
||||
);
|
||||
|
||||
final selectedStatus = _lookupService.findByCode(
|
||||
_lookupService.equipmentStatuses,
|
||||
_selectedEquipmentStatus!,
|
||||
);
|
||||
|
||||
final message = '''
|
||||
장비 입고 정보:
|
||||
- 타입: ${selectedType?.name ?? _selectedEquipmentType}
|
||||
- 상태: ${selectedStatus?.name ?? _selectedEquipmentStatus}
|
||||
- 시리얼: ${_serialNumberController.text}
|
||||
- 수량: ${_quantityController.text.isEmpty ? "1" : _quantityController.text}
|
||||
''';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('입고 정보 확인'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError ? ShadcnTheme.destructive : ShadcnTheme.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,17 @@ import 'package:superport/utils/equipment_display_helper.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
||||
class EquipmentListRedesign extends StatefulWidget {
|
||||
class EquipmentList extends StatefulWidget {
|
||||
final String currentRoute;
|
||||
|
||||
const EquipmentListRedesign({Key? key, this.currentRoute = Routes.equipment})
|
||||
const EquipmentList({Key? key, this.currentRoute = Routes.equipment})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<EquipmentListRedesign> createState() => _EquipmentListRedesignState();
|
||||
State<EquipmentList> createState() => _EquipmentListState();
|
||||
}
|
||||
|
||||
class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
class _EquipmentListState extends State<EquipmentList> {
|
||||
late final EquipmentListController _controller;
|
||||
bool _showDetailedColumns = true;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
@@ -34,14 +34,14 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
String _selectedStatus = 'all';
|
||||
// String _searchKeyword = ''; // Removed - unused field
|
||||
String _appliedSearchKeyword = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
// 페이지 상태는 이제 Controller에서 관리
|
||||
final Set<int> _selectedItems = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController();
|
||||
_controller.pageSize = 10; // 페이지 크기 설정
|
||||
_setInitialFilter();
|
||||
|
||||
// API 호출을 위해 Future로 변경
|
||||
@@ -113,7 +113,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
} else if (status == 'rent') {
|
||||
_controller.selectedStatusFilter = EquipmentStatus.rent;
|
||||
}
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
void _onSearch() async {
|
||||
setState(() {
|
||||
_appliedSearchKeyword = _searchController.text;
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
_controller.updateSearchKeyword(_searchController.text);
|
||||
}
|
||||
@@ -414,14 +414,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
dataTable: _buildDataTable(filteredEquipments),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: totalCount > _pageSize ? Pagination(
|
||||
pagination: totalCount > controller.pageSize ? Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
controller.goToPage(page);
|
||||
},
|
||||
) : null,
|
||||
);
|
||||
@@ -515,7 +513,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
onRefresh: () {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
},
|
||||
statusMessage:
|
||||
@@ -548,7 +546,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -623,7 +621,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -829,7 +827,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_currentPage - 1) * _pageSize) + index + 1}',
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
@@ -946,11 +944,11 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<UnifiedEquipment> filteredEquipments) {
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > filteredEquipments.length
|
||||
(startIndex + _controller.pageSize) > filteredEquipments.length
|
||||
? filteredEquipments.length
|
||||
: (startIndex + _pageSize);
|
||||
: (startIndex + _controller.pageSize);
|
||||
final List<UnifiedEquipment> pagedEquipments = filteredEquipments.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
@@ -975,7 +973,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1176,8 +1174,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
/// 페이지 데이터 가져오기
|
||||
List<UnifiedEquipment> _getPagedEquipments() {
|
||||
final filteredEquipments = _getFilteredEquipments();
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex = startIndex + _pageSize;
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex = startIndex + _controller.pageSize;
|
||||
|
||||
if (startIndex >= filteredEquipments.length) {
|
||||
return [];
|
||||
@@ -6,7 +6,7 @@ import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
@@ -421,7 +421,10 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
: null,
|
||||
style:
|
||||
canSubmit
|
||||
? AppThemeTailwind.primaryButtonStyle
|
||||
? ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
)
|
||||
: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
@@ -600,7 +603,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
company.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${company.contactName} ${company.contactPosition ?? ""} ${company.contactPhone ?? ""} ${company.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
if (!companyInfo.isMainCompany &&
|
||||
branch != null &&
|
||||
@@ -608,7 +611,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
branch.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${branch.contactName} ${branch.contactPosition ?? ""} ${branch.contactPhone ?? ""} ${branch.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 담당자 목록에서 실제 담당자 정보만 표시하는 부분은 제거
|
||||
@@ -686,7 +689,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
children: [
|
||||
Text(
|
||||
controller.formatDate(date),
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
@@ -817,7 +820,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
userWidgets.add(
|
||||
Text(
|
||||
'정수진 사원 010-4567-8901 jung.soojin@lg.com',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user