## 주요 변경사항 - Company, Equipment, License, Warehouse Location 모든 화면에 소프트 딜리트 구현 - 관리자 권한으로 삭제된 데이터 조회 가능 (includeInactive 파라미터) - 데이터 무결성 보장을 위한 논리 삭제 시스템 완성 ## 기능 개선 - 각 리스트 컨트롤러에 toggleIncludeInactive() 메서드 추가 - UI에 "비활성 포함" 체크박스 추가 (관리자 전용) - API 데이터소스에 includeInactive 파라미터 지원 ## 문서 정리 - 불필요한 문서 파일 제거 및 재구성 - CLAUDE.md 프로젝트 상태 업데이트 (진행률 80%) - 테스트 결과 문서화 (test20250812v01.md) ## UI 컴포넌트 - Equipment 화면 위젯 모듈화 (custom_dropdown_field, equipment_basic_info_section) - 폼 유효성 검증 강화 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
531 lines
22 KiB
Dart
531 lines
22 KiB
Dart
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_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';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:superport/core/config/environment.dart' as env;
|
|
|
|
// 유지보수 등록/수정 화면 (UI만 담당, 상태/로직 분리)
|
|
class MaintenanceFormScreen extends StatefulWidget {
|
|
final int? maintenanceId;
|
|
final bool isExtension; // 연장 모드 여부
|
|
const MaintenanceFormScreen({
|
|
Key? key,
|
|
this.maintenanceId,
|
|
this.isExtension = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
_MaintenanceFormScreenState createState() => _MaintenanceFormScreenState();
|
|
}
|
|
|
|
class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
|
|
late final LicenseFormController _controller;
|
|
// 방문주기 드롭다운 옵션
|
|
final List<String> _visitCycleOptions = [
|
|
'미방문',
|
|
'장애시 지원',
|
|
'월',
|
|
'격월',
|
|
'분기',
|
|
'반기',
|
|
'년',
|
|
];
|
|
// 점검형태 라디오 옵션
|
|
final List<String> _inspectionTypeOptions = ['방문', '원격'];
|
|
String _selectedVisitCycle = '미방문';
|
|
String _selectedInspectionType = '방문';
|
|
int _durationMonths = 12;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// API 모드 확인
|
|
final useApi = env.Environment.useApi;
|
|
debugPrint('📌 라이선스 폼 초기화 - API 모드: $useApi');
|
|
|
|
_controller = LicenseFormController(
|
|
licenseId: widget.maintenanceId,
|
|
isExtension: widget.isExtension,
|
|
);
|
|
|
|
// 컨트롤러 변경 리스너 등록 (데이터 로드 전에 등록!)
|
|
_controller.addListener(_handleControllerUpdate);
|
|
|
|
// 수정 모드 또는 연장 모드일 때
|
|
if (widget.maintenanceId != null) {
|
|
// 초기 데이터 로드
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
if (widget.isExtension) {
|
|
// 연장 모드: 기존 데이터를 로드하되 새로운 라이선스로 생성
|
|
_controller.isEditMode = false;
|
|
await _controller.loadLicenseForExtension();
|
|
} else {
|
|
// 수정 모드: 기존 라이선스 수정
|
|
_controller.isEditMode = true;
|
|
await _controller.loadLicense();
|
|
}
|
|
|
|
// 데이터 로드 후 UI 업데이트
|
|
if (mounted) {
|
|
setState(() {
|
|
// 로드된 데이터로 상태 업데이트
|
|
_selectedVisitCycle = _controller.visitCycle;
|
|
_durationMonths = _controller.durationMonths;
|
|
// 폼 필드들은 컨트롤러의 TextEditingController를 통해 자동 업데이트됨
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_handleControllerUpdate);
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleControllerUpdate() {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
// 저장 메소드
|
|
Future<void> _onSave() async {
|
|
if (_controller.formKey.currentState!.validate()) {
|
|
_controller.formKey.currentState!.save();
|
|
await _controller.saveLicense();
|
|
if (mounted) {
|
|
String message = widget.isExtension
|
|
? '유지보수가 연장되었습니다'
|
|
: (_controller.isEditMode ? '유지보수가 수정되었습니다' : '유지보수가 등록되었습니다');
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: ShadcnTheme.success,
|
|
),
|
|
);
|
|
Navigator.pop(context, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 유지보수 명은 유지보수기간, 방문주기, 점검형태를 결합해서 표기
|
|
final String maintenanceName =
|
|
'${_durationMonths}개월,${_selectedVisitCycle},${_selectedInspectionType}';
|
|
|
|
return FormLayoutTemplate(
|
|
title: widget.isExtension
|
|
? '유지보수 연장'
|
|
: (_controller.isEditMode ? '유지보수 수정' : '유지보수 등록'),
|
|
onSave: _onSave,
|
|
saveButtonText: widget.isExtension
|
|
? '연장 완료'
|
|
: (_controller.isEditMode ? '수정 완료' : '등록 완료'),
|
|
child: _controller.isLoading
|
|
? const Center(
|
|
child: CircularProgressIndicator(),
|
|
)
|
|
: Form(
|
|
key: _controller.formKey,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(UIConstants.formPadding),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 수정 모드일 때 안내 메시지
|
|
if (_controller.isEditMode)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.amber.shade50,
|
|
border: Border.all(color: Colors.amber.shade200),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.info_outline, color: Colors.amber.shade700, size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'라이선스 키, 현위치, 할당 사용자, 구매일은 보안상 수정할 수 없습니다.',
|
|
style: TextStyle(
|
|
color: Colors.amber.shade900,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 기본 정보 섹션
|
|
FormSection(
|
|
title: '기본 정보',
|
|
subtitle: '유지보수의 기본 정보를 입력하세요',
|
|
children: [
|
|
// 제품명
|
|
FormFieldWrapper(
|
|
label: '제품명',
|
|
required: true,
|
|
child: TextFormField(
|
|
controller: _controller.productNameController,
|
|
decoration: const InputDecoration(
|
|
hintText: '제품명을 입력하세요',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (value) => validateRequired(value, '제품명'),
|
|
),
|
|
),
|
|
// 라이선스 키
|
|
FormFieldWrapper(
|
|
label: '라이선스 키',
|
|
required: true,
|
|
child: TextFormField(
|
|
controller: _controller.licenseKeyController,
|
|
readOnly: _controller.isEditMode, // 수정 모드에서 읽기 전용
|
|
decoration: InputDecoration(
|
|
hintText: '라이선스 키를 입력하세요',
|
|
border: OutlineInputBorder(),
|
|
filled: _controller.isEditMode,
|
|
fillColor: _controller.isEditMode ? Colors.grey.shade100 : null,
|
|
suffixIcon: _controller.isEditMode
|
|
? Tooltip(
|
|
message: '라이선스 키는 수정할 수 없습니다',
|
|
child: Icon(Icons.lock_outline, color: Colors.grey.shade600, size: 20),
|
|
)
|
|
: null,
|
|
),
|
|
validator: (value) => validateRequired(value, '라이선스 키'),
|
|
),
|
|
),
|
|
// 벤더
|
|
FormFieldWrapper(
|
|
label: '벤더',
|
|
required: true,
|
|
child: TextFormField(
|
|
controller: _controller.vendorController,
|
|
decoration: const InputDecoration(
|
|
hintText: '벤더명을 입력하세요',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (value) => validateRequired(value, '벤더'),
|
|
),
|
|
),
|
|
// 현위치
|
|
FormFieldWrapper(
|
|
label: '현위치',
|
|
required: true,
|
|
child: TextFormField(
|
|
controller: _controller.locationController,
|
|
readOnly: _controller.isEditMode, // 수정 모드에서 읽기 전용
|
|
decoration: InputDecoration(
|
|
hintText: '현재 위치를 입력하세요',
|
|
border: OutlineInputBorder(),
|
|
filled: _controller.isEditMode,
|
|
fillColor: _controller.isEditMode ? Colors.grey.shade100 : null,
|
|
suffixIcon: _controller.isEditMode
|
|
? Tooltip(
|
|
message: '현위치는 수정할 수 없습니다',
|
|
child: Icon(Icons.lock_outline, color: Colors.grey.shade600, size: 20),
|
|
)
|
|
: null,
|
|
),
|
|
validator: (value) => validateRequired(value, '현위치'),
|
|
),
|
|
),
|
|
// 할당 사용자
|
|
FormFieldWrapper(
|
|
label: '할당 사용자',
|
|
child: TextFormField(
|
|
controller: _controller.assignedUserController,
|
|
readOnly: _controller.isEditMode, // 수정 모드에서 읽기 전용
|
|
decoration: InputDecoration(
|
|
hintText: '할당된 사용자를 입력하세요',
|
|
border: OutlineInputBorder(),
|
|
filled: _controller.isEditMode,
|
|
fillColor: _controller.isEditMode ? Colors.grey.shade100 : null,
|
|
suffixIcon: _controller.isEditMode
|
|
? Tooltip(
|
|
message: '할당 사용자는 수정할 수 없습니다',
|
|
child: Icon(Icons.lock_outline, color: Colors.grey.shade600, size: 20),
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
// 상태
|
|
FormFieldWrapper(
|
|
label: '상태',
|
|
required: true,
|
|
child: DropdownButtonFormField<String>(
|
|
value: _controller.status,
|
|
items: ['활성', '비활성', '만료'].map((status) =>
|
|
DropdownMenuItem(
|
|
value: status,
|
|
child: Text(status),
|
|
),
|
|
).toList(),
|
|
onChanged: (value) => setState(() => _controller.status = value!),
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (value) => validateRequired(value, '상태'),
|
|
),
|
|
),
|
|
// 구매일
|
|
FormFieldWrapper(
|
|
label: '구매일',
|
|
required: true,
|
|
child: InkWell(
|
|
onTap: _controller.isEditMode ? null : () async { // 수정 모드에서 비활성화
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.purchaseDate ?? DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (date != null) {
|
|
setState(() => _controller.purchaseDate = date);
|
|
}
|
|
},
|
|
child: InputDecorator(
|
|
decoration: InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
filled: _controller.isEditMode,
|
|
fillColor: _controller.isEditMode ? Colors.grey.shade100 : null,
|
|
suffixIcon: _controller.isEditMode
|
|
? Tooltip(
|
|
message: '구매일은 수정할 수 없습니다',
|
|
child: Icon(Icons.lock_outline, color: Colors.grey.shade600, size: 20),
|
|
)
|
|
: Icon(Icons.calendar_today),
|
|
),
|
|
child: Text(
|
|
_controller.purchaseDate != null
|
|
? DateFormat('yyyy-MM-dd').format(_controller.purchaseDate!)
|
|
: '구매일을 선택하세요',
|
|
style: TextStyle(
|
|
color: _controller.isEditMode ? Colors.grey.shade600 : null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 만료일
|
|
FormFieldWrapper(
|
|
label: '만료일',
|
|
required: true,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.expiryDate ?? DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (date != null) {
|
|
setState(() => _controller.expiryDate = date);
|
|
}
|
|
},
|
|
child: InputDecorator(
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
suffixIcon: Icon(Icons.calendar_today),
|
|
),
|
|
child: Text(
|
|
_controller.expiryDate != null
|
|
? DateFormat('yyyy-MM-dd').format(_controller.expiryDate!)
|
|
: '만료일을 선택하세요',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 남은 일수 (자동 계산)
|
|
if (_controller.expiryDate != null)
|
|
FormFieldWrapper(
|
|
label: '남은 일수',
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: UIConstants.borderColor),
|
|
borderRadius: BorderRadius.circular(UIConstants.borderRadius),
|
|
color: UIConstants.backgroundColor,
|
|
),
|
|
child: Text(
|
|
'${_controller.expiryDate!.difference(DateTime.now()).inDays}일',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: _controller.expiryDate!.difference(DateTime.now()).inDays < 30
|
|
? Colors.red
|
|
: _controller.expiryDate!.difference(DateTime.now()).inDays < 90
|
|
? Colors.orange
|
|
: Colors.green,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 유지보수 설정 섹션
|
|
FormSection(
|
|
title: '유지보수 설정',
|
|
subtitle: '유지보수 기간 및 방문 주기를 설정하세요',
|
|
children: [
|
|
// 유지보수 명 표기 (입력 불가, 자동 생성)
|
|
FormFieldWrapper(
|
|
label: '유지보수 명',
|
|
hint: '유지보수 기간, 방문 주기, 점검 형태를 조합하여 자동 생성됩니다',
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 12,
|
|
horizontal: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: UIConstants.borderColor),
|
|
borderRadius: BorderRadius.circular(UIConstants.borderRadius),
|
|
color: UIConstants.backgroundColor,
|
|
),
|
|
child: Text(
|
|
maintenanceName,
|
|
style: const TextStyle(fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
// 유지보수 기간 (개월)
|
|
_buildTextField(
|
|
label: '유지보수 기간 (개월)',
|
|
initialValue: _durationMonths.toString(),
|
|
hintText: '유지보수 기간을 입력하세요',
|
|
suffixText: '개월',
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
validator: (value) => validateNumber(value, '유지보수 기간'),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_durationMonths = int.tryParse(value ?? '') ?? 0;
|
|
});
|
|
},
|
|
),
|
|
// 방문 주기 (드롭다운)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'방문 주기',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 4),
|
|
DropdownButtonFormField<String>(
|
|
value: _selectedVisitCycle,
|
|
items:
|
|
_visitCycleOptions
|
|
.map(
|
|
(option) => DropdownMenuItem(
|
|
value: option,
|
|
child: Text(option),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedVisitCycle = value!;
|
|
});
|
|
},
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 0,
|
|
),
|
|
),
|
|
validator:
|
|
(value) =>
|
|
value == null || value.isEmpty
|
|
? '방문 주기를 선택하세요'
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 점검 형태 (라디오버튼)
|
|
FormFieldWrapper(
|
|
label: '점검 형태',
|
|
required: true,
|
|
child: Row(
|
|
children: _inspectionTypeOptions.map((type) {
|
|
return Expanded(
|
|
child: RadioListTile<String>(
|
|
title: Text(type),
|
|
value: type,
|
|
groupValue: _selectedInspectionType,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedInspectionType = value!;
|
|
});
|
|
},
|
|
contentPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
], // FormSection children
|
|
), // FormSection 끝
|
|
], // Column children
|
|
), // SingleChildScrollView child
|
|
), // Form child
|
|
), // FormLayoutTemplate child
|
|
);
|
|
}
|
|
|
|
// 공통 텍스트 필드 위젯 (onSaved → onChanged로 변경)
|
|
Widget _buildTextField({
|
|
required String label,
|
|
required String initialValue,
|
|
required String hintText,
|
|
String? suffixText,
|
|
TextInputType? keyboardType,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
required String? Function(String?) validator,
|
|
required void Function(String?) onChanged,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 4),
|
|
TextFormField(
|
|
initialValue: initialValue,
|
|
decoration: InputDecoration(
|
|
hintText: hintText,
|
|
suffixText: suffixText,
|
|
),
|
|
keyboardType: keyboardType,
|
|
inputFormatters: inputFormatters,
|
|
validator: validator,
|
|
onChanged: onChanged,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|