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/screens/common/custom_widgets.dart' hide FormFieldWrapper; import 'package:superport/screens/common/theme_tailwind.dart'; import 'package:superport/screens/common/templates/form_layout_template.dart'; import 'package:superport/services/mock_data_service.dart'; import 'package:superport/utils/constants.dart'; // import 'package:flutter_localizations/flutter_localizations.dart'; // import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart'; import 'controllers/equipment_in_form_controller.dart'; // import 'package:superport/screens/common/widgets/category_autocomplete_field.dart'; // import 'package:superport/screens/common/widgets/autocomplete_dropdown_field.dart'; import 'package:superport/screens/common/widgets/remark_input.dart'; class EquipmentInFormScreen extends StatefulWidget { final int? equipmentInId; const EquipmentInFormScreen({super.key, this.equipmentInId}); @override State createState() => _EquipmentInFormScreenState(); } class _EquipmentInFormScreenState extends State { late EquipmentInFormController _controller; late FocusNode _manufacturerFocusNode; late FocusNode _nameFieldFocusNode; // 구매처 드롭다운 오버레이 관련 final LayerLink _partnerLayerLink = LayerLink(); OverlayEntry? _partnerOverlayEntry; final FocusNode _partnerFocusNode = FocusNode(); late TextEditingController _partnerController; // 입고지 드롭다운 오버레이 관련 final LayerLink _warehouseLayerLink = LayerLink(); OverlayEntry? _warehouseOverlayEntry; final FocusNode _warehouseFocusNode = FocusNode(); late TextEditingController _warehouseController; // 제조사 드롭다운 오버레이 관련 final LayerLink _manufacturerLayerLink = LayerLink(); OverlayEntry? _manufacturerOverlayEntry; late TextEditingController _manufacturerController; // 장비명 드롭다운 오버레이 관련 final LayerLink _equipmentNameLayerLink = LayerLink(); OverlayEntry? _equipmentNameOverlayEntry; late TextEditingController _equipmentNameController; // 대분류 드롭다운 오버레이 관련 final LayerLink _categoryLayerLink = LayerLink(); OverlayEntry? _categoryOverlayEntry; final FocusNode _categoryFocusNode = FocusNode(); late TextEditingController _categoryController; // 중분류 드롭다운 오버레이 관련 final LayerLink _subCategoryLayerLink = LayerLink(); OverlayEntry? _subCategoryOverlayEntry; final FocusNode _subCategoryFocusNode = FocusNode(); late TextEditingController _subCategoryController; // 소분류 드롭다운 오버레이 관련 final LayerLink _subSubCategoryLayerLink = LayerLink(); OverlayEntry? _subSubCategoryOverlayEntry; final FocusNode _subSubCategoryFocusNode = FocusNode(); late TextEditingController _subSubCategoryController; // 추가 필드 컨트롤러들 late TextEditingController _nameController; late TextEditingController _serialNumberController; late TextEditingController _barcodeController; late TextEditingController _quantityController; late TextEditingController _warrantyCodeController; // 프로그램적 입력란 변경 여부 플래그 bool _isProgrammaticPartnerChange = false; bool _isProgrammaticWarehouseChange = false; bool _isProgrammaticManufacturerChange = false; bool _isProgrammaticEquipmentNameChange = false; bool _isProgrammaticCategoryChange = false; bool _isProgrammaticSubCategoryChange = false; bool _isProgrammaticSubSubCategoryChange = false; // 입력란의 정확한 위치를 위한 GlobalKey final GlobalKey _partnerFieldKey = GlobalKey(); final GlobalKey _warehouseFieldKey = GlobalKey(); final GlobalKey _manufacturerFieldKey = GlobalKey(); final GlobalKey _equipmentNameFieldKey = GlobalKey(); final GlobalKey _categoryFieldKey = GlobalKey(); final GlobalKey _subCategoryFieldKey = GlobalKey(); final GlobalKey _subSubCategoryFieldKey = GlobalKey(); // 자동완성 후보(입력값과 가장 근접한 파트너사) 계산 함수 String? _getAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.partnerCompanies.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 입고지) 계산 함수 String? _getWarehouseAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.warehouseLocations.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 제조사) 계산 함수 String? _getManufacturerAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.manufacturers.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 장비명) 계산 함수 String? _getEquipmentNameAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.equipmentNames.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 대분류) 계산 함수 String? _getCategoryAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.categories.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 중분류) 계산 함수 String? _getSubCategoryAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.subCategories.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } // 자동완성 후보(입력값과 가장 근접한 소분류) 계산 함수 String? _getSubSubCategoryAutocompleteSuggestion(String input) { if (input.isEmpty) return null; // 입력값으로 시작하는 후보 중 가장 짧은 것 final lower = input.toLowerCase(); final match = _controller.subSubCategories.firstWhere( (c) => c.toLowerCase().startsWith(lower), orElse: () => '', ); return match.isNotEmpty && match.length > input.length ? match : null; } @override void initState() { super.initState(); _controller = EquipmentInFormController( dataService: MockDataService(), equipmentInId: widget.equipmentInId, ); // 수정 모드일 때 데이터 로드 if (_controller.isEditMode) { WidgetsBinding.instance.addPostFrameCallback((_) async { await _controller.initializeForEdit(); // 데이터 로드 후 텍스트 컨트롤러 업데이트 _updateTextControllers(); }); } _manufacturerFocusNode = FocusNode(); _nameFieldFocusNode = FocusNode(); _partnerController = TextEditingController( text: _controller.partnerCompany ?? '', ); // 추가 컨트롤러 초기화 _warehouseController = TextEditingController( text: _controller.warehouseLocation ?? '', ); _manufacturerController = TextEditingController( text: _controller.manufacturer, ); _equipmentNameController = TextEditingController(text: _controller.name); _categoryController = TextEditingController(text: _controller.category); _subCategoryController = TextEditingController( text: _controller.subCategory, ); _subSubCategoryController = TextEditingController( text: _controller.subSubCategory, ); // 추가 필드 컨트롤러 초기화 _nameController = TextEditingController(text: _controller.name); _serialNumberController = TextEditingController(text: _controller.serialNumber); _barcodeController = TextEditingController(text: _controller.barcode); _quantityController = TextEditingController(text: _controller.quantity.toString()); _warrantyCodeController = TextEditingController(text: _controller.warrantyCode ?? ''); // 포커스 변경 리스너 추가 _partnerFocusNode.addListener(_onPartnerFocusChange); _warehouseFocusNode.addListener(_onWarehouseFocusChange); _manufacturerFocusNode.addListener(_onManufacturerFocusChange); _nameFieldFocusNode.addListener(_onNameFieldFocusChange); _categoryFocusNode.addListener(_onCategoryFocusChange); _subCategoryFocusNode.addListener(_onSubCategoryFocusChange); _subSubCategoryFocusNode.addListener(_onSubSubCategoryFocusChange); } // 텍스트 컨트롤러 업데이트 메서드 void _updateTextControllers() { setState(() { _manufacturerController.text = _controller.manufacturer; _nameController.text = _controller.name; _categoryController.text = _controller.category; _subCategoryController.text = _controller.subCategory; _subSubCategoryController.text = _controller.subSubCategory; _serialNumberController.text = _controller.serialNumber; _barcodeController.text = _controller.barcode; _quantityController.text = _controller.quantity.toString(); _warehouseController.text = _controller.warehouseLocation ?? ''; _partnerController.text = _controller.partnerCompany ?? ''; _warrantyCodeController.text = _controller.warrantyCode ?? ''; _controller.remarkController.text = _controller.remarkController.text; }); } @override void dispose() { _manufacturerFocusNode.dispose(); _nameFieldFocusNode.dispose(); _partnerOverlayEntry?.remove(); _partnerFocusNode.dispose(); _partnerController.dispose(); // 추가 리소스 정리 _warehouseOverlayEntry?.remove(); _warehouseFocusNode.dispose(); _warehouseController.dispose(); _manufacturerOverlayEntry?.remove(); _manufacturerController.dispose(); _equipmentNameOverlayEntry?.remove(); _equipmentNameController.dispose(); _categoryOverlayEntry?.remove(); _categoryFocusNode.dispose(); _categoryController.dispose(); _subCategoryOverlayEntry?.remove(); _subCategoryFocusNode.dispose(); _subCategoryController.dispose(); _subSubCategoryOverlayEntry?.remove(); _subSubCategoryFocusNode.dispose(); _subSubCategoryController.dispose(); // 추가 컨트롤러 정리 _nameController.dispose(); _serialNumberController.dispose(); _barcodeController.dispose(); _quantityController.dispose(); _warrantyCodeController.dispose(); _controller.dispose(); super.dispose(); } // 포커스 변경 리스너 함수들 void _onPartnerFocusChange() { if (!_partnerFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removePartnerDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_partnerOverlayEntry); } } void _onWarehouseFocusChange() { if (!_warehouseFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeWarehouseDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_warehouseOverlayEntry); } } void _onManufacturerFocusChange() { if (!_manufacturerFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeManufacturerDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_manufacturerOverlayEntry); } } void _onNameFieldFocusChange() { if (!_nameFieldFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeEquipmentNameDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_equipmentNameOverlayEntry); } } void _onCategoryFocusChange() { if (!_categoryFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeCategoryDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_categoryOverlayEntry); } } void _onSubCategoryFocusChange() { if (!_subCategoryFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeSubCategoryDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_subCategoryOverlayEntry); } } void _onSubSubCategoryFocusChange() { if (!_subSubCategoryFocusNode.hasFocus) { // 포커스가 벗어나면 드롭다운 닫기 _removeSubSubCategoryDropdown(); } else { // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_subSubCategoryOverlayEntry); } } // 현재 포커스 필드 외의 다른 모든 드롭다운 제거 void _removeOtherDropdowns(OverlayEntry? currentOverlay) { // 모든 드롭다운 중 현재 오버레이를 제외한 나머지 닫기 if (_partnerOverlayEntry != null && _partnerOverlayEntry != currentOverlay) { _removePartnerDropdown(); } if (_warehouseOverlayEntry != null && _warehouseOverlayEntry != currentOverlay) { _removeWarehouseDropdown(); } if (_manufacturerOverlayEntry != null && _manufacturerOverlayEntry != currentOverlay) { _removeManufacturerDropdown(); } if (_equipmentNameOverlayEntry != null && _equipmentNameOverlayEntry != currentOverlay) { _removeEquipmentNameDropdown(); } if (_categoryOverlayEntry != null && _categoryOverlayEntry != currentOverlay) { _removeCategoryDropdown(); } if (_subCategoryOverlayEntry != null && _subCategoryOverlayEntry != currentOverlay) { _removeSubCategoryDropdown(); } if (_subSubCategoryOverlayEntry != null && _subSubCategoryOverlayEntry != currentOverlay) { _removeSubSubCategoryDropdown(); } } Future _saveEquipmentIn() async { // 로딩 다이얼로그 표시 showDialog( context: context, barrierDismissible: false, builder: (context) => const Center( child: CircularProgressIndicator(), ), ); try { final success = await _controller.save(); // 로딩 다이얼로그 닫기 if (!mounted) return; Navigator.pop(context); if (success) { // 성공 메시지 표시 if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_controller.isEditMode ? '장비 정보가 수정되었습니다.' : '장비 입고가 등록되었습니다.'), backgroundColor: Colors.green, ), ); if (!mounted) return; Navigator.pop(context, true); } else { // 에러 메시지 표시 if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_controller.error ?? '저장 중 오류가 발생했습니다.'), backgroundColor: Colors.red, ), ); } } catch (e) { // 로딩 다이얼로그 닫기 if (!mounted) return; Navigator.pop(context); // 예외 처리 if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('오류: $e'), backgroundColor: Colors.red, ), ); } } void _showPartnerDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removePartnerDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_partnerOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _partnerFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[구매처:showPartnerDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.partnerCompanies; print('[구매처:showPartnerDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _partnerOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _partnerLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[구매처:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[구매처:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticPartnerChange = true; print( '[구매처:setState:드롭다운아이템] _controller.partnerCompany <- "$item"', ); _controller.partnerCompany = item; print( '[구매처:setState:드롭다운아이템] _partnerController.text <- "$item"', ); _partnerController.text = item; }); print( '[구매처:드롭다운아이템:클릭] setState 이후 _partnerController.text=${_partnerController.text}, _controller.partnerCompany=${_controller.partnerCompany}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticPartnerChange = false; }); _removePartnerDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_partnerOverlayEntry!); } void _removePartnerDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_partnerOverlayEntry != null) { _partnerOverlayEntry!.remove(); _partnerOverlayEntry = null; print('[구매처:removePartnerDropdown] 오버레이 제거 완료'); } } // 입고지 드롭다운 표시 함수 void _showWarehouseDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeWarehouseDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_warehouseOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _warehouseFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[입고지:showWarehouseDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.warehouseLocations; print('[입고지:showWarehouseDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _warehouseOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _warehouseLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[입고지:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[입고지:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticWarehouseChange = true; print( '[입고지:setState:드롭다운아이템] _controller.warehouseLocation <- "$item"', ); _controller.warehouseLocation = item; print( '[입고지:setState:드롭다운아이템] _warehouseController.text <- "$item"', ); _warehouseController.text = item; }); print( '[입고지:드롭다운아이템:클릭] setState 이후 _warehouseController.text=${_warehouseController.text}, _controller.warehouseLocation=${_controller.warehouseLocation}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticWarehouseChange = false; }); _removeWarehouseDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_warehouseOverlayEntry!); } void _removeWarehouseDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_warehouseOverlayEntry != null) { _warehouseOverlayEntry!.remove(); _warehouseOverlayEntry = null; print('[입고지:removeWarehouseDropdown] 오버레이 제거 완료'); } } // 제조사 드롭다운 표시 함수 void _showManufacturerDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeManufacturerDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_manufacturerOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _manufacturerFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[제조사:showManufacturerDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.manufacturers; print('[제조사:showManufacturerDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _manufacturerOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _manufacturerLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[제조사:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[제조사:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticManufacturerChange = true; print( '[제조사:setState:드롭다운아이템] _controller.manufacturer <- "$item"', ); _controller.manufacturer = item; print( '[제조사:setState:드롭다운아이템] _manufacturerController.text <- "$item"', ); _manufacturerController.text = item; }); print( '[제조사:드롭다운아이템:클릭] setState 이후 _manufacturerController.text=${_manufacturerController.text}, _controller.manufacturer=${_controller.manufacturer}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticManufacturerChange = false; }); _removeManufacturerDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_manufacturerOverlayEntry!); } void _removeManufacturerDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_manufacturerOverlayEntry != null) { _manufacturerOverlayEntry!.remove(); _manufacturerOverlayEntry = null; print('[제조사:removeManufacturerDropdown] 오버레이 제거 완료'); } } // 장비명 드롭다운 표시 함수 void _showEquipmentNameDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeEquipmentNameDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_equipmentNameOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _equipmentNameFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[장비명:showEquipmentNameDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.equipmentNames; print('[장비명:showEquipmentNameDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _equipmentNameOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _equipmentNameLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[장비명:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[장비명:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticEquipmentNameChange = true; print( '[장비명:setState:드롭다운아이템] _controller.name <- "$item"', ); _controller.name = item; print( '[장비명:setState:드롭다운아이템] _equipmentNameController.text <- "$item"', ); _equipmentNameController.text = item; }); print( '[장비명:드롭다운아이템:클릭] setState 이후 _equipmentNameController.text=${_equipmentNameController.text}, _controller.name=${_controller.name}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticEquipmentNameChange = false; }); _removeEquipmentNameDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_equipmentNameOverlayEntry!); } void _removeEquipmentNameDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_equipmentNameOverlayEntry != null) { _equipmentNameOverlayEntry!.remove(); _equipmentNameOverlayEntry = null; print('[장비명:removeEquipmentNameDropdown] 오버레이 제거 완료'); } } // 대분류 드롭다운 표시 함수 void _showCategoryDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeCategoryDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_categoryOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _categoryFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[대분류:showCategoryDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.categories; print('[대분류:showCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _categoryOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _categoryLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[대분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[대분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticCategoryChange = true; print( '[대분류:setState:드롭다운아이템] _controller.category <- "$item"', ); _controller.category = item; print( '[대분류:setState:드롭다운아이템] _categoryController.text <- "$item"', ); _categoryController.text = item; }); print( '[대분류:드롭다운아이템:클릭] setState 이후 _categoryController.text=${_categoryController.text}, _controller.category=${_controller.category}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticCategoryChange = false; }); _removeCategoryDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_categoryOverlayEntry!); } void _removeCategoryDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_categoryOverlayEntry != null) { _categoryOverlayEntry!.remove(); _categoryOverlayEntry = null; print('[대분류:removeCategoryDropdown] 오버레이 제거 완료'); } } // 중분류 드롭다운 표시 함수 void _showSubCategoryDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeSubCategoryDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_subCategoryOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _subCategoryFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[중분류:showSubCategoryDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.subCategories; print('[중분류:showSubCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _subCategoryOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _subCategoryLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[중분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[중분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticSubCategoryChange = true; print( '[중분류:setState:드롭다운아이템] _controller.subCategory <- "$item"', ); _controller.subCategory = item; print( '[중분류:setState:드롭다운아이템] _subCategoryController.text <- "$item"', ); _subCategoryController.text = item; }); print( '[중분류:드롭다운아이템:클릭] setState 이후 _subCategoryController.text=${_subCategoryController.text}, _controller.subCategory=${_controller.subCategory}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticSubCategoryChange = false; }); _removeSubCategoryDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_subCategoryOverlayEntry!); } void _removeSubCategoryDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_subCategoryOverlayEntry != null) { _subCategoryOverlayEntry!.remove(); _subCategoryOverlayEntry = null; print('[중분류:removeSubCategoryDropdown] 오버레이 제거 완료'); } } // 소분류 드롭다운 표시 함수 void _showSubSubCategoryDropdown() { // 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지 _removeSubSubCategoryDropdown(); // 다른 모든 드롭다운 닫기 _removeOtherDropdowns(_subSubCategoryOverlayEntry); // 입력란의 정확한 RenderBox를 key로부터 참조 final RenderBox renderBox = _subSubCategoryFieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; print('[소분류:showSubSubCategoryDropdown] 드롭다운 표시, width=${size.width}'); final itemsToShow = _controller.subSubCategories; print('[소분류:showSubSubCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow'); _subSubCategoryOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _subSubCategoryLayerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...itemsToShow.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { print( '[소분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})', ); if (item.isEmpty) { print('[소분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!'); } setState(() { // 프로그램적 변경 시작 _isProgrammaticSubSubCategoryChange = true; print( '[소분류:setState:드롭다운아이템] _controller.subSubCategory <- "$item"', ); _controller.subSubCategory = item; print( '[소분류:setState:드롭다운아이템] _subSubCategoryController.text <- "$item"', ); _subSubCategoryController.text = item; }); print( '[소분류:드롭다운아이템:클릭] setState 이후 _subSubCategoryController.text=${_subSubCategoryController.text}, _controller.subSubCategory=${_controller.subSubCategory}', ); // 프로그램적 변경 종료 (다음 프레임에서) WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticSubSubCategoryChange = false; }); _removeSubSubCategoryDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_subSubCategoryOverlayEntry!); } void _removeSubSubCategoryDropdown() { // 오버레이가 있으면 정상적으로 제거 및 null 처리 if (_subSubCategoryOverlayEntry != null) { _subSubCategoryOverlayEntry!.remove(); _subSubCategoryOverlayEntry = null; print('[소분류:removeSubSubCategoryDropdown] 오버레이 제거 완료'); } } @override Widget build(BuildContext context) { print( '[구매처:build] _partnerController.text=${_partnerController.text}, _controller.partnerCompany=${_controller.partnerCompany}', ); final inputText = _partnerController.text; final suggestion = _getAutocompleteSuggestion(inputText); final showSuggestion = suggestion != null && suggestion.length > inputText.length; print( '[구매처:autocomplete] 입력값: "$inputText", 자동완성 후보: "$suggestion", showSuggestion=$showSuggestion', ); return ChangeNotifierProvider.value( value: _controller, child: Consumer( builder: (context, controller, child) { // 수정 모드에서 로딩 중일 때 로딩 인디케이터 표시 if (controller.isEditMode && controller.isLoading) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('장비 정보를 불러오는 중...'), ], ), ), ); } return GestureDetector( // 화면의 다른 곳을 탭하면 모든 드롭다운 닫기 onTap: () { // 현재 포커스된 위젯 포커스 해제 FocusScope.of(context).unfocus(); // 모든 드롭다운 닫기 _removePartnerDropdown(); _removeWarehouseDropdown(); _removeManufacturerDropdown(); _removeEquipmentNameDropdown(); _removeCategoryDropdown(); _removeSubCategoryDropdown(); _removeSubSubCategoryDropdown(); }, child: FormLayoutTemplate( title: _controller.isEditMode ? '장비 입고 수정' : '장비 입고 등록', onSave: _controller.isLoading ? null : _saveEquipmentIn, onCancel: () => Navigator.of(context).pop(), saveButtonText: _controller.isEditMode ? '수정 완료' : '입고 등록', isLoading: _controller.isSaving, child: 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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: RadioListTile( title: const Text( '신제품', style: TextStyle(fontSize: 14), ), value: EquipmentType.new_, groupValue: _controller.equipmentType, onChanged: (value) { setState(() { _controller.equipmentType = value!; }); }, contentPadding: EdgeInsets.zero, dense: true, ), ), Expanded( child: RadioListTile( title: const Text( '중고', style: TextStyle(fontSize: 14), ), value: EquipmentType.used, groupValue: _controller.equipmentType, onChanged: (value) { setState(() { _controller.equipmentType = value!; }); }, contentPadding: EdgeInsets.zero, dense: true, ), ), Expanded( child: RadioListTile( title: const Text( '계약', style: TextStyle(fontSize: 14), ), subtitle: const Text( '(입고후 즉각 출고)', style: TextStyle(fontSize: 11), ), value: EquipmentType.contract, groupValue: _controller.equipmentType, onChanged: (value) { setState(() { _controller.equipmentType = value!; }); }, contentPadding: EdgeInsets.zero, dense: true, ), ), ], ), ], ), ), // 1행: 구매처(파트너사), 입고지 Row( children: [ Expanded( child: FormFieldWrapper( label: '구매처', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _partnerLayerLink, child: TextFormField( key: _partnerFieldKey, controller: _partnerController, focusNode: _partnerFocusNode, decoration: InputDecoration( labelText: '구매처', hintText: '구매처를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showPartnerDropdown, ), ), onChanged: (value) { print('[구매처:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticPartnerChange) { print('[구매처:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[구매처:setState:onChanged] _controller.partnerCompany <- "$value"', ); _controller.partnerCompany = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 print( '[구매처:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$suggestion", showSuggestion=$showSuggestion', ); if (showSuggestion) { setState(() { print( '[구매처:onFieldSubmitted] 자동완성 적용: "$suggestion"', ); _isProgrammaticPartnerChange = true; _partnerController.text = suggestion; _controller.partnerCompany = suggestion; // 커서를 맨 뒤로 이동 _partnerController .selection = TextSelection.collapsed( offset: suggestion.length, ); print( '[구매처:onFieldSubmitted] 커서 위치: ${_partnerController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticPartnerChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (showSuggestion) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( suggestion, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), const SizedBox(width: 16), Expanded( child: FormFieldWrapper( label: '입고지', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _warehouseLayerLink, child: TextFormField( key: _warehouseFieldKey, controller: _warehouseController, focusNode: _warehouseFocusNode, decoration: InputDecoration( labelText: '입고지', hintText: '입고지를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showWarehouseDropdown, ), ), onChanged: (value) { print('[입고지:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticWarehouseChange) { print('[입고지:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[입고지:setState:onChanged] _controller.warehouseLocation <- "$value"', ); _controller.warehouseLocation = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final warehouseSuggestion = _getWarehouseAutocompleteSuggestion( value, ); final showWarehouseSuggestion = warehouseSuggestion != null && warehouseSuggestion.length > value.length; print( '[입고지:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$warehouseSuggestion", showWarehouseSuggestion=$showWarehouseSuggestion', ); if (showWarehouseSuggestion) { setState(() { print( '[입고지:onFieldSubmitted] 자동완성 적용: "$warehouseSuggestion"', ); _isProgrammaticWarehouseChange = true; _warehouseController.text = warehouseSuggestion; _controller.warehouseLocation = warehouseSuggestion; // 커서를 맨 뒤로 이동 _warehouseController .selection = TextSelection.collapsed( offset: warehouseSuggestion.length, ); print( '[입고지:onFieldSubmitted] 커서 위치: ${_warehouseController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticWarehouseChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getWarehouseAutocompleteSuggestion( _warehouseController.text, ) != null && _getWarehouseAutocompleteSuggestion( _warehouseController.text, )!.length > _warehouseController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getWarehouseAutocompleteSuggestion( _warehouseController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), ], ), // 2행: 제조사, 장비명 Row( children: [ Expanded( child: FormFieldWrapper( label: '제조사', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _manufacturerLayerLink, child: TextFormField( key: _manufacturerFieldKey, controller: _manufacturerController, focusNode: _manufacturerFocusNode, decoration: InputDecoration( labelText: '제조사', hintText: '제조사를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showManufacturerDropdown, ), ), onChanged: (value) { print('[제조사:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticManufacturerChange) { print('[제조사:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[제조사:setState:onChanged] _controller.manufacturer <- "$value"', ); _controller.manufacturer = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final manufacturerSuggestion = _getManufacturerAutocompleteSuggestion( value, ); final showManufacturerSuggestion = manufacturerSuggestion != null && manufacturerSuggestion.length > value.length; print( '[제조사:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$manufacturerSuggestion", showManufacturerSuggestion=$showManufacturerSuggestion', ); if (showManufacturerSuggestion) { setState(() { print( '[제조사:onFieldSubmitted] 자동완성 적용: "$manufacturerSuggestion"', ); _isProgrammaticManufacturerChange = true; _manufacturerController.text = manufacturerSuggestion; _controller.manufacturer = manufacturerSuggestion; // 커서를 맨 뒤로 이동 _manufacturerController .selection = TextSelection.collapsed( offset: manufacturerSuggestion.length, ); print( '[제조사:onFieldSubmitted] 커서 위치: ${_manufacturerController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticManufacturerChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getManufacturerAutocompleteSuggestion( _manufacturerController.text, ) != null && _getManufacturerAutocompleteSuggestion( _manufacturerController.text, )!.length > _manufacturerController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getManufacturerAutocompleteSuggestion( _manufacturerController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), const SizedBox(width: 16), Expanded( child: FormFieldWrapper( label: '장비명', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _equipmentNameLayerLink, child: TextFormField( key: _equipmentNameFieldKey, controller: _equipmentNameController, focusNode: _nameFieldFocusNode, decoration: InputDecoration( labelText: '장비명', hintText: '장비명을 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showEquipmentNameDropdown, ), ), onChanged: (value) { print('[장비명:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticEquipmentNameChange) { print('[장비명:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[장비명:setState:onChanged] _controller.name <- "$value"', ); _controller.name = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final equipmentNameSuggestion = _getEquipmentNameAutocompleteSuggestion( value, ); final showEquipmentNameSuggestion = equipmentNameSuggestion != null && equipmentNameSuggestion.length > value.length; print( '[장비명:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$equipmentNameSuggestion", showEquipmentNameSuggestion=$showEquipmentNameSuggestion', ); if (showEquipmentNameSuggestion) { setState(() { print( '[장비명:onFieldSubmitted] 자동완성 적용: "$equipmentNameSuggestion"', ); _isProgrammaticEquipmentNameChange = true; _equipmentNameController.text = equipmentNameSuggestion; _controller.name = equipmentNameSuggestion; // 커서를 맨 뒤로 이동 _equipmentNameController .selection = TextSelection.collapsed( offset: equipmentNameSuggestion.length, ); print( '[장비명:onFieldSubmitted] 커서 위치: ${_equipmentNameController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticEquipmentNameChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getEquipmentNameAutocompleteSuggestion( _equipmentNameController.text, ) != null && _getEquipmentNameAutocompleteSuggestion( _equipmentNameController.text, )!.length > _equipmentNameController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getEquipmentNameAutocompleteSuggestion( _equipmentNameController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), ], ), // 3행: 대분류, 중분류, 소분류 Row( children: [ Expanded( child: FormFieldWrapper( label: '대분류', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _categoryLayerLink, child: TextFormField( key: _categoryFieldKey, controller: _categoryController, focusNode: _categoryFocusNode, decoration: InputDecoration( labelText: '대분류', hintText: '대분류를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showCategoryDropdown, ), ), onChanged: (value) { print('[대분류:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticCategoryChange) { print('[대분류:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[대분류:setState:onChanged] _controller.category <- "$value"', ); _controller.category = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final categorySuggestion = _getCategoryAutocompleteSuggestion(value); final showCategorySuggestion = categorySuggestion != null && categorySuggestion.length > value.length; print( '[대분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$categorySuggestion", showCategorySuggestion=$showCategorySuggestion', ); if (showCategorySuggestion) { setState(() { print( '[대분류:onFieldSubmitted] 자동완성 적용: "$categorySuggestion"', ); _isProgrammaticCategoryChange = true; _categoryController.text = categorySuggestion; _controller.category = categorySuggestion; // 커서를 맨 뒤로 이동 _categoryController .selection = TextSelection.collapsed( offset: categorySuggestion.length, ); print( '[대분류:onFieldSubmitted] 커서 위치: ${_categoryController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticCategoryChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getCategoryAutocompleteSuggestion( _categoryController.text, ) != null && _getCategoryAutocompleteSuggestion( _categoryController.text, )!.length > _categoryController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getCategoryAutocompleteSuggestion( _categoryController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), const SizedBox(width: 16), Expanded( child: FormFieldWrapper( label: '중분류', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _subCategoryLayerLink, child: TextFormField( key: _subCategoryFieldKey, controller: _subCategoryController, focusNode: _subCategoryFocusNode, decoration: InputDecoration( labelText: '중분류', hintText: '중분류를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showSubCategoryDropdown, ), ), onChanged: (value) { print('[중분류:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticSubCategoryChange) { print('[중분류:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[중분류:setState:onChanged] _controller.subCategory <- "$value"', ); _controller.subCategory = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final subCategorySuggestion = _getSubCategoryAutocompleteSuggestion( value, ); final showSubCategorySuggestion = subCategorySuggestion != null && subCategorySuggestion.length > value.length; print( '[중분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$subCategorySuggestion", showSubCategorySuggestion=$showSubCategorySuggestion', ); if (showSubCategorySuggestion) { setState(() { print( '[중분류:onFieldSubmitted] 자동완성 적용: "$subCategorySuggestion"', ); _isProgrammaticSubCategoryChange = true; _subCategoryController.text = subCategorySuggestion; _controller.subCategory = subCategorySuggestion; // 커서를 맨 뒤로 이동 _subCategoryController .selection = TextSelection.collapsed( offset: subCategorySuggestion.length, ); print( '[중분류:onFieldSubmitted] 커서 위치: ${_subCategoryController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticSubCategoryChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getSubCategoryAutocompleteSuggestion( _subCategoryController.text, ) != null && _getSubCategoryAutocompleteSuggestion( _subCategoryController.text, )!.length > _subCategoryController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getSubCategoryAutocompleteSuggestion( _subCategoryController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), const SizedBox(width: 16), Expanded( child: FormFieldWrapper( label: '소분류', required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 입력란(CompositedTransformTarget으로 감싸기) CompositedTransformTarget( link: _subSubCategoryLayerLink, child: TextFormField( key: _subSubCategoryFieldKey, controller: _subSubCategoryController, focusNode: _subSubCategoryFocusNode, decoration: InputDecoration( labelText: '소분류', hintText: '소분류를 입력 또는 선택하세요', suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: _showSubSubCategoryDropdown, ), ), onChanged: (value) { print('[소분류:onChanged] 입력값: "$value"'); // 프로그램적 변경이면 무시 if (_isProgrammaticSubSubCategoryChange) { print('[소분류:onChanged] 프로그램적 변경이므로 무시'); return; } setState(() { print( '[소분류:setState:onChanged] _controller.subSubCategory <- "$value"', ); _controller.subSubCategory = value; }); }, onFieldSubmitted: (value) { // 엔터 입력 시 자동완성 final subSubCategorySuggestion = _getSubSubCategoryAutocompleteSuggestion( value, ); final showSubSubCategorySuggestion = subSubCategorySuggestion != null && subSubCategorySuggestion.length > value.length; print( '[소분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$subSubCategorySuggestion", showSubSubCategorySuggestion=$showSubSubCategorySuggestion', ); if (showSubSubCategorySuggestion) { setState(() { print( '[소분류:onFieldSubmitted] 자동완성 적용: "$subSubCategorySuggestion"', ); _isProgrammaticSubSubCategoryChange = true; _subSubCategoryController.text = subSubCategorySuggestion; _controller.subSubCategory = subSubCategorySuggestion; // 커서를 맨 뒤로 이동 _subSubCategoryController .selection = TextSelection.collapsed( offset: subSubCategorySuggestion.length, ); print( '[소분류:onFieldSubmitted] 커서 위치: ${_subSubCategoryController.selection.start}', ); }); WidgetsBinding.instance .addPostFrameCallback((_) { _isProgrammaticSubSubCategoryChange = false; }); } }, ), ), // 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시 if (_getSubSubCategoryAutocompleteSuggestion( _subSubCategoryController.text, ) != null && _getSubSubCategoryAutocompleteSuggestion( _subSubCategoryController.text, )!.length > _subSubCategoryController.text.length) Padding( padding: const EdgeInsets.only( left: 12, top: 2, ), child: Text( _getSubSubCategoryAutocompleteSuggestion( _subSubCategoryController.text, )!, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, // 더 작은 글씨 ), ), ), ], ), ), ), ], ), // 시리얼 번호 유무 토글 FormFieldWrapper( label: '시리얼 번호', required: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Checkbox( value: _controller.hasSerialNumber, onChanged: (value) { setState(() { _controller.hasSerialNumber = value ?? true; }); }, ), const Text('시리얼 번호 있음'), ], ), if (_controller.hasSerialNumber) TextFormField( initialValue: _controller.serialNumber, decoration: const InputDecoration( hintText: '시리얼 번호를 입력하세요', ), validator: (value) { if (_controller.hasSerialNumber && (value == null || value.isEmpty)) { return '시리얼 번호를 입력해주세요'; } return null; }, onSaved: (value) { _controller.serialNumber = value ?? ''; }, ), ], ), ), // 바코드 필드 FormFieldWrapper( label: '바코드', required: false, child: TextFormField( initialValue: _controller.barcode, decoration: const InputDecoration( hintText: '바코드를 입력하세요 (선택사항)', ), onSaved: (value) { _controller.barcode = value ?? ''; }, ), ), // 수량 필드 FormFieldWrapper( label: '수량', required: true, child: TextFormField( initialValue: _controller.quantity.toString(), decoration: const InputDecoration(hintText: '수량을 입력하세요'), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], validator: (value) { if (value == null || value.isEmpty) { return '수량을 입력해주세요'; } if (int.tryParse(value) == null || int.parse(value) <= 0) { return '유효한 수량을 입력해주세요'; } return null; }, onSaved: (value) { _controller.quantity = int.tryParse(value ?? '1') ?? 1; }, ), ), // 입고일 필드 FormFieldWrapper( label: '입고일', required: true, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _controller.inDate, firstDate: DateTime(2000), lastDate: DateTime.now(), ); if (picked != null && picked != _controller.inDate) { setState(() { _controller.inDate = picked; // 입고일 변경 시 워런티 시작일도 같이 변경 _controller.warrantyStartDate = picked; }); } }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 15, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${_controller.inDate.year}-${_controller.inDate.month.toString().padLeft(2, '0')}-${_controller.inDate.day.toString().padLeft(2, '0')}', style: AppThemeTailwind.bodyStyle, ), const Icon(Icons.calendar_today, size: 20), ], ), ), ), ), // 워런티 정보 섹션 const SizedBox(height: 16), Text('워런티 정보', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), // 워런티 필드들을 1행으로 통합 (전체 너비 사용) SizedBox( width: double.infinity, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 워런티 라이센스 Expanded( flex: 2, child: FormFieldWrapper( label: '워런티 라이센스', required: false, child: TextFormField( initialValue: _controller.warrantyLicense ?? '', decoration: const InputDecoration( hintText: '워런티 라이센스명을 입력하세요', ), onChanged: (value) { _controller.warrantyLicense = value; }, ), ), ), const SizedBox(width: 12), // 워런티 코드 입력란 추가 Expanded( flex: 2, child: FormFieldWrapper( label: '워런티 코드', required: false, child: TextFormField( initialValue: _controller.warrantyCode ?? '', decoration: const InputDecoration( hintText: '워런티 코드를 입력하세요', ), onChanged: (value) { _controller.warrantyCode = value; }, ), ), ), const SizedBox(width: 12), // 워런티 시작일 Expanded( flex: 1, child: FormFieldWrapper( label: '시작일', required: false, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _controller.warrantyStartDate, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (picked != null && picked != _controller.warrantyStartDate) { setState(() { _controller.warrantyStartDate = picked; }); } }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 15, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}', style: AppThemeTailwind.bodyStyle, overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.calendar_today, size: 16), ], ), ), ), ), ), const SizedBox(width: 12), // 워런티 종료일 Expanded( flex: 1, child: FormFieldWrapper( label: '종료일', required: false, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _controller.warrantyEndDate, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (picked != null && picked != _controller.warrantyEndDate) { setState(() { _controller.warrantyEndDate = picked; }); } }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 15, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}', style: AppThemeTailwind.bodyStyle, overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.calendar_today, size: 16), ], ), ), ), ), ), const SizedBox(width: 12), // 워런티 기간 요약 Expanded( flex: 1, child: FormFieldWrapper( label: '워런티 기간', required: false, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 15, ), decoration: BoxDecoration( color: Colors.grey.shade100, border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.centerLeft, child: Text( ' ${_controller.getWarrantyPeriodSummary()}', style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), ), ), ), ], ), ), // 비고 입력란 추가 const SizedBox(height: 16), FormFieldWrapper( label: '비고', required: false, child: RemarkInput( controller: _controller.remarkController, hint: '비고를 입력하세요', minLines: 4, ), ), ], // FormSection children 끝 ), // FormSection 끝 ], // Column children 끝 ), // SingleChildScrollView child 끝 ), // Form child 끝 ), // FormLayoutTemplate child 끝 ), // GestureDetector 끝 ); }, // Consumer builder 끝 ), // Consumer 끝 ); // ChangeNotifierProvider.value 끝 } }