프로젝트 최초 커밋
This commit is contained in:
88
lib/screens/common/custom_widgets/autocomplete_dropdown.dart
Normal file
88
lib/screens/common/custom_widgets/autocomplete_dropdown.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'highlight_text.dart';
|
||||
|
||||
// 자동완성 드롭다운 공통 위젯
|
||||
class AutocompleteDropdown extends StatelessWidget {
|
||||
// 드롭다운에 표시할 항목 리스트
|
||||
final List<String> items;
|
||||
// 현재 입력된 텍스트(하이라이트 기준)
|
||||
final String inputText;
|
||||
// 항목 선택 시 콜백
|
||||
final void Function(String) onSelect;
|
||||
// 드롭다운 표시 여부
|
||||
final bool showDropdown;
|
||||
// 최대 높이(항목 개수에 따라 자동 조절)
|
||||
final double maxHeight;
|
||||
// 드롭다운이 비었을 때 표시할 위젯
|
||||
final Widget emptyWidget;
|
||||
|
||||
const AutocompleteDropdown({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.inputText,
|
||||
required this.onSelect,
|
||||
required this.showDropdown,
|
||||
this.maxHeight = 200,
|
||||
this.emptyWidget = const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text('검색 결과가 없습니다'),
|
||||
),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height:
|
||||
showDropdown
|
||||
? (items.length > 4 ? maxHeight : items.length * 50.0)
|
||||
: 0,
|
||||
margin: EdgeInsets.only(top: showDropdown ? 4 : 0),
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: GestureDetector(
|
||||
onTap: () {}, // 이벤트 버블링 방지
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withAlpha(77),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child:
|
||||
items.isEmpty
|
||||
? emptyWidget
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder:
|
||||
(context, index) =>
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
itemBuilder: (context, index) {
|
||||
final String item = items[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: HighlightText(
|
||||
text: item,
|
||||
highlight: inputText,
|
||||
highlightColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () => onSelect(item),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/screens/common/custom_widgets/category_data.dart
Normal file
18
lib/screens/common/custom_widgets/category_data.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
// 카테고리 데이터 (예시)
|
||||
final Map<String, Map<String, List<String>>> categoryData = {
|
||||
'컴퓨터': {
|
||||
'데스크탑': ['사무용', '게이밍', '워크스테이션'],
|
||||
'노트북': ['사무용', '게이밍', '울트라북'],
|
||||
'태블릿': ['안드로이드', 'iOS', '윈도우'],
|
||||
},
|
||||
'네트워크': {
|
||||
'라우터': ['가정용', '기업용', '산업용'],
|
||||
'스위치': ['관리형', '비관리형'],
|
||||
'액세스 포인트': ['실내용', '실외용'],
|
||||
},
|
||||
'주변기기': {
|
||||
'모니터': ['LCD', 'LED', 'OLED'],
|
||||
'키보드': ['유선', '무선', '기계식'],
|
||||
'마우스': ['유선', '무선', '트랙볼'],
|
||||
},
|
||||
};
|
||||
562
lib/screens/common/custom_widgets/category_selection_field.dart
Normal file
562
lib/screens/common/custom_widgets/category_selection_field.dart
Normal file
@@ -0,0 +1,562 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'autocomplete_dropdown.dart';
|
||||
import 'form_field_wrapper.dart';
|
||||
import 'category_data.dart';
|
||||
|
||||
// 카테고리 선택 필드 (대분류/중분류/소분류)
|
||||
class CategorySelectionField extends StatefulWidget {
|
||||
final String category;
|
||||
final String subCategory;
|
||||
final String subSubCategory;
|
||||
final Function(String, String, String) onCategoryChanged;
|
||||
final bool isRequired;
|
||||
|
||||
const CategorySelectionField({
|
||||
Key? key,
|
||||
required this.category,
|
||||
required this.subCategory,
|
||||
required this.subSubCategory,
|
||||
required this.onCategoryChanged,
|
||||
this.isRequired = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CategorySelectionField> createState() => _CategorySelectionFieldState();
|
||||
}
|
||||
|
||||
class _CategorySelectionFieldState extends State<CategorySelectionField> {
|
||||
// 검색 관련 컨트롤러 및 상태 변수
|
||||
final TextEditingController _categoryController = TextEditingController();
|
||||
final FocusNode _categoryFocusNode = FocusNode();
|
||||
bool _showCategoryDropdown = false;
|
||||
List<String> _filteredCategories = [];
|
||||
|
||||
// 중분류 관련 변수
|
||||
final TextEditingController _subCategoryController = TextEditingController();
|
||||
final FocusNode _subCategoryFocusNode = FocusNode();
|
||||
bool _showSubCategoryDropdown = false;
|
||||
List<String> _filteredSubCategories = [];
|
||||
|
||||
// 소분류 관련 변수
|
||||
final TextEditingController _subSubCategoryController =
|
||||
TextEditingController();
|
||||
final FocusNode _subSubCategoryFocusNode = FocusNode();
|
||||
bool _showSubSubCategoryDropdown = false;
|
||||
List<String> _filteredSubSubCategories = [];
|
||||
|
||||
List<String> _allCategories = [];
|
||||
String _selectedCategory = '';
|
||||
String _selectedSubCategory = '';
|
||||
String _selectedSubSubCategory = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedCategory = widget.category;
|
||||
_selectedSubCategory = widget.subCategory;
|
||||
_selectedSubSubCategory = widget.subSubCategory;
|
||||
_categoryController.text = _selectedCategory;
|
||||
_subCategoryController.text = _selectedSubCategory;
|
||||
_subSubCategoryController.text = _selectedSubSubCategory;
|
||||
|
||||
// 모든 카테고리 목록 초기화
|
||||
_allCategories = categoryData.keys.toList();
|
||||
_filteredCategories = List.from(_allCategories);
|
||||
|
||||
// 중분류 목록 초기화
|
||||
_updateSubCategories();
|
||||
|
||||
// 소분류 목록 초기화
|
||||
_updateSubSubCategories();
|
||||
|
||||
// 대분류 컨트롤러 리스너 설정
|
||||
_categoryController.addListener(_onCategoryTextChanged);
|
||||
_categoryFocusNode.addListener(() {
|
||||
setState(() {
|
||||
if (_categoryFocusNode.hasFocus) {
|
||||
_showCategoryDropdown = _filteredCategories.isNotEmpty;
|
||||
} else {
|
||||
_showCategoryDropdown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 중분류 컨트롤러 리스너 설정
|
||||
_subCategoryController.addListener(_onSubCategoryTextChanged);
|
||||
_subCategoryFocusNode.addListener(() {
|
||||
setState(() {
|
||||
if (_subCategoryFocusNode.hasFocus) {
|
||||
_showSubCategoryDropdown = _filteredSubCategories.isNotEmpty;
|
||||
} else {
|
||||
_showSubCategoryDropdown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 소분류 컨트롤러 리스너 설정
|
||||
_subSubCategoryController.addListener(_onSubSubCategoryTextChanged);
|
||||
_subSubCategoryFocusNode.addListener(() {
|
||||
setState(() {
|
||||
if (_subSubCategoryFocusNode.hasFocus) {
|
||||
_showSubSubCategoryDropdown = _filteredSubSubCategories.isNotEmpty;
|
||||
} else {
|
||||
_showSubSubCategoryDropdown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_categoryController.dispose();
|
||||
_categoryFocusNode.dispose();
|
||||
_subCategoryController.dispose();
|
||||
_subCategoryFocusNode.dispose();
|
||||
_subSubCategoryController.dispose();
|
||||
_subSubCategoryFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 중분류 목록 업데이트
|
||||
void _updateSubCategories() {
|
||||
if (_selectedCategory.isNotEmpty) {
|
||||
final subCategories =
|
||||
categoryData[_selectedCategory]?.keys.toList() ?? [];
|
||||
_filteredSubCategories = List.from(subCategories);
|
||||
} else {
|
||||
_filteredSubCategories = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 소분류 목록 업데이트
|
||||
void _updateSubSubCategories() {
|
||||
if (_selectedCategory.isNotEmpty && _selectedSubCategory.isNotEmpty) {
|
||||
final subSubCategories =
|
||||
categoryData[_selectedCategory]?[_selectedSubCategory] ?? [];
|
||||
_filteredSubSubCategories = List.from(subSubCategories);
|
||||
} else {
|
||||
_filteredSubSubCategories = [];
|
||||
}
|
||||
}
|
||||
|
||||
void _onCategoryTextChanged() {
|
||||
final text = _categoryController.text;
|
||||
setState(() {
|
||||
_selectedCategory = text;
|
||||
|
||||
if (text.isEmpty) {
|
||||
_filteredCategories = List.from(_allCategories);
|
||||
} else {
|
||||
_filteredCategories =
|
||||
_allCategories
|
||||
.where(
|
||||
(item) => item.toLowerCase().contains(text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// 시작 부분이 일치하는 항목 우선 정렬
|
||||
_filteredCategories.sort((a, b) {
|
||||
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
|
||||
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
|
||||
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
|
||||
_showCategoryDropdown =
|
||||
_filteredCategories.isNotEmpty && _categoryFocusNode.hasFocus;
|
||||
|
||||
// 카테고리가 변경되면 하위 카테고리 초기화
|
||||
if (_selectedCategory != widget.category) {
|
||||
_selectedSubCategory = '';
|
||||
_subCategoryController.text = '';
|
||||
_selectedSubSubCategory = '';
|
||||
_subSubCategoryController.text = '';
|
||||
_updateSubCategories();
|
||||
_updateSubSubCategories();
|
||||
}
|
||||
|
||||
// 콜백 호출
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 중분류 텍스트 변경 핸들러
|
||||
void _onSubCategoryTextChanged() {
|
||||
final text = _subCategoryController.text;
|
||||
setState(() {
|
||||
_selectedSubCategory = text;
|
||||
|
||||
if (_selectedCategory.isNotEmpty) {
|
||||
final subCategories =
|
||||
categoryData[_selectedCategory]?.keys.toList() ?? [];
|
||||
|
||||
if (text.isEmpty) {
|
||||
_filteredSubCategories = List.from(subCategories);
|
||||
} else {
|
||||
_filteredSubCategories =
|
||||
subCategories
|
||||
.where(
|
||||
(item) => item.toLowerCase().contains(text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// 시작 부분이 일치하는 항목 우선 정렬
|
||||
_filteredSubCategories.sort((a, b) {
|
||||
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
|
||||
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
|
||||
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_filteredSubCategories = [];
|
||||
}
|
||||
|
||||
_showSubCategoryDropdown =
|
||||
_filteredSubCategories.isNotEmpty && _subCategoryFocusNode.hasFocus;
|
||||
|
||||
// 중분류가 변경되면 소분류 초기화
|
||||
if (_selectedSubCategory != widget.subCategory) {
|
||||
_selectedSubSubCategory = '';
|
||||
_subSubCategoryController.text = '';
|
||||
_updateSubSubCategories();
|
||||
}
|
||||
|
||||
// 콜백 호출
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 소분류 텍스트 변경 핸들러
|
||||
void _onSubSubCategoryTextChanged() {
|
||||
final text = _subSubCategoryController.text;
|
||||
setState(() {
|
||||
_selectedSubSubCategory = text;
|
||||
|
||||
if (_selectedCategory.isNotEmpty && _selectedSubCategory.isNotEmpty) {
|
||||
final subSubCategories =
|
||||
categoryData[_selectedCategory]?[_selectedSubCategory] ?? [];
|
||||
|
||||
if (text.isEmpty) {
|
||||
_filteredSubSubCategories = List.from(subSubCategories);
|
||||
} else {
|
||||
_filteredSubSubCategories =
|
||||
subSubCategories
|
||||
.where(
|
||||
(item) => item.toLowerCase().contains(text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// 시작 부분이 일치하는 항목 우선 정렬
|
||||
_filteredSubSubCategories.sort((a, b) {
|
||||
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
|
||||
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
|
||||
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_filteredSubSubCategories = [];
|
||||
}
|
||||
|
||||
_showSubSubCategoryDropdown =
|
||||
_filteredSubSubCategories.isNotEmpty &&
|
||||
_subSubCategoryFocusNode.hasFocus;
|
||||
|
||||
// 콜백 호출
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _selectCategory(String category) {
|
||||
setState(() {
|
||||
_selectedCategory = category;
|
||||
_categoryController.text = category;
|
||||
_showCategoryDropdown = false;
|
||||
_selectedSubCategory = '';
|
||||
_subCategoryController.text = '';
|
||||
_selectedSubSubCategory = '';
|
||||
_subSubCategoryController.text = '';
|
||||
_updateSubCategories();
|
||||
_updateSubSubCategories();
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 중분류 선택 핸들러
|
||||
void _selectSubCategory(String subCategory) {
|
||||
setState(() {
|
||||
_selectedSubCategory = subCategory;
|
||||
_subCategoryController.text = subCategory;
|
||||
_showSubCategoryDropdown = false;
|
||||
_selectedSubSubCategory = '';
|
||||
_subSubCategoryController.text = '';
|
||||
_updateSubSubCategories();
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 소분류 선택 핸들러
|
||||
void _selectSubSubCategory(String subSubCategory) {
|
||||
setState(() {
|
||||
_selectedSubSubCategory = subSubCategory;
|
||||
_subSubCategoryController.text = subSubCategory;
|
||||
_showSubSubCategoryDropdown = false;
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
_selectedSubSubCategory,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormFieldWrapper(
|
||||
label: '카테고리',
|
||||
isRequired: widget.isRequired,
|
||||
child: Column(
|
||||
children: [
|
||||
// 대분류 입력 필드 (자동완성)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _categoryController,
|
||||
focusNode: _categoryFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '대분류',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon:
|
||||
_categoryController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_categoryController.clear();
|
||||
_selectedCategory = '';
|
||||
_selectedSubCategory = '';
|
||||
_selectedSubSubCategory = '';
|
||||
_subCategoryController.clear();
|
||||
_subSubCategoryController.clear();
|
||||
_filteredCategories = List.from(_allCategories);
|
||||
_filteredSubCategories = [];
|
||||
_filteredSubSubCategories = [];
|
||||
_showCategoryDropdown =
|
||||
_categoryFocusNode.hasFocus;
|
||||
widget.onCategoryChanged('', '', '');
|
||||
});
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showCategoryDropdown = !_showCategoryDropdown;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return '대분류를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (!_showCategoryDropdown) {
|
||||
_showCategoryDropdown = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 대분류 자동완성 드롭다운
|
||||
AutocompleteDropdown(
|
||||
items: _filteredCategories,
|
||||
inputText: _categoryController.text,
|
||||
onSelect: _selectCategory,
|
||||
showDropdown: _showCategoryDropdown,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 중분류 및 소분류 선택 행
|
||||
Row(
|
||||
children: [
|
||||
// 중분류 입력 필드 (자동완성)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _subCategoryController,
|
||||
focusNode: _subCategoryFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '중분류',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon:
|
||||
_subCategoryController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_subCategoryController.clear();
|
||||
_selectedSubCategory = '';
|
||||
_selectedSubSubCategory = '';
|
||||
_subSubCategoryController.clear();
|
||||
_updateSubCategories();
|
||||
_updateSubSubCategories();
|
||||
_showSubCategoryDropdown =
|
||||
_subCategoryFocusNode.hasFocus;
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showSubCategoryDropdown =
|
||||
!_showSubCategoryDropdown;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
enabled: _selectedCategory.isNotEmpty,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (!_showSubCategoryDropdown &&
|
||||
_filteredSubCategories.isNotEmpty) {
|
||||
_showSubCategoryDropdown = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 중분류 자동완성 드롭다운
|
||||
AutocompleteDropdown(
|
||||
items: _filteredSubCategories,
|
||||
inputText: _subCategoryController.text,
|
||||
onSelect: _selectSubCategory,
|
||||
showDropdown: _showSubCategoryDropdown,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 소분류 입력 필드 (자동완성)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _subSubCategoryController,
|
||||
focusNode: _subSubCategoryFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '소분류',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon:
|
||||
_subSubCategoryController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_subSubCategoryController.clear();
|
||||
_selectedSubSubCategory = '';
|
||||
_updateSubSubCategories();
|
||||
_showSubSubCategoryDropdown =
|
||||
_subSubCategoryFocusNode.hasFocus;
|
||||
widget.onCategoryChanged(
|
||||
_selectedCategory,
|
||||
_selectedSubCategory,
|
||||
'',
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showSubSubCategoryDropdown =
|
||||
!_showSubSubCategoryDropdown;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
enabled:
|
||||
_selectedCategory.isNotEmpty &&
|
||||
_selectedSubCategory.isNotEmpty,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (!_showSubSubCategoryDropdown &&
|
||||
_filteredSubSubCategories.isNotEmpty) {
|
||||
_showSubSubCategoryDropdown = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 소분류 자동완성 드롭다운
|
||||
AutocompleteDropdown(
|
||||
items: _filteredSubSubCategories,
|
||||
inputText: _subSubCategoryController.text,
|
||||
onSelect: _selectSubSubCategory,
|
||||
showDropdown: _showSubSubCategoryDropdown,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/screens/common/custom_widgets/data_table_card.dart
Normal file
32
lib/screens/common/custom_widgets/data_table_card.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
|
||||
// 데이터 테이블 카드
|
||||
class DataTableCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String? title;
|
||||
final double? width;
|
||||
|
||||
const DataTableCard({Key? key, required this.child, this.title, this.width})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
decoration: AppThemeTailwind.cardDecoration,
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(title!, style: AppThemeTailwind.subheadingStyle),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(16.0), child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/screens/common/custom_widgets/date_picker_field.dart
Normal file
57
lib/screens/common/custom_widgets/date_picker_field.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'form_field_wrapper.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
|
||||
// 날짜 선택 필드
|
||||
class DatePickerField extends StatelessWidget {
|
||||
final DateTime selectedDate;
|
||||
final Function(DateTime) onDateChanged;
|
||||
final bool allowFutureDate;
|
||||
final bool isRequired;
|
||||
|
||||
const DatePickerField({
|
||||
Key? key,
|
||||
required this.selectedDate,
|
||||
required this.onDateChanged,
|
||||
this.allowFutureDate = false,
|
||||
this.isRequired = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: allowFutureDate ? DateTime(2100) : DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != selectedDate) {
|
||||
onDateChanged(picked);
|
||||
}
|
||||
},
|
||||
child: FormFieldWrapper(
|
||||
label: '날짜',
|
||||
isRequired: isRequired,
|
||||
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(
|
||||
'${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/screens/common/custom_widgets/form_field_wrapper.dart
Normal file
48
lib/screens/common/custom_widgets/form_field_wrapper.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 폼 필드 래퍼
|
||||
class FormFieldWrapper extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool isRequired;
|
||||
|
||||
const FormFieldWrapper({
|
||||
Key? key,
|
||||
required this.label,
|
||||
required this.child,
|
||||
this.isRequired = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (isRequired)
|
||||
const Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/screens/common/custom_widgets/highlight_text.dart
Normal file
53
lib/screens/common/custom_widgets/highlight_text.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 자동완성 드롭다운에서 텍스트 하이라이트를 위한 위젯
|
||||
class HighlightText extends StatelessWidget {
|
||||
// 전체 텍스트
|
||||
final String text;
|
||||
// 하이라이트할 부분
|
||||
final String highlight;
|
||||
// 하이라이트 색상
|
||||
final Color highlightColor;
|
||||
// 텍스트 스타일
|
||||
final TextStyle? style;
|
||||
|
||||
const HighlightText({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.highlight,
|
||||
this.highlightColor = Colors.blue,
|
||||
this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (highlight.isEmpty) {
|
||||
// 하이라이트가 없으면 전체 텍스트 반환
|
||||
return Text(text, style: style);
|
||||
}
|
||||
final String lowerText = text.toLowerCase();
|
||||
final String lowerHighlight = highlight.toLowerCase();
|
||||
final int start = lowerText.indexOf(lowerHighlight);
|
||||
if (start < 0) {
|
||||
// 일치하는 부분이 없으면 전체 텍스트 반환
|
||||
return Text(text, style: style);
|
||||
}
|
||||
final int end = start + highlight.length;
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
if (start > 0) TextSpan(text: text.substring(0, start)),
|
||||
TextSpan(
|
||||
text: text.substring(start, end),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: highlightColor,
|
||||
),
|
||||
),
|
||||
if (end < text.length) TextSpan(text: text.substring(end)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/screens/common/custom_widgets/page_title.dart
Normal file
27
lib/screens/common/custom_widgets/page_title.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
|
||||
// 페이지 타이틀 위젯
|
||||
class PageTitle extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? rightWidget;
|
||||
final double? width;
|
||||
|
||||
const PageTitle({Key? key, required this.title, this.rightWidget, this.width})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: AppThemeTailwind.headingStyle),
|
||||
if (rightWidget != null) rightWidget!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user