주요 변경사항: - Company-Branch → 계층형 Company 구조 완전 마이그레이션 - Equipment 모델 필드명 표준화 (current_company_id → company_id) - DropdownButton assertion 오류 완전 해결 - 지점 추가 드롭다운 페이지네이션 문제 해결 (20개→55개 전체 표시) - Equipment 백엔드 API 데이터 활용도 40%→100% 달성 - 소프트 딜리트 시스템 안정성 향상 기술적 개선: - Branch 관련 deprecated 메서드 정리 - Equipment Status 유효성 검증 로직 추가 - Company 리스트 페이지네이션 최적화 - DTO 모델 Freezed 코드 생성 완료 - 테스트 파일 API 구조 변경 대응 성과: - Flutter 웹 빌드 성공 (컴파일 에러 0건) - 백엔드 API 호환성 95% 달성 - 시스템 안정성 및 사용자 경험 대폭 개선
2585 lines
122 KiB
Dart
2585 lines
122 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' hide FormFieldWrapper;
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
import 'package:superport/screens/common/templates/form_layout_template.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;
|
|
|
|
// 추가 필드 컨트롤러들
|
|
late TextEditingController _nameController;
|
|
late TextEditingController _serialNumberController;
|
|
late TextEditingController _barcodeController;
|
|
late TextEditingController _quantityController;
|
|
late TextEditingController _warrantyCodeController;
|
|
|
|
// 프로그램적 입력란 변경 여부 플래그
|
|
bool _isProgrammaticPartnerChange = false;
|
|
bool _isProgrammaticWarehouseChange = false;
|
|
bool _isProgrammaticManufacturerChange = false;
|
|
bool _isProgrammaticEquipmentNameChange = false;
|
|
bool _isProgrammaticCategoryChange = false;
|
|
bool _isProgrammaticSubCategoryChange = false;
|
|
bool _isProgrammaticSubSubCategoryChange = false;
|
|
|
|
// 입력란의 정확한 위치를 위한 GlobalKey
|
|
final GlobalKey _partnerFieldKey = GlobalKey();
|
|
final GlobalKey _warehouseFieldKey = GlobalKey();
|
|
final GlobalKey _manufacturerFieldKey = GlobalKey();
|
|
final GlobalKey _equipmentNameFieldKey = GlobalKey();
|
|
final GlobalKey _categoryFieldKey = GlobalKey();
|
|
final GlobalKey _subCategoryFieldKey = GlobalKey();
|
|
final GlobalKey _subSubCategoryFieldKey = GlobalKey();
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 파트너사) 계산 함수
|
|
String? _getAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.partnerCompanies.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 입고지) 계산 함수
|
|
String? _getWarehouseAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.warehouseLocations.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 제조사) 계산 함수
|
|
String? _getManufacturerAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.manufacturers.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 장비명) 계산 함수
|
|
String? _getEquipmentNameAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.equipmentNames.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 대분류) 계산 함수
|
|
String? _getCategoryAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.categories.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 중분류) 계산 함수
|
|
String? _getSubCategoryAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.subCategories.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
// 자동완성 후보(입력값과 가장 근접한 소분류) 계산 함수
|
|
String? _getSubSubCategoryAutocompleteSuggestion(String input) {
|
|
if (input.isEmpty) return null;
|
|
// 입력값으로 시작하는 후보 중 가장 짧은 것
|
|
final lower = input.toLowerCase();
|
|
final match = _controller.subSubCategories.firstWhere(
|
|
(c) => c.toLowerCase().startsWith(lower),
|
|
orElse: () => '',
|
|
);
|
|
return match.isNotEmpty && match.length > input.length ? match : null;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = EquipmentInFormController(
|
|
equipmentInId: widget.equipmentInId,
|
|
);
|
|
|
|
print('DEBUG: initState - equipmentInId: ${widget.equipmentInId}, isEditMode: ${_controller.isEditMode}');
|
|
|
|
// 컨트롤러 변경 리스너 추가 (데이터 로드 전에 추가해야 변경사항을 감지할 수 있음)
|
|
_controller.addListener(_onControllerUpdated);
|
|
|
|
// 수정 모드일 때 데이터 로드
|
|
if (_controller.isEditMode) {
|
|
print('DEBUG: Edit mode detected, loading equipment data...');
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
await _controller.initializeForEdit();
|
|
print('DEBUG: Equipment data loaded, calling _updateTextControllers directly');
|
|
// 데이터 로드 후 직접 UI 업데이트 호출
|
|
if (mounted) {
|
|
_updateTextControllers();
|
|
}
|
|
});
|
|
}
|
|
|
|
_manufacturerFocusNode = FocusNode();
|
|
_nameFieldFocusNode = FocusNode();
|
|
|
|
// 컨트롤러들을 빈 값으로 초기화 (나중에 데이터 로드 시 업데이트됨)
|
|
_partnerController = TextEditingController();
|
|
_warehouseController = TextEditingController();
|
|
_manufacturerController = TextEditingController();
|
|
_equipmentNameController = TextEditingController();
|
|
_categoryController = TextEditingController();
|
|
_subCategoryController = TextEditingController();
|
|
_subSubCategoryController = TextEditingController();
|
|
_nameController = TextEditingController();
|
|
_serialNumberController = TextEditingController();
|
|
_barcodeController = TextEditingController();
|
|
_quantityController = TextEditingController(text: '1');
|
|
_warrantyCodeController = TextEditingController();
|
|
|
|
// 포커스 변경 리스너 추가
|
|
_partnerFocusNode.addListener(_onPartnerFocusChange);
|
|
_warehouseFocusNode.addListener(_onWarehouseFocusChange);
|
|
_manufacturerFocusNode.addListener(_onManufacturerFocusChange);
|
|
_nameFieldFocusNode.addListener(_onNameFieldFocusChange);
|
|
_categoryFocusNode.addListener(_onCategoryFocusChange);
|
|
_subCategoryFocusNode.addListener(_onSubCategoryFocusChange);
|
|
_subSubCategoryFocusNode.addListener(_onSubSubCategoryFocusChange);
|
|
}
|
|
|
|
// 컨트롤러 데이터 변경 시 텍스트 컨트롤러 업데이트
|
|
void _onControllerUpdated() {
|
|
print('DEBUG [_onControllerUpdated] Called - isEditMode: ${_controller.isEditMode}, isLoading: ${_controller.isLoading}, actualEquipmentId: ${_controller.actualEquipmentId}');
|
|
// 데이터 로딩이 완료되고 수정 모드일 때 텍스트 컨트롤러 업데이트
|
|
// actualEquipmentId가 설정되었다는 것은 데이터가 로드되었다는 의미
|
|
if (_controller.isEditMode && !_controller.isLoading && _controller.actualEquipmentId != null) {
|
|
print('DEBUG [_onControllerUpdated] Condition met, updating text controllers');
|
|
print('DEBUG [_onControllerUpdated] manufacturer: "${_controller.manufacturer}", name: "${_controller.name}"');
|
|
_updateTextControllers();
|
|
}
|
|
}
|
|
|
|
// 텍스트 컨트롤러 업데이트 메서드
|
|
void _updateTextControllers() {
|
|
print('DEBUG [_updateTextControllers] Called');
|
|
print('DEBUG [_updateTextControllers] Before update:');
|
|
print(' manufacturerController.text="${_manufacturerController.text}"');
|
|
print(' nameController.text="${_nameController.text}"');
|
|
print('DEBUG [_updateTextControllers] Controller values:');
|
|
print(' controller.manufacturer="${_controller.manufacturer}"');
|
|
print(' controller.name="${_controller.name}"');
|
|
print(' controller.serialNumber="${_controller.serialNumber}"');
|
|
print(' controller.quantity=${_controller.quantity}');
|
|
|
|
setState(() {
|
|
_manufacturerController.text = _controller.manufacturer;
|
|
_nameController.text = _controller.name;
|
|
_equipmentNameController.text = _controller.name; // 장비명 컨트롤러 추가
|
|
_categoryController.text = _controller.category;
|
|
_subCategoryController.text = _controller.subCategory;
|
|
_subSubCategoryController.text = _controller.subSubCategory;
|
|
_serialNumberController.text = _controller.serialNumber;
|
|
_barcodeController.text = _controller.barcode;
|
|
_quantityController.text = _controller.quantity.toString();
|
|
_warehouseController.text = _controller.warehouseLocation ?? '';
|
|
_partnerController.text = _controller.partnerCompany ?? '';
|
|
_warrantyCodeController.text = _controller.warrantyCode ?? '';
|
|
_controller.remarkController.text = _controller.remarkController.text;
|
|
});
|
|
|
|
print('DEBUG [_updateTextControllers] After update:');
|
|
print(' manufacturerController.text="${_manufacturerController.text}"');
|
|
print(' nameController.text="${_nameController.text}"');
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_onControllerUpdated);
|
|
_manufacturerFocusNode.dispose();
|
|
_nameFieldFocusNode.dispose();
|
|
_partnerOverlayEntry?.remove();
|
|
_partnerFocusNode.dispose();
|
|
_partnerController.dispose();
|
|
|
|
// 추가 리소스 정리
|
|
_warehouseOverlayEntry?.remove();
|
|
_warehouseFocusNode.dispose();
|
|
_warehouseController.dispose();
|
|
|
|
_manufacturerOverlayEntry?.remove();
|
|
_manufacturerController.dispose();
|
|
|
|
_equipmentNameOverlayEntry?.remove();
|
|
_equipmentNameController.dispose();
|
|
|
|
_categoryOverlayEntry?.remove();
|
|
_categoryFocusNode.dispose();
|
|
_categoryController.dispose();
|
|
|
|
_subCategoryOverlayEntry?.remove();
|
|
_subCategoryFocusNode.dispose();
|
|
_subCategoryController.dispose();
|
|
|
|
_subSubCategoryOverlayEntry?.remove();
|
|
_subSubCategoryFocusNode.dispose();
|
|
_subSubCategoryController.dispose();
|
|
|
|
// 추가 컨트롤러 정리
|
|
_nameController.dispose();
|
|
_serialNumberController.dispose();
|
|
_barcodeController.dispose();
|
|
_quantityController.dispose();
|
|
_warrantyCodeController.dispose();
|
|
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// 유효한 장비 상태 값을 반환하는 메서드
|
|
String? _getValidEquipmentStatus(String? status) {
|
|
const validStatuses = ['available', 'inuse', 'maintenance', 'disposed'];
|
|
return validStatuses.contains(status) ? status : null;
|
|
}
|
|
|
|
// 포커스 변경 리스너 함수들
|
|
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();
|
|
|
|
// 로딩 다이얼로그 닫기
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
|
|
if (success) {
|
|
// 성공 메시지 표시
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_controller.isEditMode ? '장비 정보가 수정되었습니다.' : '장비 입고가 등록되었습니다.'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
if (!mounted) return;
|
|
Navigator.pop(context, true);
|
|
} else {
|
|
// 에러 메시지 표시
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_controller.error ?? '저장 중 오류가 발생했습니다.'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// 로딩 다이얼로그 닫기
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
|
|
// 예외 처리
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('오류: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _showPartnerDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removePartnerDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_partnerOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_partnerFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[구매처:showPartnerDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.partnerCompanies;
|
|
print('[구매처:showPartnerDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_partnerOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _partnerLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[구매처:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[구매처:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticPartnerChange = true;
|
|
print(
|
|
'[구매처:setState:드롭다운아이템] _controller.partnerCompany <- "$item"',
|
|
);
|
|
_controller.partnerCompany = item;
|
|
print(
|
|
'[구매처:setState:드롭다운아이템] _partnerController.text <- "$item"',
|
|
);
|
|
_partnerController.text = item;
|
|
});
|
|
print(
|
|
'[구매처:드롭다운아이템:클릭] setState 이후 _partnerController.text=${_partnerController.text}, _controller.partnerCompany=${_controller.partnerCompany}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticPartnerChange = false;
|
|
});
|
|
_removePartnerDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_partnerOverlayEntry!);
|
|
}
|
|
|
|
void _removePartnerDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_partnerOverlayEntry != null) {
|
|
_partnerOverlayEntry!.remove();
|
|
_partnerOverlayEntry = null;
|
|
print('[구매처:removePartnerDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 입고지 드롭다운 표시 함수
|
|
void _showWarehouseDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeWarehouseDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_warehouseOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_warehouseFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[입고지:showWarehouseDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.warehouseLocations;
|
|
print('[입고지:showWarehouseDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_warehouseOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _warehouseLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[입고지:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[입고지:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticWarehouseChange = true;
|
|
print(
|
|
'[입고지:setState:드롭다운아이템] _controller.warehouseLocation <- "$item"',
|
|
);
|
|
_controller.warehouseLocation = item;
|
|
print(
|
|
'[입고지:setState:드롭다운아이템] _warehouseController.text <- "$item"',
|
|
);
|
|
_warehouseController.text = item;
|
|
});
|
|
print(
|
|
'[입고지:드롭다운아이템:클릭] setState 이후 _warehouseController.text=${_warehouseController.text}, _controller.warehouseLocation=${_controller.warehouseLocation}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticWarehouseChange = false;
|
|
});
|
|
_removeWarehouseDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_warehouseOverlayEntry!);
|
|
}
|
|
|
|
void _removeWarehouseDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_warehouseOverlayEntry != null) {
|
|
_warehouseOverlayEntry!.remove();
|
|
_warehouseOverlayEntry = null;
|
|
print('[입고지:removeWarehouseDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 제조사 드롭다운 표시 함수
|
|
void _showManufacturerDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeManufacturerDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_manufacturerOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_manufacturerFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[제조사:showManufacturerDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.manufacturers;
|
|
print('[제조사:showManufacturerDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_manufacturerOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _manufacturerLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[제조사:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[제조사:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticManufacturerChange = true;
|
|
print(
|
|
'[제조사:setState:드롭다운아이템] _controller.manufacturer <- "$item"',
|
|
);
|
|
_controller.manufacturer = item;
|
|
print(
|
|
'[제조사:setState:드롭다운아이템] _manufacturerController.text <- "$item"',
|
|
);
|
|
_manufacturerController.text = item;
|
|
});
|
|
print(
|
|
'[제조사:드롭다운아이템:클릭] setState 이후 _manufacturerController.text=${_manufacturerController.text}, _controller.manufacturer=${_controller.manufacturer}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticManufacturerChange = false;
|
|
});
|
|
_removeManufacturerDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_manufacturerOverlayEntry!);
|
|
}
|
|
|
|
void _removeManufacturerDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_manufacturerOverlayEntry != null) {
|
|
_manufacturerOverlayEntry!.remove();
|
|
_manufacturerOverlayEntry = null;
|
|
print('[제조사:removeManufacturerDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 장비명 드롭다운 표시 함수
|
|
void _showEquipmentNameDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeEquipmentNameDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_equipmentNameOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_equipmentNameFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[장비명:showEquipmentNameDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.equipmentNames;
|
|
print('[장비명:showEquipmentNameDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_equipmentNameOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _equipmentNameLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[장비명:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[장비명:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticEquipmentNameChange = true;
|
|
print(
|
|
'[장비명:setState:드롭다운아이템] _controller.name <- "$item"',
|
|
);
|
|
_controller.name = item;
|
|
print(
|
|
'[장비명:setState:드롭다운아이템] _equipmentNameController.text <- "$item"',
|
|
);
|
|
_equipmentNameController.text = item;
|
|
});
|
|
print(
|
|
'[장비명:드롭다운아이템:클릭] setState 이후 _equipmentNameController.text=${_equipmentNameController.text}, _controller.name=${_controller.name}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticEquipmentNameChange = false;
|
|
});
|
|
_removeEquipmentNameDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_equipmentNameOverlayEntry!);
|
|
}
|
|
|
|
void _removeEquipmentNameDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_equipmentNameOverlayEntry != null) {
|
|
_equipmentNameOverlayEntry!.remove();
|
|
_equipmentNameOverlayEntry = null;
|
|
print('[장비명:removeEquipmentNameDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 대분류 드롭다운 표시 함수
|
|
void _showCategoryDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeCategoryDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_categoryOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_categoryFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[대분류:showCategoryDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.categories;
|
|
print('[대분류:showCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_categoryOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _categoryLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[대분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[대분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticCategoryChange = true;
|
|
print(
|
|
'[대분류:setState:드롭다운아이템] _controller.category <- "$item"',
|
|
);
|
|
_controller.category = item;
|
|
print(
|
|
'[대분류:setState:드롭다운아이템] _categoryController.text <- "$item"',
|
|
);
|
|
_categoryController.text = item;
|
|
});
|
|
print(
|
|
'[대분류:드롭다운아이템:클릭] setState 이후 _categoryController.text=${_categoryController.text}, _controller.category=${_controller.category}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticCategoryChange = false;
|
|
});
|
|
_removeCategoryDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_categoryOverlayEntry!);
|
|
}
|
|
|
|
void _removeCategoryDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_categoryOverlayEntry != null) {
|
|
_categoryOverlayEntry!.remove();
|
|
_categoryOverlayEntry = null;
|
|
print('[대분류:removeCategoryDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 중분류 드롭다운 표시 함수
|
|
void _showSubCategoryDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeSubCategoryDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_subCategoryOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_subCategoryFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[중분류:showSubCategoryDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.subCategories;
|
|
print('[중분류:showSubCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_subCategoryOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _subCategoryLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[중분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[중분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticSubCategoryChange = true;
|
|
print(
|
|
'[중분류:setState:드롭다운아이템] _controller.subCategory <- "$item"',
|
|
);
|
|
_controller.subCategory = item;
|
|
print(
|
|
'[중분류:setState:드롭다운아이템] _subCategoryController.text <- "$item"',
|
|
);
|
|
_subCategoryController.text = item;
|
|
});
|
|
print(
|
|
'[중분류:드롭다운아이템:클릭] setState 이후 _subCategoryController.text=${_subCategoryController.text}, _controller.subCategory=${_controller.subCategory}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticSubCategoryChange = false;
|
|
});
|
|
_removeSubCategoryDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_subCategoryOverlayEntry!);
|
|
}
|
|
|
|
void _removeSubCategoryDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_subCategoryOverlayEntry != null) {
|
|
_subCategoryOverlayEntry!.remove();
|
|
_subCategoryOverlayEntry = null;
|
|
print('[중분류:removeSubCategoryDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
// 소분류 드롭다운 표시 함수
|
|
void _showSubSubCategoryDropdown() {
|
|
// 항상 기존 오버레이를 먼저 제거하여 중복 생성 방지
|
|
_removeSubSubCategoryDropdown();
|
|
// 다른 모든 드롭다운 닫기
|
|
_removeOtherDropdowns(_subSubCategoryOverlayEntry);
|
|
// 입력란의 정확한 RenderBox를 key로부터 참조
|
|
final RenderBox renderBox =
|
|
_subSubCategoryFieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final size = renderBox.size;
|
|
print('[소분류:showSubSubCategoryDropdown] 드롭다운 표시, width=${size.width}');
|
|
final itemsToShow = _controller.subSubCategories;
|
|
print('[소분류:showSubSubCategoryDropdown] 드롭다운에 노출될 아이템: $itemsToShow');
|
|
_subSubCategoryOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => Positioned(
|
|
width: size.width,
|
|
child: CompositedTransformFollower(
|
|
link: _subSubCategoryLayerLink,
|
|
showWhenUnlinked: false,
|
|
offset: const Offset(0, 45),
|
|
child: Material(
|
|
elevation: 4,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withValues(alpha: 0.3),
|
|
spreadRadius: 1,
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
...itemsToShow.map((item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
print(
|
|
'[소분류:드롭다운아이템:클릭] 선택값: "$item" (길이: ${item.length})',
|
|
);
|
|
if (item.isEmpty) {
|
|
print('[소분류:드롭다운아이템:클릭] 경고: 빈 값이 선택됨!');
|
|
}
|
|
setState(() {
|
|
// 프로그램적 변경 시작
|
|
_isProgrammaticSubSubCategoryChange = true;
|
|
print(
|
|
'[소분류:setState:드롭다운아이템] _controller.subSubCategory <- "$item"',
|
|
);
|
|
_controller.subSubCategory = item;
|
|
print(
|
|
'[소분류:setState:드롭다운아이템] _subSubCategoryController.text <- "$item"',
|
|
);
|
|
_subSubCategoryController.text = item;
|
|
});
|
|
print(
|
|
'[소분류:드롭다운아이템:클릭] setState 이후 _subSubCategoryController.text=${_subSubCategoryController.text}, _controller.subSubCategory=${_controller.subSubCategory}',
|
|
);
|
|
// 프로그램적 변경 종료 (다음 프레임에서)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_isProgrammaticSubSubCategoryChange = false;
|
|
});
|
|
_removeSubSubCategoryDropdown();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
width: double.infinity,
|
|
child: Text(item),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
Overlay.of(context).insert(_subSubCategoryOverlayEntry!);
|
|
}
|
|
|
|
void _removeSubSubCategoryDropdown() {
|
|
// 오버레이가 있으면 정상적으로 제거 및 null 처리
|
|
if (_subSubCategoryOverlayEntry != null) {
|
|
_subSubCategoryOverlayEntry!.remove();
|
|
_subSubCategoryOverlayEntry = null;
|
|
print('[소분류:removeSubSubCategoryDropdown] 오버레이 제거 완료');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
print(
|
|
'[구매처:build] _partnerController.text=${_partnerController.text}, _controller.partnerCompany=${_controller.partnerCompany}',
|
|
);
|
|
final inputText = _partnerController.text;
|
|
final suggestion = _getAutocompleteSuggestion(inputText);
|
|
final showSuggestion =
|
|
suggestion != null && suggestion.length > inputText.length;
|
|
print(
|
|
'[구매처:autocomplete] 입력값: "$inputText", 자동완성 후보: "$suggestion", showSuggestion=$showSuggestion',
|
|
);
|
|
return ChangeNotifierProvider<EquipmentInFormController>.value(
|
|
value: _controller,
|
|
child: Consumer<EquipmentInFormController>(
|
|
builder: (context, controller, child) {
|
|
// 수정 모드에서 로딩 중일 때 로딩 인디케이터 표시
|
|
if (controller.isEditMode && controller.isLoading) {
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('장비 정보를 불러오는 중...'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return GestureDetector(
|
|
// 화면의 다른 곳을 탭하면 모든 드롭다운 닫기
|
|
onTap: () {
|
|
// 현재 포커스된 위젯 포커스 해제
|
|
FocusScope.of(context).unfocus();
|
|
// 모든 드롭다운 닫기
|
|
_removePartnerDropdown();
|
|
_removeWarehouseDropdown();
|
|
_removeManufacturerDropdown();
|
|
_removeEquipmentNameDropdown();
|
|
_removeCategoryDropdown();
|
|
_removeSubCategoryDropdown();
|
|
_removeSubSubCategoryDropdown();
|
|
},
|
|
child: FormLayoutTemplate(
|
|
title: _controller.isEditMode ? '장비 입고 수정' : '장비 입고 등록',
|
|
onSave: _controller.isLoading ? null : _saveEquipmentIn,
|
|
onCancel: () => Navigator.of(context).pop(),
|
|
saveButtonText: _controller.isEditMode ? '수정 완료' : '입고 등록',
|
|
isLoading: _controller.isSaving,
|
|
child: Form(
|
|
key: _controller.formKey,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(UIConstants.formPadding),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 기본 정보 섹션
|
|
FormSection(
|
|
title: '기본 정보',
|
|
subtitle: '입고할 장비의 기본 정보를 입력하세요',
|
|
children: [
|
|
// 장비 유형 선택 (라디오 버튼)
|
|
FormFieldWrapper(
|
|
label: '장비 유형',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: RadioListTile<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: '구매처',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _partnerLayerLink,
|
|
child: TextFormField(
|
|
key: _partnerFieldKey,
|
|
controller: _partnerController,
|
|
focusNode: _partnerFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '구매처',
|
|
hintText: '구매처를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showPartnerDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[구매처:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticPartnerChange) {
|
|
print('[구매처:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[구매처:setState:onChanged] _controller.partnerCompany <- "$value"',
|
|
);
|
|
_controller.partnerCompany = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
print(
|
|
'[구매처:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$suggestion", showSuggestion=$showSuggestion',
|
|
);
|
|
if (showSuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[구매처:onFieldSubmitted] 자동완성 적용: "$suggestion"',
|
|
);
|
|
_isProgrammaticPartnerChange = true;
|
|
_partnerController.text = suggestion;
|
|
_controller.partnerCompany = suggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_partnerController
|
|
.selection = TextSelection.collapsed(
|
|
offset: suggestion.length,
|
|
);
|
|
print(
|
|
'[구매처:onFieldSubmitted] 커서 위치: ${_partnerController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticPartnerChange = false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (showSuggestion)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
suggestion,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '입고지',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _warehouseLayerLink,
|
|
child: TextFormField(
|
|
key: _warehouseFieldKey,
|
|
controller: _warehouseController,
|
|
focusNode: _warehouseFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '입고지',
|
|
hintText: '입고지를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showWarehouseDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[입고지:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticWarehouseChange) {
|
|
print('[입고지:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[입고지:setState:onChanged] _controller.warehouseLocation <- "$value"',
|
|
);
|
|
_controller.warehouseLocation = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final warehouseSuggestion =
|
|
_getWarehouseAutocompleteSuggestion(
|
|
value,
|
|
);
|
|
final showWarehouseSuggestion =
|
|
warehouseSuggestion != null &&
|
|
warehouseSuggestion.length > value.length;
|
|
print(
|
|
'[입고지:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$warehouseSuggestion", showWarehouseSuggestion=$showWarehouseSuggestion',
|
|
);
|
|
if (showWarehouseSuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[입고지:onFieldSubmitted] 자동완성 적용: "$warehouseSuggestion"',
|
|
);
|
|
_isProgrammaticWarehouseChange = true;
|
|
_warehouseController.text =
|
|
warehouseSuggestion;
|
|
_controller.warehouseLocation =
|
|
warehouseSuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_warehouseController
|
|
.selection = TextSelection.collapsed(
|
|
offset: warehouseSuggestion.length,
|
|
);
|
|
print(
|
|
'[입고지:onFieldSubmitted] 커서 위치: ${_warehouseController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticWarehouseChange =
|
|
false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getWarehouseAutocompleteSuggestion(
|
|
_warehouseController.text,
|
|
) !=
|
|
null &&
|
|
_getWarehouseAutocompleteSuggestion(
|
|
_warehouseController.text,
|
|
)!.length >
|
|
_warehouseController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getWarehouseAutocompleteSuggestion(
|
|
_warehouseController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// 2행: 제조사, 장비명
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '제조사',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _manufacturerLayerLink,
|
|
child: TextFormField(
|
|
key: _manufacturerFieldKey,
|
|
controller: _manufacturerController,
|
|
focusNode: _manufacturerFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '제조사',
|
|
hintText: '제조사를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showManufacturerDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[제조사:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticManufacturerChange) {
|
|
print('[제조사:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[제조사:setState:onChanged] _controller.manufacturer <- "$value"',
|
|
);
|
|
_controller.manufacturer = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final manufacturerSuggestion =
|
|
_getManufacturerAutocompleteSuggestion(
|
|
value,
|
|
);
|
|
final showManufacturerSuggestion =
|
|
manufacturerSuggestion != null &&
|
|
manufacturerSuggestion.length >
|
|
value.length;
|
|
print(
|
|
'[제조사:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$manufacturerSuggestion", showManufacturerSuggestion=$showManufacturerSuggestion',
|
|
);
|
|
if (showManufacturerSuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[제조사:onFieldSubmitted] 자동완성 적용: "$manufacturerSuggestion"',
|
|
);
|
|
_isProgrammaticManufacturerChange = true;
|
|
_manufacturerController.text =
|
|
manufacturerSuggestion;
|
|
_controller.manufacturer =
|
|
manufacturerSuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_manufacturerController
|
|
.selection = TextSelection.collapsed(
|
|
offset: manufacturerSuggestion.length,
|
|
);
|
|
print(
|
|
'[제조사:onFieldSubmitted] 커서 위치: ${_manufacturerController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticManufacturerChange =
|
|
false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getManufacturerAutocompleteSuggestion(
|
|
_manufacturerController.text,
|
|
) !=
|
|
null &&
|
|
_getManufacturerAutocompleteSuggestion(
|
|
_manufacturerController.text,
|
|
)!.length >
|
|
_manufacturerController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getManufacturerAutocompleteSuggestion(
|
|
_manufacturerController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '장비명',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _equipmentNameLayerLink,
|
|
child: TextFormField(
|
|
key: _equipmentNameFieldKey,
|
|
controller: _equipmentNameController,
|
|
focusNode: _nameFieldFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '장비명',
|
|
hintText: '장비명을 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showEquipmentNameDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[장비명:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticEquipmentNameChange) {
|
|
print('[장비명:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[장비명:setState:onChanged] _controller.name <- "$value"',
|
|
);
|
|
_controller.name = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final equipmentNameSuggestion =
|
|
_getEquipmentNameAutocompleteSuggestion(
|
|
value,
|
|
);
|
|
final showEquipmentNameSuggestion =
|
|
equipmentNameSuggestion != null &&
|
|
equipmentNameSuggestion.length >
|
|
value.length;
|
|
print(
|
|
'[장비명:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$equipmentNameSuggestion", showEquipmentNameSuggestion=$showEquipmentNameSuggestion',
|
|
);
|
|
if (showEquipmentNameSuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[장비명:onFieldSubmitted] 자동완성 적용: "$equipmentNameSuggestion"',
|
|
);
|
|
_isProgrammaticEquipmentNameChange = true;
|
|
_equipmentNameController.text =
|
|
equipmentNameSuggestion;
|
|
_controller.name =
|
|
equipmentNameSuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_equipmentNameController
|
|
.selection = TextSelection.collapsed(
|
|
offset: equipmentNameSuggestion.length,
|
|
);
|
|
print(
|
|
'[장비명:onFieldSubmitted] 커서 위치: ${_equipmentNameController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticEquipmentNameChange =
|
|
false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getEquipmentNameAutocompleteSuggestion(
|
|
_equipmentNameController.text,
|
|
) !=
|
|
null &&
|
|
_getEquipmentNameAutocompleteSuggestion(
|
|
_equipmentNameController.text,
|
|
)!.length >
|
|
_equipmentNameController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getEquipmentNameAutocompleteSuggestion(
|
|
_equipmentNameController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// 3행: 대분류, 중분류, 소분류
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '대분류',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _categoryLayerLink,
|
|
child: TextFormField(
|
|
key: _categoryFieldKey,
|
|
controller: _categoryController,
|
|
focusNode: _categoryFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '대분류',
|
|
hintText: '대분류를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showCategoryDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[대분류:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticCategoryChange) {
|
|
print('[대분류:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[대분류:setState:onChanged] _controller.category <- "$value"',
|
|
);
|
|
_controller.category = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final categorySuggestion =
|
|
_getCategoryAutocompleteSuggestion(value);
|
|
final showCategorySuggestion =
|
|
categorySuggestion != null &&
|
|
categorySuggestion.length > value.length;
|
|
print(
|
|
'[대분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$categorySuggestion", showCategorySuggestion=$showCategorySuggestion',
|
|
);
|
|
if (showCategorySuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[대분류:onFieldSubmitted] 자동완성 적용: "$categorySuggestion"',
|
|
);
|
|
_isProgrammaticCategoryChange = true;
|
|
_categoryController.text =
|
|
categorySuggestion;
|
|
_controller.category = categorySuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_categoryController
|
|
.selection = TextSelection.collapsed(
|
|
offset: categorySuggestion.length,
|
|
);
|
|
print(
|
|
'[대분류:onFieldSubmitted] 커서 위치: ${_categoryController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticCategoryChange = false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getCategoryAutocompleteSuggestion(
|
|
_categoryController.text,
|
|
) !=
|
|
null &&
|
|
_getCategoryAutocompleteSuggestion(
|
|
_categoryController.text,
|
|
)!.length >
|
|
_categoryController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getCategoryAutocompleteSuggestion(
|
|
_categoryController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '중분류',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _subCategoryLayerLink,
|
|
child: TextFormField(
|
|
key: _subCategoryFieldKey,
|
|
controller: _subCategoryController,
|
|
focusNode: _subCategoryFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '중분류',
|
|
hintText: '중분류를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showSubCategoryDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[중분류:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticSubCategoryChange) {
|
|
print('[중분류:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[중분류:setState:onChanged] _controller.subCategory <- "$value"',
|
|
);
|
|
_controller.subCategory = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final subCategorySuggestion =
|
|
_getSubCategoryAutocompleteSuggestion(
|
|
value,
|
|
);
|
|
final showSubCategorySuggestion =
|
|
subCategorySuggestion != null &&
|
|
subCategorySuggestion.length >
|
|
value.length;
|
|
print(
|
|
'[중분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$subCategorySuggestion", showSubCategorySuggestion=$showSubCategorySuggestion',
|
|
);
|
|
if (showSubCategorySuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[중분류:onFieldSubmitted] 자동완성 적용: "$subCategorySuggestion"',
|
|
);
|
|
_isProgrammaticSubCategoryChange = true;
|
|
_subCategoryController.text =
|
|
subCategorySuggestion;
|
|
_controller.subCategory =
|
|
subCategorySuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_subCategoryController
|
|
.selection = TextSelection.collapsed(
|
|
offset: subCategorySuggestion.length,
|
|
);
|
|
print(
|
|
'[중분류:onFieldSubmitted] 커서 위치: ${_subCategoryController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticSubCategoryChange =
|
|
false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getSubCategoryAutocompleteSuggestion(
|
|
_subCategoryController.text,
|
|
) !=
|
|
null &&
|
|
_getSubCategoryAutocompleteSuggestion(
|
|
_subCategoryController.text,
|
|
)!.length >
|
|
_subCategoryController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getSubCategoryAutocompleteSuggestion(
|
|
_subCategoryController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '소분류',
|
|
required: true,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 입력란(CompositedTransformTarget으로 감싸기)
|
|
CompositedTransformTarget(
|
|
link: _subSubCategoryLayerLink,
|
|
child: TextFormField(
|
|
key: _subSubCategoryFieldKey,
|
|
controller: _subSubCategoryController,
|
|
focusNode: _subSubCategoryFocusNode,
|
|
decoration: InputDecoration(
|
|
labelText: '소분류',
|
|
hintText: '소분류를 입력 또는 선택하세요',
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
onPressed: _showSubSubCategoryDropdown,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
print('[소분류:onChanged] 입력값: "$value"');
|
|
// 프로그램적 변경이면 무시
|
|
if (_isProgrammaticSubSubCategoryChange) {
|
|
print('[소분류:onChanged] 프로그램적 변경이므로 무시');
|
|
return;
|
|
}
|
|
setState(() {
|
|
print(
|
|
'[소분류:setState:onChanged] _controller.subSubCategory <- "$value"',
|
|
);
|
|
_controller.subSubCategory = value;
|
|
});
|
|
},
|
|
onFieldSubmitted: (value) {
|
|
// 엔터 입력 시 자동완성
|
|
final subSubCategorySuggestion =
|
|
_getSubSubCategoryAutocompleteSuggestion(
|
|
value,
|
|
);
|
|
final showSubSubCategorySuggestion =
|
|
subSubCategorySuggestion != null &&
|
|
subSubCategorySuggestion.length >
|
|
value.length;
|
|
print(
|
|
'[소분류:onFieldSubmitted] 엔터 입력됨, 입력값: "$value", 자동완성 후보: "$subSubCategorySuggestion", showSubSubCategorySuggestion=$showSubSubCategorySuggestion',
|
|
);
|
|
if (showSubSubCategorySuggestion) {
|
|
setState(() {
|
|
print(
|
|
'[소분류:onFieldSubmitted] 자동완성 적용: "$subSubCategorySuggestion"',
|
|
);
|
|
_isProgrammaticSubSubCategoryChange =
|
|
true;
|
|
_subSubCategoryController.text =
|
|
subSubCategorySuggestion;
|
|
_controller.subSubCategory =
|
|
subSubCategorySuggestion;
|
|
// 커서를 맨 뒤로 이동
|
|
_subSubCategoryController
|
|
.selection = TextSelection.collapsed(
|
|
offset: subSubCategorySuggestion.length,
|
|
);
|
|
print(
|
|
'[소분류:onFieldSubmitted] 커서 위치: ${_subSubCategoryController.selection.start}',
|
|
);
|
|
});
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) {
|
|
_isProgrammaticSubSubCategoryChange =
|
|
false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// 입력란 아래에 자동완성 후보 전체를 더 작은 글씨로 명확하게 표시
|
|
if (_getSubSubCategoryAutocompleteSuggestion(
|
|
_subSubCategoryController.text,
|
|
) !=
|
|
null &&
|
|
_getSubSubCategoryAutocompleteSuggestion(
|
|
_subSubCategoryController.text,
|
|
)!.length >
|
|
_subSubCategoryController.text.length)
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 12,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
_getSubSubCategoryAutocompleteSuggestion(
|
|
_subSubCategoryController.text,
|
|
)!,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1976D2),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13, // 더 작은 글씨
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// 시리얼 번호 유무 토글
|
|
FormFieldWrapper(
|
|
label: '시리얼 번호',
|
|
required: false,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Checkbox(
|
|
value: _controller.hasSerialNumber,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_controller.hasSerialNumber = value ?? true;
|
|
});
|
|
},
|
|
),
|
|
const Text('시리얼 번호 있음'),
|
|
],
|
|
),
|
|
if (_controller.hasSerialNumber)
|
|
TextFormField(
|
|
initialValue: _controller.serialNumber,
|
|
decoration: const InputDecoration(
|
|
hintText: '시리얼 번호를 입력하세요',
|
|
),
|
|
validator: (value) {
|
|
if (_controller.hasSerialNumber &&
|
|
(value == null || value.isEmpty)) {
|
|
return '시리얼 번호를 입력해주세요';
|
|
}
|
|
return null;
|
|
},
|
|
onSaved: (value) {
|
|
_controller.serialNumber = value ?? '';
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 바코드 필드
|
|
FormFieldWrapper(
|
|
label: '바코드',
|
|
required: false,
|
|
child: TextFormField(
|
|
initialValue: _controller.barcode,
|
|
decoration: const InputDecoration(
|
|
hintText: '바코드를 입력하세요 (선택사항)',
|
|
),
|
|
onSaved: (value) {
|
|
_controller.barcode = value ?? '';
|
|
},
|
|
),
|
|
),
|
|
// 수량 필드
|
|
FormFieldWrapper(
|
|
label: '수량',
|
|
required: true,
|
|
child: TextFormField(
|
|
initialValue: _controller.quantity.toString(),
|
|
decoration: const InputDecoration(hintText: '수량을 입력하세요'),
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return '수량을 입력해주세요';
|
|
}
|
|
if (int.tryParse(value) == null ||
|
|
int.parse(value) <= 0) {
|
|
return '유효한 수량을 입력해주세요';
|
|
}
|
|
return null;
|
|
},
|
|
onSaved: (value) {
|
|
_controller.quantity = int.tryParse(value ?? '1') ?? 1;
|
|
},
|
|
),
|
|
),
|
|
// 입고일 필드
|
|
FormFieldWrapper(
|
|
label: '입고일',
|
|
required: true,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.inDate,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime.now(),
|
|
);
|
|
if (picked != null && picked != _controller.inDate) {
|
|
setState(() {
|
|
_controller.inDate = picked;
|
|
// 입고일 변경 시 워런티 시작일도 같이 변경
|
|
_controller.warrantyStartDate = picked;
|
|
});
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 15,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade400),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'${_controller.inDate.year}-${_controller.inDate.month.toString().padLeft(2, '0')}-${_controller.inDate.day.toString().padLeft(2, '0')}',
|
|
style: ShadcnTheme.bodyMedium,
|
|
),
|
|
const Icon(Icons.calendar_today, size: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 워런티 정보 섹션
|
|
const SizedBox(height: 16),
|
|
Text('워런티 정보', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 12),
|
|
|
|
// 워런티 필드들을 1행으로 통합 (전체 너비 사용)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 워런티 라이센스
|
|
Expanded(
|
|
flex: 2,
|
|
child: FormFieldWrapper(
|
|
label: '워런티 라이센스',
|
|
required: false,
|
|
child: TextFormField(
|
|
initialValue: _controller.warrantyLicense ?? '',
|
|
decoration: const InputDecoration(
|
|
hintText: '워런티 라이센스명을 입력하세요',
|
|
),
|
|
onChanged: (value) {
|
|
_controller.warrantyLicense = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// 워런티 코드 입력란 추가
|
|
Expanded(
|
|
flex: 2,
|
|
child: FormFieldWrapper(
|
|
label: '워런티 코드',
|
|
required: false,
|
|
child: TextFormField(
|
|
initialValue: _controller.warrantyCode ?? '',
|
|
decoration: const InputDecoration(
|
|
hintText: '워런티 코드를 입력하세요',
|
|
),
|
|
onChanged: (value) {
|
|
_controller.warrantyCode = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// 워런티 시작일
|
|
Expanded(
|
|
flex: 1,
|
|
child: FormFieldWrapper(
|
|
label: '시작일',
|
|
required: false,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.warrantyStartDate,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null &&
|
|
picked != _controller.warrantyStartDate) {
|
|
setState(() {
|
|
_controller.warrantyStartDate = picked;
|
|
});
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 15,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade400),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}',
|
|
style: ShadcnTheme.bodyMedium,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const Icon(Icons.calendar_today, size: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// 워런티 종료일
|
|
Expanded(
|
|
flex: 1,
|
|
child: FormFieldWrapper(
|
|
label: '종료일',
|
|
required: false,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.warrantyEndDate,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null &&
|
|
picked != _controller.warrantyEndDate) {
|
|
setState(() {
|
|
_controller.warrantyEndDate = picked;
|
|
});
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 15,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade400),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}',
|
|
style: ShadcnTheme.bodyMedium,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const Icon(Icons.calendar_today, size: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// 워런티 기간 요약
|
|
Expanded(
|
|
flex: 1,
|
|
child: FormFieldWrapper(
|
|
label: '워런티 기간',
|
|
required: false,
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 15,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
border: Border.all(color: Colors.grey.shade400),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
' ${_controller.getWarrantyPeriodSummary()}',
|
|
style: TextStyle(
|
|
color: Colors.grey.shade700,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 현재 위치 및 상태 정보 섹션
|
|
const SizedBox(height: 24),
|
|
|
|
// 현재 회사 및 지점 정보
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '현재 회사',
|
|
required: false,
|
|
child: DropdownButtonFormField<String>(
|
|
value: _controller.currentCompanyId?.toString(),
|
|
decoration: const InputDecoration(
|
|
hintText: '현재 배치된 회사를 선택하세요',
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(value: null, child: Text('선택하지 않음')),
|
|
// TODO: 실제 회사 목록으로 대체 필요
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_controller.currentCompanyId = value != null ? int.tryParse(value) : null;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '현재 지점',
|
|
required: false,
|
|
child: DropdownButtonFormField<String>(
|
|
value: _controller.currentBranchId?.toString(),
|
|
decoration: const InputDecoration(
|
|
hintText: '현재 배치된 지점을 선택하세요',
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(value: null, child: Text('선택하지 않음')),
|
|
// TODO: 실제 지점 목록으로 대체 필요
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_controller.currentBranchId = value != null ? int.tryParse(value) : null;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// 점검 날짜 정보
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '최근 점검일',
|
|
required: false,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.lastInspectionDate ?? DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime.now(),
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_controller.lastInspectionDate = 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.lastInspectionDate != null
|
|
? '${_controller.lastInspectionDate!.year}-${_controller.lastInspectionDate!.month.toString().padLeft(2, '0')}-${_controller.lastInspectionDate!.day.toString().padLeft(2, '0')}'
|
|
: '날짜를 선택하세요',
|
|
style: TextStyle(
|
|
color: _controller.lastInspectionDate != null
|
|
? Colors.black87
|
|
: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const Icon(Icons.calendar_today, size: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: FormFieldWrapper(
|
|
label: '다음 점검일',
|
|
required: false,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _controller.nextInspectionDate ?? DateTime.now().add(const Duration(days: 365)),
|
|
firstDate: DateTime.now(),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_controller.nextInspectionDate = 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.nextInspectionDate != null
|
|
? '${_controller.nextInspectionDate!.year}-${_controller.nextInspectionDate!.month.toString().padLeft(2, '0')}-${_controller.nextInspectionDate!.day.toString().padLeft(2, '0')}'
|
|
: '날짜를 선택하세요',
|
|
style: TextStyle(
|
|
color: _controller.nextInspectionDate != null
|
|
? Colors.black87
|
|
: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const Icon(Icons.calendar_today, size: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// 장비 상태
|
|
const SizedBox(height: 16),
|
|
FormFieldWrapper(
|
|
label: '장비 상태',
|
|
required: false,
|
|
child: DropdownButtonFormField<String>(
|
|
value: _getValidEquipmentStatus(_controller.equipmentStatus),
|
|
decoration: const InputDecoration(
|
|
hintText: '장비 상태를 선택하세요',
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(value: 'available', child: Text('사용 가능')),
|
|
DropdownMenuItem(value: 'inuse', child: Text('사용 중')),
|
|
DropdownMenuItem(value: 'maintenance', child: Text('유지보수')),
|
|
DropdownMenuItem(value: 'disposed', child: Text('폐기')),
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_controller.equipmentStatus = value;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
|
|
// 비고 입력란 추가
|
|
const SizedBox(height: 16),
|
|
FormFieldWrapper(
|
|
label: '비고',
|
|
required: false,
|
|
child: RemarkInput(
|
|
controller: _controller.remarkController,
|
|
hint: '비고를 입력하세요',
|
|
minLines: 4,
|
|
),
|
|
),
|
|
], // FormSection children 끝
|
|
), // FormSection 끝
|
|
], // Column children 끝
|
|
), // SingleChildScrollView child 끝
|
|
), // Form child 끝
|
|
), // FormLayoutTemplate child 끝
|
|
), // GestureDetector 끝
|
|
);
|
|
}, // Consumer builder 끝
|
|
), // Consumer 끝
|
|
); // ChangeNotifierProvider.value 끝
|
|
}
|
|
}
|