feat: 장비 관리 API 통합 완료
- 장비 출고 API 연동 및 Provider 패턴 적용 - 장비 수정 API 연동 (데이터 로드 시 API 사용) - 장비 삭제 API 연동 (Controller 메서드 추가) - 장비 이력 조회 화면 추가 및 API 연동 - 모든 컨트롤러에 ChangeNotifier 패턴 적용 - 에러 처리 및 로딩 상태 관리 개선 - API/Mock 데이터 전환 가능 (Feature Flag) 진행률: 전체 API 통합 70%, 장비 관리 100% 완료
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/address_model.dart';
|
||||
@@ -53,10 +54,16 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 요약 테이블 위젯 - 다중 선택 장비에 대한 요약 테이블
|
||||
Widget _buildSummaryTable() {
|
||||
if (_controller.selectedEquipments == null ||
|
||||
_controller.selectedEquipments!.isEmpty) {
|
||||
Widget _buildSummaryTable(EquipmentOutFormController controller) {
|
||||
if (controller.selectedEquipments == null ||
|
||||
controller.selectedEquipments!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -72,7 +79,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'선택된 장비 목록 (${_controller.selectedEquipments!.length}개)',
|
||||
'선택된 장비 목록 (${controller.selectedEquipments!.length}개)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -122,10 +129,10 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
const Divider(),
|
||||
// 리스트 본문
|
||||
Column(
|
||||
children: List.generate(_controller.selectedEquipments!.length, (
|
||||
children: List.generate(controller.selectedEquipments!.length, (
|
||||
index,
|
||||
) {
|
||||
final equipmentData = _controller.selectedEquipments![index];
|
||||
final equipmentData = controller.selectedEquipments![index];
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
// 워런티 날짜를 임시로 저장할 수 있도록 상태를 관리(컨트롤러에 리스트로 추가하거나, 여기서 임시로 관리)
|
||||
// 여기서는 equipment 객체의 필드를 직접 수정(실제 서비스에서는 별도 상태 관리 필요)
|
||||
@@ -149,9 +156,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
equipment.warrantyStartDate = picked;
|
||||
});
|
||||
equipment.warrantyStartDate = picked;
|
||||
controller.notifyListeners();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -185,9 +191,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
equipment.warrantyEndDate = picked;
|
||||
});
|
||||
equipment.warrantyEndDate = picked;
|
||||
controller.notifyListeners();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -229,18 +234,75 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 담당자가 없거나 첫 번째 회사에 대한 담당자가 '없음'인 경우 등록 버튼 비활성화 조건
|
||||
final bool canSubmit =
|
||||
_controller.selectedCompanies.isNotEmpty &&
|
||||
_controller.selectedCompanies[0] != null &&
|
||||
_controller.hasManagersPerCompany[0] &&
|
||||
_controller.filteredManagersPerCompany[0].first != '없음';
|
||||
final int totalSelectedEquipments =
|
||||
_controller.selectedEquipments?.length ?? 0;
|
||||
return Scaffold(
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<EquipmentOutFormController>(
|
||||
builder: (context, controller, child) {
|
||||
// 담당자가 없거나 첫 번째 회사에 대한 담당자가 '없음'인 경우 등록 버튼 비활성화 조건
|
||||
final bool canSubmit =
|
||||
controller.selectedCompanies.isNotEmpty &&
|
||||
controller.selectedCompanies[0] != null &&
|
||||
controller.hasManagersPerCompany[0] &&
|
||||
controller.filteredManagersPerCompany[0].first != '없음';
|
||||
final int totalSelectedEquipments =
|
||||
controller.selectedEquipments?.length ?? 0;
|
||||
|
||||
// 로딩 상태 처리
|
||||
if (controller.isLoading) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('장비 출고'),
|
||||
),
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 에러 상태 처리
|
||||
if (controller.errorMessage != null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('장비 출고'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'오류가 발생했습니다',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.errorMessage!,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.clearError();
|
||||
controller.loadDropdownData();
|
||||
},
|
||||
child: const Text('다시 시도'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_controller.isEditMode
|
||||
controller.isEditMode
|
||||
? '장비 출고 수정'
|
||||
: totalSelectedEquipments > 0
|
||||
? '장비 출고 등록 (${totalSelectedEquipments}개)'
|
||||
@@ -250,21 +312,21 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
key: controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 장비 정보 요약 섹션
|
||||
if (_controller.selectedEquipments != null &&
|
||||
_controller.selectedEquipments!.isNotEmpty)
|
||||
_buildSummaryTable()
|
||||
else if (_controller.selectedEquipment != null)
|
||||
if (controller.selectedEquipments != null &&
|
||||
controller.selectedEquipments!.isNotEmpty)
|
||||
_buildSummaryTable(controller)
|
||||
else if (controller.selectedEquipment != null)
|
||||
// 단일 장비 요약 카드도 전체 폭으로 맞춤
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: EquipmentSingleSummaryCard(
|
||||
equipment: _controller.selectedEquipment!,
|
||||
equipment: controller.selectedEquipment!,
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -272,27 +334,27 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 요약 카드 아래 라디오 버튼 추가
|
||||
const SizedBox(height: 12),
|
||||
// 전체 폭을 사용하는 라디오 버튼
|
||||
Container(width: double.infinity, child: _buildOutTypeRadio()),
|
||||
Container(width: double.infinity, child: _buildOutTypeRadio(controller)),
|
||||
const SizedBox(height: 16),
|
||||
// 출고 정보 입력 섹션 (수정/등록)
|
||||
_buildOutgoingInfoSection(context),
|
||||
_buildOutgoingInfoSection(context, controller),
|
||||
// 비고 입력란 추가
|
||||
const SizedBox(height: 16),
|
||||
FormFieldWrapper(
|
||||
label: '비고',
|
||||
isRequired: false,
|
||||
child: RemarkInput(
|
||||
controller: _controller.remarkController,
|
||||
controller: controller.remarkController,
|
||||
hint: '비고를 입력하세요',
|
||||
minLines: 4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 담당자 없음 경고 메시지
|
||||
if (_controller.selectedCompanies.isNotEmpty &&
|
||||
_controller.selectedCompanies[0] != null &&
|
||||
(!_controller.hasManagersPerCompany[0] ||
|
||||
_controller.filteredManagersPerCompany[0].first ==
|
||||
if (controller.selectedCompanies.isNotEmpty &&
|
||||
controller.selectedCompanies[0] != null &&
|
||||
(!controller.hasManagersPerCompany[0] ||
|
||||
controller.filteredManagersPerCompany[0].first ==
|
||||
'없음'))
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -325,26 +387,26 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 각 회사별 담당자를 첫 번째 항목으로 설정
|
||||
for (
|
||||
int i = 0;
|
||||
i < _controller.selectedCompanies.length;
|
||||
i < controller.selectedCompanies.length;
|
||||
i++
|
||||
) {
|
||||
if (_controller.selectedCompanies[i] != null &&
|
||||
_controller.hasManagersPerCompany[i] &&
|
||||
_controller
|
||||
if (controller.selectedCompanies[i] != null &&
|
||||
controller.hasManagersPerCompany[i] &&
|
||||
controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.isNotEmpty &&
|
||||
_controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.first !=
|
||||
'없음') {
|
||||
_controller.selectedManagersPerCompany[i] =
|
||||
_controller
|
||||
controller.selectedManagersPerCompany[i] =
|
||||
controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.first;
|
||||
}
|
||||
}
|
||||
|
||||
_controller.saveEquipmentOut(
|
||||
controller.saveEquipmentOut(
|
||||
(msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -375,7 +437,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
_controller.isEditMode ? '수정하기' : '등록하기',
|
||||
controller.isEditMode ? '수정하기' : '등록하기',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
@@ -386,11 +448,14 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 출고 정보 입력 섹션 위젯 (등록/수정 공통)
|
||||
Widget _buildOutgoingInfoSection(BuildContext context) {
|
||||
Widget _buildOutgoingInfoSection(BuildContext context, EquipmentOutFormController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -399,12 +464,11 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 출고일
|
||||
_buildDateField(
|
||||
context,
|
||||
controller,
|
||||
label: '출고일',
|
||||
date: _controller.outDate,
|
||||
date: controller.outDate,
|
||||
onDateChanged: (picked) {
|
||||
setState(() {
|
||||
_controller.outDate = picked;
|
||||
});
|
||||
controller.outDate = picked;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -415,9 +479,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
const Text('출고 회사', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.addCompany();
|
||||
});
|
||||
controller.addCompany();
|
||||
},
|
||||
icon: const Icon(Icons.add_circle_outline, size: 18),
|
||||
label: const Text('출고 회사 추가'),
|
||||
@@ -432,24 +494,24 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 동적 출고 회사 드롭다운 목록
|
||||
...List.generate(_controller.selectedCompanies.length, (index) {
|
||||
...List.generate(controller.selectedCompanies.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _controller.selectedCompanies[index],
|
||||
value: controller.selectedCompanies[index],
|
||||
decoration: InputDecoration(
|
||||
hintText: index == 0 ? '출고할 회사를 선택하세요' : '추가된 출고할 회사를 선택하세요',
|
||||
// 이전 드롭다운에 값이 선택되지 않았으면 비활성화
|
||||
enabled:
|
||||
index == 0 ||
|
||||
_controller.selectedCompanies[index - 1] != null,
|
||||
controller.selectedCompanies[index - 1] != null,
|
||||
),
|
||||
items:
|
||||
_controller.availableCompaniesPerDropdown[index]
|
||||
controller.availableCompaniesPerDropdown[index]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: _buildCompanyDropdownItem(item),
|
||||
child: _buildCompanyDropdownItem(item, controller),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@@ -461,16 +523,14 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
},
|
||||
onChanged:
|
||||
(index == 0 ||
|
||||
_controller.selectedCompanies[index - 1] != null)
|
||||
controller.selectedCompanies[index - 1] != null)
|
||||
? (value) {
|
||||
setState(() {
|
||||
_controller.selectedCompanies[index] = value;
|
||||
_controller.filterManagersByCompanyAtIndex(
|
||||
controller.selectedCompanies[index] = value;
|
||||
controller.filterManagersByCompanyAtIndex(
|
||||
value,
|
||||
index,
|
||||
);
|
||||
_controller.updateAvailableCompanies();
|
||||
});
|
||||
controller.updateAvailableCompanies();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -478,17 +538,17 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
}),
|
||||
|
||||
// 각 회사별 담당자 선택 목록
|
||||
...List.generate(_controller.selectedCompanies.length, (index) {
|
||||
...List.generate(controller.selectedCompanies.length, (index) {
|
||||
// 회사가 선택된 경우에만 담당자 표시
|
||||
if (_controller.selectedCompanies[index] != null) {
|
||||
if (controller.selectedCompanies[index] != null) {
|
||||
// 회사 정보 가져오기
|
||||
final companyInfo = _controller.companiesWithBranches.firstWhere(
|
||||
(info) => info.name == _controller.selectedCompanies[index],
|
||||
final companyInfo = controller.companiesWithBranches.firstWhere(
|
||||
(info) => info.name == controller.selectedCompanies[index],
|
||||
orElse:
|
||||
() => CompanyBranchInfo(
|
||||
id: 0,
|
||||
name: _controller.selectedCompanies[index]!,
|
||||
originalName: _controller.selectedCompanies[index]!,
|
||||
name: controller.selectedCompanies[index]!,
|
||||
originalName: controller.selectedCompanies[index]!,
|
||||
isMainCompany: true,
|
||||
companyId: 0,
|
||||
branchId: null,
|
||||
@@ -500,7 +560,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
Branch? branch;
|
||||
|
||||
if (companyInfo.companyId != null) {
|
||||
company = _controller.dataService.getCompanyById(
|
||||
company = controller.dataService.getCompanyById(
|
||||
companyInfo.companyId!,
|
||||
);
|
||||
if (!companyInfo.isMainCompany &&
|
||||
@@ -526,7 +586,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'담당자 정보 (${_controller.selectedCompanies[index]})',
|
||||
'담당자 정보 (${controller.selectedCompanies[index]})',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -584,13 +644,11 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 유지 보수(라이센스) 선택
|
||||
_buildDropdownField(
|
||||
label: '유지 보수', // 텍스트 변경
|
||||
value: _controller.selectedLicense,
|
||||
items: _controller.licenses,
|
||||
value: controller.selectedLicense,
|
||||
items: controller.licenses,
|
||||
hint: '유지 보수를 선택하세요', // 텍스트 변경
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedLicense = value;
|
||||
});
|
||||
controller.selectedLicense = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
@@ -605,7 +663,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
|
||||
// 날짜 선택 필드 위젯
|
||||
Widget _buildDateField(
|
||||
BuildContext context, {
|
||||
BuildContext context,
|
||||
EquipmentOutFormController controller, {
|
||||
required String label,
|
||||
required DateTime date,
|
||||
required ValueChanged<DateTime> onDateChanged,
|
||||
@@ -637,7 +696,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_controller.formatDate(date),
|
||||
controller.formatDate(date),
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
@@ -685,7 +744,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
}
|
||||
|
||||
// 회사 이름을 표시하는 위젯 (지점 포함)
|
||||
Widget _buildCompanyDropdownItem(String item) {
|
||||
Widget _buildCompanyDropdownItem(String item, EquipmentOutFormController controller) {
|
||||
final TextStyle defaultStyle = TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 14,
|
||||
@@ -694,7 +753,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
|
||||
// 컨트롤러에서 해당 항목에 대한 정보 확인
|
||||
final companyInfoList =
|
||||
_controller.companiesWithBranches
|
||||
controller.companiesWithBranches
|
||||
.where((info) => info.name == item)
|
||||
.toList();
|
||||
|
||||
@@ -778,7 +837,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
}
|
||||
|
||||
// 출고/대여/폐기 라디오 버튼 위젯
|
||||
Widget _buildOutTypeRadio() {
|
||||
Widget _buildOutTypeRadio(EquipmentOutFormController controller) {
|
||||
// 출고 유형 리스트
|
||||
final List<String> outTypes = ['출고', '대여', '폐기'];
|
||||
return Row(
|
||||
@@ -789,11 +848,9 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: type,
|
||||
groupValue: _controller.outType, // 컨트롤러에서 현재 선택값 관리
|
||||
groupValue: controller.outType, // 컨트롤러에서 현재 선택값 관리
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.outType = value!;
|
||||
});
|
||||
controller.outType = value!;
|
||||
},
|
||||
),
|
||||
Text(type),
|
||||
|
||||
Reference in New Issue
Block a user