Files
superport/lib/screens/equipment/equipment_in_form.dart
2025-07-02 17:45:44 +09:00

2268 lines
107 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/theme_tailwind.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<EquipmentInFormScreen> createState() => _EquipmentInFormScreenState();
}
class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
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;
// 프로그램적 입력란 변경 여부 플래그
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,
);
_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,
);
// 포커스 변경 리스너 추가
_partnerFocusNode.addListener(_onPartnerFocusChange);
_warehouseFocusNode.addListener(_onWarehouseFocusChange);
_manufacturerFocusNode.addListener(_onManufacturerFocusChange);
_nameFieldFocusNode.addListener(_onNameFieldFocusChange);
_categoryFocusNode.addListener(_onCategoryFocusChange);
_subCategoryFocusNode.addListener(_onSubCategoryFocusChange);
_subSubCategoryFocusNode.addListener(_onSubSubCategoryFocusChange);
}
@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();
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();
}
}
void _saveEquipmentIn() {
if (_controller.save()) {
Navigator.pop(context, true);
}
}
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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.withOpacity(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),
),
);
}).toList(),
],
),
),
),
),
),
),
);
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 GestureDetector(
// 화면의 다른 곳을 탭하면 모든 드롭다운 닫기
onTap: () {
// 현재 포커스된 위젯 포커스 해제
FocusScope.of(context).unfocus();
// 모든 드롭다운 닫기
_removePartnerDropdown();
_removeWarehouseDropdown();
_removeManufacturerDropdown();
_removeEquipmentNameDropdown();
_removeCategoryDropdown();
_removeSubCategoryDropdown();
_removeSubSubCategoryDropdown();
},
child: Scaffold(
appBar: AppBar(
title: Text(_controller.isEditMode ? '장비 입고 수정' : '장비 입고 등록'),
),
body: Form(
key: _controller.formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 기본 정보 섹션
Text('기본 정보', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
// 장비 유형 선택 (라디오 버튼)
FormFieldWrapper(
label: '장비 유형',
isRequired: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: RadioListTile<String>(
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<String>(
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<String>(
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: '구매처',
isRequired: 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: '입고지',
isRequired: 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: '제조사',
isRequired: 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: '장비명',
isRequired: 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: '대분류',
isRequired: 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: '중분류',
isRequired: 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: '소분류',
isRequired: 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: '시리얼 번호',
isRequired: 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: '바코드',
isRequired: false,
child: TextFormField(
initialValue: _controller.barcode,
decoration: const InputDecoration(
hintText: '바코드를 입력하세요 (선택사항)',
),
onSaved: (value) {
_controller.barcode = value ?? '';
},
),
),
// 수량 필드
FormFieldWrapper(
label: '수량',
isRequired: 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: '입고일',
isRequired: 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: '워런티 라이센스',
isRequired: 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: '워런티 코드',
isRequired: 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: '시작일',
isRequired: 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: '종료일',
isRequired: 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: '워런티 기간',
isRequired: 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: '비고',
isRequired: false,
child: RemarkInput(
controller: _controller.remarkController,
hint: '비고를 입력하세요',
minLines: 4,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveEquipmentIn,
style: AppThemeTailwind.primaryButtonStyle,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_controller.isEditMode ? '수정하기' : '등록하기',
style: const TextStyle(fontSize: 16),
),
),
),
),
],
),
),
),
),
);
}
}