From ef059d50eac884de3f4ab2a52b44b6e366656fef Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Sat, 9 Aug 2025 02:17:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=BC=20=EA=B2=80=EC=A6=9D=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LicenseDto 모델 업데이트 - 라이선스 폼 UI 개선 및 검증 로직 강화 - 라이선스 리스트 화면 필터링 기능 추가 - 만료일 관리 기능 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/data/models/license/license_dto.dart | 23 +- .../models/license/license_dto.freezed.dart | 33 +- lib/data/models/license/license_dto.g.dart | 4 +- .../controllers/license_form_controller.dart | 143 +++++- lib/screens/license/license_form.dart | 424 +++++++++++++----- .../license/license_list_redesign.dart | 36 +- 6 files changed, 525 insertions(+), 138 deletions(-) diff --git a/lib/data/models/license/license_dto.dart b/lib/data/models/license/license_dto.dart index 23d6579..ac948a7 100644 --- a/lib/data/models/license/license_dto.dart +++ b/lib/data/models/license/license_dto.dart @@ -25,6 +25,27 @@ DateTime? _dateFromJson(String? dateStr) { } } +// 문자열이나 숫자를 double로 변환하는 헬퍼 함수 +double? _priceFromJson(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) { + try { + return double.parse(value); + } catch (e) { + return null; + } + } + return null; +} + +// double을 문자열로 변환하는 헬퍼 함수 +String? _priceToJson(double? value) { + if (value == null) return null; + return value.toStringAsFixed(2); +} + // 필수 날짜 필드용 헬퍼 함수 (항상 non-null DateTime 반환) DateTime _requiredDateFromJson(String? dateStr) { if (dateStr == null || dateStr.isEmpty) return DateTime.now(); @@ -53,7 +74,7 @@ class LicenseDto with _$LicenseDto { @JsonKey(name: 'user_count') int? userCount, @JsonKey(name: 'purchase_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? purchaseDate, @JsonKey(name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? expiryDate, - @JsonKey(name: 'purchase_price') double? purchasePrice, + @JsonKey(name: 'purchase_price', toJson: _priceToJson, fromJson: _priceFromJson) double? purchasePrice, @JsonKey(name: 'company_id') int? companyId, @JsonKey(name: 'branch_id') int? branchId, @JsonKey(name: 'assigned_user_id') int? assignedUserId, diff --git a/lib/data/models/license/license_dto.freezed.dart b/lib/data/models/license/license_dto.freezed.dart index 0c5d7d0..4388ad4 100644 --- a/lib/data/models/license/license_dto.freezed.dart +++ b/lib/data/models/license/license_dto.freezed.dart @@ -34,7 +34,8 @@ mixin _$LicenseDto { DateTime? get purchaseDate => throw _privateConstructorUsedError; @JsonKey(name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? get expiryDate => throw _privateConstructorUsedError; - @JsonKey(name: 'purchase_price') + @JsonKey( + name: 'purchase_price', toJson: _priceToJson, fromJson: _priceFromJson) double? get purchasePrice => throw _privateConstructorUsedError; @JsonKey(name: 'company_id') int? get companyId => throw _privateConstructorUsedError; @@ -86,7 +87,11 @@ abstract class $LicenseDtoCopyWith<$Res> { @JsonKey( name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? expiryDate, - @JsonKey(name: 'purchase_price') double? purchasePrice, + @JsonKey( + name: 'purchase_price', + toJson: _priceToJson, + fromJson: _priceFromJson) + double? purchasePrice, @JsonKey(name: 'company_id') int? companyId, @JsonKey(name: 'branch_id') int? branchId, @JsonKey(name: 'assigned_user_id') int? assignedUserId, @@ -236,7 +241,11 @@ abstract class _$$LicenseDtoImplCopyWith<$Res> @JsonKey( name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? expiryDate, - @JsonKey(name: 'purchase_price') double? purchasePrice, + @JsonKey( + name: 'purchase_price', + toJson: _priceToJson, + fromJson: _priceFromJson) + double? purchasePrice, @JsonKey(name: 'company_id') int? companyId, @JsonKey(name: 'branch_id') int? branchId, @JsonKey(name: 'assigned_user_id') int? assignedUserId, @@ -379,7 +388,11 @@ class _$LicenseDtoImpl implements _LicenseDto { @JsonKey( name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) this.expiryDate, - @JsonKey(name: 'purchase_price') this.purchasePrice, + @JsonKey( + name: 'purchase_price', + toJson: _priceToJson, + fromJson: _priceFromJson) + this.purchasePrice, @JsonKey(name: 'company_id') this.companyId, @JsonKey(name: 'branch_id') this.branchId, @JsonKey(name: 'assigned_user_id') this.assignedUserId, @@ -417,7 +430,8 @@ class _$LicenseDtoImpl implements _LicenseDto { @JsonKey(name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) final DateTime? expiryDate; @override - @JsonKey(name: 'purchase_price') + @JsonKey( + name: 'purchase_price', toJson: _priceToJson, fromJson: _priceFromJson) final double? purchasePrice; @override @JsonKey(name: 'company_id') @@ -552,7 +566,11 @@ abstract class _LicenseDto implements LicenseDto { @JsonKey( name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) final DateTime? expiryDate, - @JsonKey(name: 'purchase_price') final double? purchasePrice, + @JsonKey( + name: 'purchase_price', + toJson: _priceToJson, + fromJson: _priceFromJson) + final double? purchasePrice, @JsonKey(name: 'company_id') final int? companyId, @JsonKey(name: 'branch_id') final int? branchId, @JsonKey(name: 'assigned_user_id') final int? assignedUserId, @@ -591,7 +609,8 @@ abstract class _LicenseDto implements LicenseDto { @JsonKey(name: 'expiry_date', toJson: _dateToJson, fromJson: _dateFromJson) DateTime? get expiryDate; @override - @JsonKey(name: 'purchase_price') + @JsonKey( + name: 'purchase_price', toJson: _priceToJson, fromJson: _priceFromJson) double? get purchasePrice; @override @JsonKey(name: 'company_id') diff --git a/lib/data/models/license/license_dto.g.dart b/lib/data/models/license/license_dto.g.dart index f758504..c398c14 100644 --- a/lib/data/models/license/license_dto.g.dart +++ b/lib/data/models/license/license_dto.g.dart @@ -16,7 +16,7 @@ _$LicenseDtoImpl _$$LicenseDtoImplFromJson(Map json) => userCount: (json['user_count'] as num?)?.toInt(), purchaseDate: _dateFromJson(json['purchase_date'] as String?), expiryDate: _dateFromJson(json['expiry_date'] as String?), - purchasePrice: (json['purchase_price'] as num?)?.toDouble(), + purchasePrice: _priceFromJson(json['purchase_price']), companyId: (json['company_id'] as num?)?.toInt(), branchId: (json['branch_id'] as num?)?.toInt(), assignedUserId: (json['assigned_user_id'] as num?)?.toInt(), @@ -39,7 +39,7 @@ Map _$$LicenseDtoImplToJson(_$LicenseDtoImpl instance) => 'user_count': instance.userCount, 'purchase_date': _dateToJson(instance.purchaseDate), 'expiry_date': _dateToJson(instance.expiryDate), - 'purchase_price': instance.purchasePrice, + 'purchase_price': _priceToJson(instance.purchasePrice), 'company_id': instance.companyId, 'branch_id': instance.branchId, 'assigned_user_id': instance.assignedUserId, diff --git a/lib/screens/license/controllers/license_form_controller.dart b/lib/screens/license/controllers/license_form_controller.dart index 8e3a813..97f57ec 100644 --- a/lib/screens/license/controllers/license_form_controller.dart +++ b/lib/screens/license/controllers/license_form_controller.dart @@ -23,6 +23,16 @@ class LicenseFormController extends ChangeNotifier { int _companyId = 1; int _durationMonths = 12; // 기본값: 12개월 String _visitCycle = '미방문'; // 기본값: 미방문 + + // 추가 필드 컨트롤러 + final TextEditingController productNameController = TextEditingController(); + final TextEditingController licenseKeyController = TextEditingController(); + final TextEditingController vendorController = TextEditingController(); + final TextEditingController locationController = TextEditingController(); + final TextEditingController assignedUserController = TextEditingController(); + String status = '활성'; + DateTime? purchaseDate; + DateTime? expiryDate; // isEditMode setter set isEditMode(bool value) { @@ -52,15 +62,19 @@ class LicenseFormController extends ChangeNotifier { this.useApi = false, MockDataService? dataService, int? licenseId, + bool isExtension = false, }) : mockDataService = dataService ?? MockDataService() { if (useApi && GetIt.instance.isRegistered()) { _licenseService = GetIt.instance(); } - if (licenseId != null) { + if (licenseId != null && !isExtension) { _licenseId = licenseId; _isEditMode = true; - loadLicense(); + // loadLicense()는 별도로 호출됨 + } else if (licenseId != null && isExtension) { + _licenseId = licenseId; + _isEditMode = false; // 연장 모드는 새로운 라이선스 생성 } } @@ -101,29 +115,122 @@ class LicenseFormController extends ChangeNotifier { Future loadLicense() async { if (_licenseId == null) return; + debugPrint('📝 loadLicense 시작 - ID: $_licenseId'); + _isLoading = true; _error = null; notifyListeners(); try { if (useApi && GetIt.instance.isRegistered()) { + debugPrint('📝 API에서 라이센스 로드 중...'); _originalLicense = await _licenseService.getLicenseById(_licenseId!); } else { + debugPrint('📝 Mock에서 라이센스 로드 중...'); _originalLicense = mockDataService?.getLicenseById(_licenseId!); } + debugPrint('📝 로드된 라이센스: $_originalLicense'); + if (_originalLicense != null) { + // 폼 필드에 데이터 설정 + productNameController.text = _originalLicense!.productName ?? ''; + licenseKeyController.text = _originalLicense!.licenseKey; + vendorController.text = _originalLicense!.vendor ?? ''; + locationController.text = _originalLicense!.companyName ?? ''; + assignedUserController.text = _originalLicense!.assignedUserName ?? ''; + + debugPrint('📝 폼 필드 설정 완료:'); + debugPrint(' - 제품명: ${productNameController.text}'); + debugPrint(' - 라이선스 키: ${licenseKeyController.text}'); + debugPrint(' - 벤더: ${vendorController.text}'); + debugPrint(' - 현위치: ${locationController.text}'); + debugPrint(' - 할당 사용자: ${assignedUserController.text}'); + status = _originalLicense!.isActive ? '활성' : '비활성'; + purchaseDate = _originalLicense!.purchaseDate; + expiryDate = _originalLicense!.expiryDate; + _name = _originalLicense!.productName ?? ''; _companyId = _originalLicense!.companyId ?? 1; - // durationMonths와 visitCycle은 License 모델에 없으므로 기본값 유지 - // _durationMonths = _originalLicense!.durationMonths; - // _visitCycle = _originalLicense!.visitCycle; + + // remark에서 방문주기 정보 추출 (있는 경우) + if (_originalLicense!.remark != null && _originalLicense!.remark!.contains('방문주기:')) { + final remarkParts = _originalLicense!.remark!.split('방문주기:'); + if (remarkParts.length > 1) { + _visitCycle = remarkParts[1].trim(); + } + } } } catch (e) { _error = e.toString(); + debugPrint('❌ 라이센스 로드 실패: $e'); } finally { _isLoading = false; notifyListeners(); + debugPrint('📝 loadLicense 완료 - isLoading: false'); + } + } + + // 라이센스 정보 로드 (연장 모드 - 기존 데이터로 새 라이선스 생성) + Future loadLicenseForExtension() async { + if (_licenseId == null) return; + + debugPrint('📝 loadLicenseForExtension 시작 - ID: $_licenseId'); + + _isLoading = true; + _error = null; + notifyListeners(); + + try { + License? sourceLicense; + if (useApi && GetIt.instance.isRegistered()) { + debugPrint('📝 API에서 라이센스 로드 중 (연장용)...'); + sourceLicense = await _licenseService.getLicenseById(_licenseId!); + } else { + debugPrint('📝 Mock에서 라이센스 로드 중 (연장용)...'); + sourceLicense = mockDataService?.getLicenseById(_licenseId!); + } + + debugPrint('📝 로드된 소스 라이센스: $sourceLicense'); + + if (sourceLicense != null) { + // 연장용으로 기존 데이터 복사 (ID는 null로 새 라이선스 생성) + productNameController.text = sourceLicense.productName ?? ''; + licenseKeyController.text = '${sourceLicense.licenseKey}-EXT-${DateTime.now().millisecondsSinceEpoch}'; + vendorController.text = sourceLicense.vendor ?? ''; + locationController.text = sourceLicense.companyName ?? ''; + assignedUserController.text = sourceLicense.assignedUserName ?? ''; + + debugPrint('📝 연장용 폼 필드 설정 완료:'); + debugPrint(' - 제품명: ${productNameController.text}'); + debugPrint(' - 라이선스 키: ${licenseKeyController.text}'); + debugPrint(' - 벤더: ${vendorController.text}'); + debugPrint(' - 현위치: ${locationController.text}'); + debugPrint(' - 할당 사용자: ${assignedUserController.text}'); + status = '활성'; // 연장은 항상 활성으로 시작 + purchaseDate = DateTime.now(); // 구매일은 오늘 + // 만료일은 기존 만료일에서 연장 (기본 12개월) + expiryDate = sourceLicense.expiryDate?.add(Duration(days: _durationMonths * 30)) + ?? DateTime.now().add(Duration(days: _durationMonths * 30)); + + _name = sourceLicense.productName ?? ''; + _companyId = sourceLicense.companyId ?? 1; + + // remark에서 방문주기 정보 추출 (있는 경우) + if (sourceLicense.remark != null && sourceLicense.remark!.contains('방문주기:')) { + final remarkParts = sourceLicense.remark!.split('방문주기:'); + if (remarkParts.length > 1) { + _visitCycle = remarkParts[1].trim(); + } + } + } + } catch (e) { + _error = e.toString(); + debugPrint('❌ 라이센스 연장 로드 실패: $e'); + } finally { + _isLoading = false; + notifyListeners(); + debugPrint('📝 loadLicenseForExtension 완료 - isLoading: false'); } } @@ -140,14 +247,20 @@ class LicenseFormController extends ChangeNotifier { try { final license = License( id: _isEditMode ? _licenseId : null, - licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}', - productName: _name, + licenseKey: licenseKeyController.text.isNotEmpty + ? licenseKeyController.text + : 'LIC-${DateTime.now().millisecondsSinceEpoch}', + productName: productNameController.text, + vendor: vendorController.text, + companyName: locationController.text, + assignedUserName: assignedUserController.text.isNotEmpty + ? assignedUserController.text + : null, companyId: _companyId, - // durationMonths와 visitCycle은 License 모델에 없음 - // 대신 expiryDate를 설정 - purchaseDate: DateTime.now(), - expiryDate: DateTime.now().add(Duration(days: _durationMonths * 30)), - remark: '방문주기: $_visitCycle', + isActive: status == '활성', + purchaseDate: purchaseDate ?? DateTime.now(), + expiryDate: expiryDate ?? DateTime.now().add(Duration(days: _durationMonths * 30)), + remark: '${_durationMonths}개월,${_visitCycle},방문', ); if (useApi && GetIt.instance.isRegistered()) { @@ -210,6 +323,12 @@ class LicenseFormController extends ChangeNotifier { @override void dispose() { + // TextEditingController들 정리 + productNameController.dispose(); + licenseKeyController.dispose(); + vendorController.dispose(); + locationController.dispose(); + assignedUserController.dispose(); super.dispose(); } } diff --git a/lib/screens/license/license_form.dart b/lib/screens/license/license_form.dart index 29234bd..676e8c7 100644 --- a/lib/screens/license/license_form.dart +++ b/lib/screens/license/license_form.dart @@ -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 { @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 _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 { // 유지보수 명은 유지보수기간, 방문주기, 점검형태를 결합해서 표기 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( + 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 { ), ), // 점검 형태 (라디오버튼) - 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( - 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( + 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 ); } diff --git a/lib/screens/license/license_list_redesign.dart b/lib/screens/license/license_list_redesign.dart index c1d3626..5b170c9 100644 --- a/lib/screens/license/license_list_redesign.dart +++ b/lib/screens/license/license_list_redesign.dart @@ -84,9 +84,31 @@ class _LicenseListRedesignState extends State { _controller.search(_searchController.text); } - /// 라이선스 추가 폼으로 이동 + /// 유지보수 연장 폼으로 이동 void _navigateToAdd() async { - final result = await Navigator.pushNamed(context, Routes.licenseAdd); + // 선택된 라이선스 확인 + final selectedLicenses = _controller.getSelectedLicenses(); + + // 선택된 라이선스가 1개인 경우 해당 라이선스 ID 전달 + int? licenseId; + if (selectedLicenses.length == 1) { + licenseId = selectedLicenses.first.id; + } else if (selectedLicenses.length > 1) { + // 여러 개 선택된 경우 경고 메시지 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('유지보수 연장은 한 번에 하나의 라이선스만 선택할 수 있습니다.'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final result = await Navigator.pushNamed( + context, + Routes.licenseAdd, + arguments: licenseId, // 선택된 라이선스 ID 전달 (없으면 null) + ); if (result == true && mounted) { _controller.refresh(); } @@ -352,7 +374,7 @@ class _LicenseListRedesignState extends State { controller: _searchController, onSubmitted: (_) => _onSearch(), decoration: InputDecoration( - hintText: '제품명, 라이선스 키, 벤더명, 회사명 검색...', + hintText: '제품명, 라이선스 키, 벤더명, 현위치 검색...', hintStyle: TextStyle(color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8), fontSize: 14), prefixIcon: Icon(Icons.search, color: ShadcnTheme.muted, size: 20), border: InputBorder.none, @@ -431,7 +453,7 @@ class _LicenseListRedesignState extends State { children: [ // 액션 버튼들 ShadcnButton( - text: '라이선스 추가', + text: '유지보수 연장', onPressed: _navigateToAdd, variant: ShadcnButtonVariant.primary, textColor: Colors.white, @@ -634,10 +656,10 @@ class _LicenseListRedesignState extends State { width: 120, child: Text('벤더', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), ), - // 회사명 + // 현위치 const SizedBox( width: 150, - child: Text('회사명', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), + child: Text('현위치', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), ), // 할당 사용자 const SizedBox( @@ -735,7 +757,7 @@ class _LicenseListRedesignState extends State { overflow: TextOverflow.ellipsis, ), ), - // 회사명 + // 현위치 SizedBox( width: 150, child: Text(