feat: API 연동 개선 및 라이선스 모델 확장

- 라이선스 모델 전면 개편 (상세 필드 추가, 계산 필드 구현)
- API 응답 처리 개선 (HTTP 상태 코드 기반)
- 장비 출고 폼 컨트롤러 추가
- 회사 지점 정보 모델 추가
- 공통 데이터 모델 구조 추가
- 전체 서비스 레이어 API 호출 방식 통일
- UI 컴포넌트 마이너 개선
This commit is contained in:
JiWoong Sul
2025-07-25 01:22:15 +09:00
parent 8384423cf2
commit 71b7b7f40b
42 changed files with 1543 additions and 315 deletions

View File

@@ -171,7 +171,7 @@ class EquipmentInFormController extends ChangeNotifier {
// API 실패 시 Mock 데이터 사용
_loadFromMockData(equipmentIn);
}
} else {
} else if (equipmentIn != null) {
_loadFromMockData(equipmentIn);
}
} else {
@@ -311,11 +311,15 @@ class EquipmentInFormController extends ChangeNotifier {
int? warehouseLocationId;
if (warehouseLocation != null) {
// TODO: 창고 위치 ID 가져오기 - 현재는 목 데이터에서 찾기
final warehouse = dataService.getAllWarehouseLocations().firstWhere(
(w) => w.name == warehouseLocation,
orElse: () => null,
);
warehouseLocationId = warehouse?.id;
try {
final warehouse = dataService.getAllWarehouseLocations().firstWhere(
(w) => w.name == warehouseLocation,
);
warehouseLocationId = warehouse.id;
} catch (e) {
// 창고를 찾을 수 없는 경우
warehouseLocationId = null;
}
}
await _equipmentService.equipmentIn(

View File

@@ -63,13 +63,12 @@ class EquipmentListController extends ChangeNotifier {
);
// API 모델을 UnifiedEquipment로 변환
final unifiedEquipments = apiEquipments.map((equipment) {
final List<UnifiedEquipment> unifiedEquipments = apiEquipments.map((equipment) {
return UnifiedEquipment(
id: equipment.id,
equipment: equipment,
quantity: equipment.quantity,
date: DateTime.now(), // 실제로는 API에서 날짜 정보를 가져와야 함
status: EquipmentStatus.in_, // 기본값, 실제로는 API에서 가져와야 함
locationTrack: LocationTrack.inStock,
);
}).toList();

View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/models/company_branch_info.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/utils/constants.dart';
/// 장비 출고 폼 컨트롤러
///
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
class EquipmentOutFormController extends ChangeNotifier {
final MockDataService dataService;
int? equipmentOutId;
// 편집 모드 여부
bool isEditMode = false;
// 상태 관리
bool _isLoading = false;
String? _errorMessage;
// Getters
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// 폼 키
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
// 선택된 장비 정보
Equipment? selectedEquipment;
int? selectedEquipmentInId;
List<Map<String, dynamic>>? selectedEquipments;
// 출고처 정보
List<String?> selectedCompanies = [null];
List<bool> hasManagersPerCompany = [false];
List<List<String>> filteredManagersPerCompany = [[]];
List<String?> selectedManagersPerCompany = [null];
// 라이선스 정보
String? selectedLicense;
// 출고 타입
String outType = 'O'; // 기본값: 출고
// 드롭다운 데이터
List<CompanyBranchInfo> companies = [];
List<String> licenses = [];
// 날짜
DateTime outDate = DateTime.now();
// 회사 정보 (지점 포함)
List<CompanyBranchInfo> get companiesWithBranches => companies;
// 비고
final TextEditingController remarkController = TextEditingController();
EquipmentOutFormController({
required this.dataService,
this.equipmentOutId,
}) {
isEditMode = equipmentOutId != null;
}
// 이용 가능한 회사 목록 (선택된 회사 제외)
List<List<CompanyBranchInfo>> get availableCompaniesPerDropdown {
return List.generate(selectedCompanies.length, (index) {
final selectedBefore = selectedCompanies.sublist(0, index).whereType<String>().toSet();
return companies.where((company) => !selectedBefore.contains(company.name)).toList();
});
}
// 드롭다운 데이터 로드
void loadDropdownData() {
// 회사 목록 로드 (출고처 가능한 회사만)
companies = dataService.getAllCompanies()
.where((c) => c.companyTypes.contains(CompanyType.customer))
.map((c) => CompanyBranchInfo(
id: c.id,
name: c.name,
originalName: c.name,
isMainCompany: true,
companyId: c.id,
branchId: null,
))
.toList();
// 라이선스 목록 로드
licenses = dataService.getAllLicenses().map((l) => l.name).toList();
}
// 선택된 장비로 초기화
void initializeWithSelectedEquipment(Equipment equipment) {
selectedEquipment = equipment;
notifyListeners();
}
// 회사 선택 시 담당자 목록 필터링
void filterManagersForCompany(int index) {
if (index >= selectedCompanies.length || selectedCompanies[index] == null) {
hasManagersPerCompany[index] = false;
filteredManagersPerCompany[index] = [];
return;
}
// Mock 데이터에서 회사별 담당자 목록 가져오기
final company = dataService.getAllCompanies().firstWhere(
(c) => c.name == selectedCompanies[index],
orElse: () => Company(
name: '',
companyTypes: [],
),
);
if (company.name.isNotEmpty && company.contactName != null && company.contactName!.isNotEmpty) {
// 회사의 담당자 정보
hasManagersPerCompany[index] = true;
filteredManagersPerCompany[index] = [company.contactName!];
} else {
hasManagersPerCompany[index] = false;
filteredManagersPerCompany[index] = ['없음'];
}
notifyListeners();
}
// 인덱스별 담당자 필터링
void filterManagersByCompanyAtIndex(int index) {
filterManagersForCompany(index);
}
// 회사 추가
void addCompany() {
selectedCompanies.add(null);
hasManagersPerCompany.add(false);
filteredManagersPerCompany.add([]);
selectedManagersPerCompany.add(null);
notifyListeners();
}
// 회사 제거
void removeCompany(int index) {
if (selectedCompanies.length > 1) {
selectedCompanies.removeAt(index);
hasManagersPerCompany.removeAt(index);
filteredManagersPerCompany.removeAt(index);
selectedManagersPerCompany.removeAt(index);
notifyListeners();
}
}
// 회사 선택 초기화
void resetCompanySelection() {
selectedCompanies = [null];
hasManagersPerCompany = [false];
filteredManagersPerCompany = [[]];
selectedManagersPerCompany = [null];
notifyListeners();
}
// 에러 초기화
void clearError() {
_errorMessage = null;
notifyListeners();
}
// 이용 가능한 회사 목록 업데이트
void updateAvailableCompanies() {
notifyListeners();
}
// 날짜 포맷팅
String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
// 출고 정보 저장
Future<bool> saveEquipmentOut(BuildContext context, {String? note}) async {
// 유효성 검사
if (selectedCompanies.isEmpty || selectedCompanies[0] == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('출고처를 선택해주세요.')),
);
return false;
}
if (selectedEquipment == null && (selectedEquipments == null || selectedEquipments!.isEmpty)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('출고할 장비를 선택해주세요.')),
);
return false;
}
try {
// TODO: 실제 저장 로직 구현
// 현재는 Mock 데이터 서비스에 저장
if (isEditMode) {
// 수정 모드
// dataService.updateEquipmentOut(...)
} else {
// 생성 모드
if (selectedEquipments != null && selectedEquipments!.isNotEmpty) {
// 다중 장비 출고
for (var equipmentData in selectedEquipments!) {
// dataService.addEquipmentOut(...)
}
} else if (selectedEquipment != null) {
// 단일 장비 출고
// dataService.addEquipmentOut(...)
}
}
return true;
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('저장 중 오류가 발생했습니다: $e')),
);
return false;
}
}
@override
void dispose() {
remarkController.dispose();
super.dispose();
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/models/company_branch_info.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
@@ -406,25 +407,17 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
}
}
controller.saveEquipmentOut(
(msg) {
controller.saveEquipmentOut(context).then((success) {
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
duration: const Duration(seconds: 2),
const SnackBar(
content: Text('출고가 완료되었습니다.'),
duration: Duration(seconds: 2),
),
);
Navigator.pop(context, true);
},
(err) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(err),
duration: const Duration(seconds: 2),
),
);
},
);
}
});
}
: null,
style:
@@ -510,8 +503,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
controller.availableCompaniesPerDropdown[index]
.map(
(item) => DropdownMenuItem<String>(
value: item,
child: _buildCompanyDropdownItem(item, controller),
value: item.name,
child: _buildCompanyDropdownItem(item.name, controller),
),
)
.toList(),
@@ -526,10 +519,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
controller.selectedCompanies[index - 1] != null)
? (value) {
controller.selectedCompanies[index] = value;
controller.filterManagersByCompanyAtIndex(
value,
index,
);
controller.filterManagersByCompanyAtIndex(index);
controller.updateAvailableCompanies();
}
: null,