- Equipment 관련 DTO 모델 생성 (Request/Response/List/History/In/Out/IO) - EquipmentRemoteDataSource 구현 (10개 API 엔드포인트) - EquipmentService 비즈니스 로직 구현 - Controller를 ChangeNotifier 패턴으로 개선 - 장비 목록 화면에 Provider 패턴 및 무한 스크롤 적용 - 장비 입고 화면 API 연동 및 비동기 처리 - DI 컨테이너에 Equipment 관련 의존성 등록 - API/Mock 데이터 소스 전환 가능 (Feature Flag) - API 통합 진행 상황 문서 업데이트
2316 lines
109 KiB
Dart
2316 lines
109 KiB
Dart
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';
|
|
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();
|
|
}
|
|
}
|
|
|
|
Future<void> _saveEquipmentIn() async {
|
|
// 로딩 다이얼로그 표시
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
|
|
try {
|
|
final success = await _controller.save();
|
|
|
|
// 로딩 다이얼로그 닫기
|
|
Navigator.pop(context);
|
|
|
|
if (success) {
|
|
// 성공 메시지 표시
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_controller.isEditMode ? '장비 입고가 수정되었습니다.' : '장비 입고가 등록되었습니다.'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
Navigator.pop(context, true);
|
|
} else {
|
|
// 에러 메시지 표시
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_controller.error ?? '저장 중 오류가 발생했습니다.'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// 로딩 다이얼로그 닫기
|
|
Navigator.pop(context);
|
|
|
|
// 예외 처리
|
|
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.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 ChangeNotifierProvider<EquipmentInFormController>.value(
|
|
value: _controller,
|
|
child: Consumer<EquipmentInFormController>(
|
|
builder: (context, controller, child) => 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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|