프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View File

@@ -0,0 +1,405 @@
import 'package:flutter/material.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/screens/common/main_layout.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/widgets/pagination.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/common/widgets/category_autocomplete_field.dart';
/// 물품 관리(등록) 화면
/// 이름, 제조사, 대분류, 중분류, 소분류만 등록/조회 가능
class GoodsListScreen extends StatefulWidget {
const GoodsListScreen({super.key});
@override
State<GoodsListScreen> createState() => _GoodsListScreenState();
}
class _GoodsListScreenState extends State<GoodsListScreen> {
final MockDataService _dataService = MockDataService();
late List<_GoodsItem> _goodsList;
int _currentPage = 1;
final int _pageSize = 10;
@override
void initState() {
super.initState();
_loadGoods();
}
void _loadGoods() {
final allEquipments = _dataService.getAllEquipmentIns();
final goodsSet = <String, _GoodsItem>{};
for (final equipmentIn in allEquipments) {
final eq = equipmentIn.equipment;
final key =
'${eq.manufacturer}|${eq.name}|${eq.category}|${eq.subCategory}|${eq.subSubCategory}';
goodsSet[key] = _GoodsItem(
name: eq.name,
manufacturer: eq.manufacturer,
category: eq.category,
subCategory: eq.subCategory,
subSubCategory: eq.subSubCategory,
);
}
setState(() {
_goodsList = goodsSet.values.toList();
});
}
void _showAddGoodsDialog() async {
final result = await showDialog<_GoodsItem>(
context: context,
builder: (context) => _GoodsFormDialog(),
);
if (result != null) {
setState(() {
_goodsList.add(result);
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('물품이 등록되었습니다.')));
}
}
void _showEditGoodsDialog(int index) async {
final result = await showDialog<_GoodsItem>(
context: context,
builder: (context) => _GoodsFormDialog(item: _goodsList[index]),
);
if (result != null) {
setState(() {
_goodsList[index] = result;
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('물품 정보가 수정되었습니다.')));
}
}
void _deleteGoods(int index) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('삭제 확인'),
content: const Text('이 물품 정보를 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('취소'),
),
TextButton(
onPressed: () {
setState(() {
_goodsList.removeAt(index);
});
Navigator.pop(context);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('물품이 삭제되었습니다.')));
},
child: const Text('삭제'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32;
final int totalCount = _goodsList.length;
final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex =
(startIndex + _pageSize) > totalCount
? totalCount
: (startIndex + _pageSize);
final pagedGoods = _goodsList.sublist(startIndex, endIndex);
return MainLayout(
title: '물품 관리',
currentRoute: Routes.goods,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadGoods,
color: Colors.grey,
),
],
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageTitle(
title: '물품 목록',
width: maxContentWidth - 32,
rightWidget: ElevatedButton.icon(
onPressed: _showAddGoodsDialog,
icon: const Icon(Icons.add),
label: const Text('추가'),
style: AppThemeTailwind.primaryButtonStyle,
),
),
Expanded(
child: DataTableCard(
width: maxContentWidth - 32,
child:
pagedGoods.isEmpty
? const Center(child: Text('등록된 물품이 없습니다.'))
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
constraints: BoxConstraints(
minWidth: maxContentWidth - 64,
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: DataTable(
columns: const [
DataColumn(label: Text('번호')),
DataColumn(label: Text('이름')),
DataColumn(label: Text('제조사')),
DataColumn(label: Text('대분류')),
DataColumn(label: Text('중분류')),
DataColumn(label: Text('소분류')),
DataColumn(label: Text('관리')),
],
rows: List.generate(pagedGoods.length, (i) {
final item = pagedGoods[i];
final realIndex = startIndex + i;
return DataRow(
cells: [
DataCell(Text('${realIndex + 1}')),
DataCell(Text(item.name)),
DataCell(Text(item.manufacturer)),
DataCell(Text(item.category)),
DataCell(Text(item.subCategory)),
DataCell(Text(item.subSubCategory)),
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.edit,
color: AppThemeTailwind.primary,
),
onPressed:
() => _showEditGoodsDialog(
realIndex,
),
),
IconButton(
icon: const Icon(
Icons.delete,
color: AppThemeTailwind.danger,
),
onPressed:
() => _deleteGoods(realIndex),
),
],
),
),
],
);
}),
),
),
),
),
),
),
if (totalCount > _pageSize)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Pagination(
totalCount: totalCount,
currentPage: _currentPage,
pageSize: _pageSize,
onPageChanged: (page) {
setState(() {
_currentPage = page;
});
},
),
),
],
),
),
);
}
}
/// 물품 데이터 모델 (이름, 제조사, 대중소분류)
class _GoodsItem {
final String name;
final String manufacturer;
final String category;
final String subCategory;
final String subSubCategory;
_GoodsItem({
required this.name,
required this.manufacturer,
required this.category,
required this.subCategory,
required this.subSubCategory,
});
}
/// 물품 등록/수정 폼 다이얼로그
class _GoodsFormDialog extends StatefulWidget {
final _GoodsItem? item;
const _GoodsFormDialog({this.item});
@override
State<_GoodsFormDialog> createState() => _GoodsFormDialogState();
}
class _GoodsFormDialogState extends State<_GoodsFormDialog> {
final _formKey = GlobalKey<FormState>();
late String _name;
late String _manufacturer;
late String _category;
late String _subCategory;
late String _subSubCategory;
late final MockDataService _dataService;
late final List<String> _manufacturerList;
late final List<String> _nameList;
late final List<String> _categoryList;
late final List<String> _subCategoryList;
late final List<String> _subSubCategoryList;
@override
void initState() {
super.initState();
_name = widget.item?.name ?? '';
_manufacturer = widget.item?.manufacturer ?? '';
_category = widget.item?.category ?? '';
_subCategory = widget.item?.subCategory ?? '';
_subSubCategory = widget.item?.subSubCategory ?? '';
_dataService = MockDataService();
_manufacturerList = _dataService.getAllManufacturers();
_nameList = _dataService.getAllEquipmentNames();
_categoryList = _dataService.getAllCategories();
_subCategoryList = _dataService.getAllSubCategories();
_subSubCategoryList = _dataService.getAllSubSubCategories();
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.item == null ? '신상품 등록' : '신상품 정보 수정',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
FormFieldWrapper(
label: '이름',
isRequired: true,
child: CategoryAutocompleteField(
hintText: '이름을 입력 또는 선택하세요',
value: _name,
items: _nameList,
isRequired: true,
onSelect: (v) => setState(() => _name = v),
),
),
FormFieldWrapper(
label: '제조사',
isRequired: true,
child: CategoryAutocompleteField(
hintText: '제조사를 입력 또는 선택하세요',
value: _manufacturer,
items: _manufacturerList,
isRequired: true,
onSelect: (v) => setState(() => _manufacturer = v),
),
),
FormFieldWrapper(
label: '대분류',
isRequired: true,
child: CategoryAutocompleteField(
hintText: '대분류를 입력 또는 선택하세요',
value: _category,
items: _categoryList,
isRequired: true,
onSelect: (v) => setState(() => _category = v),
),
),
FormFieldWrapper(
label: '중분류',
isRequired: true,
child: CategoryAutocompleteField(
hintText: '중분류를 입력 또는 선택하세요',
value: _subCategory,
items: _subCategoryList,
isRequired: true,
onSelect: (v) => setState(() => _subCategory = v),
),
),
FormFieldWrapper(
label: '소분류',
isRequired: true,
child: CategoryAutocompleteField(
hintText: '소분류를 입력 또는 선택하세요',
value: _subSubCategory,
items: _subSubCategoryList,
isRequired: true,
onSelect: (v) => setState(() => _subSubCategory = v),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
const SizedBox(width: 8),
ElevatedButton(
style: AppThemeTailwind.primaryButtonStyle,
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
Navigator.of(context).pop(
_GoodsItem(
name: _name,
manufacturer: _manufacturer,
category: _category,
subCategory: _subCategory,
subSubCategory: _subSubCategory,
),
);
}
},
child: Text(widget.item == null ? '등록' : '수정'),
),
],
),
],
),
),
),
),
);
}
}