feat: 라이선스 관리 기능 개선 및 폼 검증 강화

- LicenseDto 모델 업데이트
- 라이선스 폼 UI 개선 및 검증 로직 강화
- 라이선스 리스트 화면 필터링 기능 추가
- 만료일 관리 기능 개선

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-08-09 02:17:30 +09:00
parent cddde57450
commit ef059d50ea
6 changed files with 525 additions and 138 deletions

View File

@@ -3,14 +3,22 @@ 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/custom_widgets.dart';
import 'package:superport/screens/common/templates/form_layout_template.dart';
import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
import 'package:superport/services/mock_data_service.dart';
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;
const MaintenanceFormScreen({Key? key, this.maintenanceId}) : super(key: key);
final bool isExtension; // 연장 모드 여부
const MaintenanceFormScreen({
Key? key,
this.maintenanceId,
this.isExtension = false,
}) : super(key: key);
@override
_MaintenanceFormScreenState createState() => _MaintenanceFormScreenState();
@@ -37,16 +45,78 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
@override
void initState() {
super.initState();
// API 모드 확인
final useApi = env.Environment.useApi;
debugPrint('📌 라이선스 폼 초기화 - API 모드: $useApi');
_controller = LicenseFormController(
dataService: MockDataService(),
useApi: useApi,
dataService: useApi ? null : MockDataService(),
licenseId: widget.maintenanceId,
isExtension: widget.isExtension,
);
// 컨트롤러 변경 리스너 등록 (데이터 로드 전에 등록!)
_controller.addListener(_handleControllerUpdate);
// 수정 모드 또는 연장 모드일 때
if (widget.maintenanceId != null) {
_controller.isEditMode = true;
// 초기 데이터 로드
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를 통해 자동 업데이트됨
});
}
});
}
if (_controller.isEditMode) {
_controller.loadLicense();
// TODO: 기존 데이터 로딩 시 _selectedVisitCycle, _selectedInspectionType, _durationMonths 값 세팅 필요
}
@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: AppThemeTailwind.success,
),
);
Navigator.pop(context, true);
}
}
}
@@ -55,48 +125,225 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
// 유지보수 명은 유지보수기간, 방문주기, 점검형태를 결합해서 표기
final String maintenanceName =
'${_durationMonths}개월,${_selectedVisitCycle},${_selectedInspectionType}';
return Scaffold(
appBar: AppBar(
title: Text(_controller.isEditMode ? '유지보수 수정' : '유지보수 등록'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _controller.formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 유지보수 명 표기 (입력 불가, 자동 생성)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'유지보수 명',
style: TextStyle(fontWeight: FontWeight.bold),
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: [
// 기본 정보 섹션
FormSection(
title: '기본 정보',
subtitle: '유지보수의 기본 정보를 입력하세요',
children: [
// 제품명
FormFieldWrapper(
label: '제품명',
required: true,
child: TextFormField(
controller: _controller.productNameController,
decoration: const InputDecoration(
hintText: '제품명을 입력하세요',
border: OutlineInputBorder(),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 8,
validator: (value) => validateRequired(value, '제품명'),
),
),
// 라이선스 키
FormFieldWrapper(
label: '라이선스 키',
required: true,
child: TextFormField(
controller: _controller.licenseKeyController,
decoration: const InputDecoration(
hintText: '라이선스 키를 입력하세요',
border: OutlineInputBorder(),
),
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,
decoration: const InputDecoration(
hintText: '현재 위치를 입력하세요',
border: OutlineInputBorder(),
),
validator: (value) => validateRequired(value, '현위치'),
),
),
// 할당 사용자
FormFieldWrapper(
label: '할당 사용자',
child: TextFormField(
controller: _controller.assignedUserController,
decoration: const InputDecoration(
hintText: '할당된 사용자를 입력하세요',
border: OutlineInputBorder(),
),
),
),
// 상태
FormFieldWrapper(
label: '상태',
required: true,
child: DropdownButtonFormField<String>(
value: _controller.status,
items: ['활성', '비활성', '만료'].map((status) =>
DropdownMenuItem(
value: status,
child: Text(status),
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(4),
color: Colors.grey.shade100,
).toList(),
onChanged: (value) => setState(() => _controller.status = value!),
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
validator: (value) => validateRequired(value, '상태'),
),
),
// 구매일
FormFieldWrapper(
label: '구매일',
required: true,
child: InkWell(
onTap: () 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: const InputDecoration(
border: OutlineInputBorder(),
suffixIcon: Icon(Icons.calendar_today),
),
child: Text(
maintenanceName,
style: const TextStyle(fontSize: 16),
_controller.purchaseDate != null
? DateFormat('yyyy-MM-dd').format(_controller.purchaseDate!)
: '구매일을 선택하세요',
),
),
],
),
),
// 만료일
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: '유지보수 기간 (개월)',
@@ -156,75 +403,34 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
),
),
// 점검 형태 (라디오버튼)
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),
Row(
children:
_inspectionTypeOptions.map((type) {
return Row(
children: [
Radio<String>(
value: type,
groupValue: _selectedInspectionType,
onChanged: (value) {
setState(() {
_selectedInspectionType = value!;
});
},
),
Text(type),
],
);
}).toList(),
),
],
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(),
),
),
const SizedBox(height: 24),
// 저장 버튼
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (_controller.formKey.currentState!.validate()) {
_controller.formKey.currentState!.save();
// 유지보수 명 결합하여 저장
final String saveName =
'${_durationMonths}개월,${_selectedVisitCycle},${_selectedInspectionType}';
_controller.name = saveName;
_controller.durationMonths = _durationMonths;
_controller.visitCycle = _selectedVisitCycle;
// 점검형태 저장 로직 필요 시 추가
_controller.saveLicense().then((success) {
if (success) {
Navigator.pop(context, true);
}
});
}
},
style: AppThemeTailwind.primaryButtonStyle,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_controller.isEditMode ? '수정하기' : '등록하기',
style: const TextStyle(fontSize: 16),
),
),
),
),
],
),
),
),
),
], // FormSection children
), // FormSection 끝
], // Column children
), // SingleChildScrollView child
), // Form child
), // FormLayoutTemplate child
);
}