사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
188
lib/screens/model/components/model_grouped_table.dart
Normal file
188
lib/screens/model/components/model_grouped_table.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/data/models/vendor_dto.dart';
|
||||
import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
|
||||
/// Vendor별로 그룹화된 Model 테이블
|
||||
class ModelGroupedTable extends StatelessWidget {
|
||||
final ModelController controller;
|
||||
final Function(ModelDto) onEdit;
|
||||
final Function(ModelDto) onDelete;
|
||||
|
||||
const ModelGroupedTable({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final modelsByVendor = controller.modelsByVendor;
|
||||
|
||||
if (modelsByVendor.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('등록된 모델이 없습니다.'),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: modelsByVendor.length,
|
||||
itemBuilder: (context, index) {
|
||||
final vendorId = modelsByVendor.keys.elementAt(index);
|
||||
final vendor = controller.getVendorById(vendorId);
|
||||
final models = modelsByVendor[vendorId] ?? [];
|
||||
|
||||
return _buildVendorGroup(context, vendor, models);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVendorGroup(
|
||||
BuildContext context,
|
||||
VendorDto? vendor,
|
||||
List<ModelDto> models,
|
||||
) {
|
||||
return ShadCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Vendor 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business,
|
||||
size: 20,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
vendor?.name ?? 'Unknown Vendor',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ShadBadge(
|
||||
child: Text('${models.length}개 모델'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Model 목록
|
||||
...models.map((model) => _buildModelRow(context, model)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelRow(BuildContext context, ModelDto model) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Model ID
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
'#${model.id}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Model Name
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
model.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Description
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
'-', // 백엔드에 description 필드 없음
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Status
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: ShadBadge(
|
||||
backgroundColor: model.isActive
|
||||
? Colors.green.shade100
|
||||
: Colors.grey.shade200,
|
||||
child: Text(
|
||||
model.isActive ? '활성' : '비활성',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: model.isActive ? Colors.green.shade700 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
onPressed: () => onEdit(model),
|
||||
tooltip: '수정',
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
onPressed: () => onDelete(model),
|
||||
tooltip: '삭제',
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
226
lib/screens/model/components/model_vendor_cascade.dart
Normal file
226
lib/screens/model/components/model_vendor_cascade.dart
Normal file
@@ -0,0 +1,226 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/data/models/vendor_dto.dart';
|
||||
import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
|
||||
/// Vendor → Model 캐스케이드 선택 컴포넌트
|
||||
/// Equipment 등의 화면에서 재사용 가능
|
||||
class ModelVendorCascade extends StatefulWidget {
|
||||
final ModelController controller;
|
||||
final int? initialVendorId;
|
||||
final int? initialModelId;
|
||||
final void Function(int? vendorId, int? modelId)? onChanged;
|
||||
final bool isRequired;
|
||||
|
||||
const ModelVendorCascade({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.initialVendorId,
|
||||
this.initialModelId,
|
||||
this.onChanged,
|
||||
this.isRequired = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ModelVendorCascade> createState() => _ModelVendorCascadeState();
|
||||
}
|
||||
|
||||
class _ModelVendorCascadeState extends State<ModelVendorCascade> {
|
||||
int? _selectedVendorId;
|
||||
int? _selectedModelId;
|
||||
List<ModelDto> _availableModels = [];
|
||||
bool _isLoadingModels = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedVendorId = widget.initialVendorId;
|
||||
_selectedModelId = widget.initialModelId;
|
||||
|
||||
// 초기 vendor가 있으면 모델 로드
|
||||
if (_selectedVendorId != null) {
|
||||
_loadModelsForVendor(_selectedVendorId!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadModelsForVendor(int vendorId) async {
|
||||
setState(() {
|
||||
_isLoadingModels = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Controller에서 해당 vendor의 모델 가져오기
|
||||
final models = widget.controller.getModelsByVendor(vendorId);
|
||||
|
||||
setState(() {
|
||||
_availableModels = models;
|
||||
// 선택된 모델이 새 vendor의 모델 목록에 없으면 초기화
|
||||
if (_selectedModelId != null &&
|
||||
!models.any((m) => m.id == _selectedModelId)) {
|
||||
_selectedModelId = null;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoadingModels = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Vendor 선택
|
||||
_buildVendorSelect(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Model 선택 (Vendor가 선택된 경우에만 표시)
|
||||
if (_selectedVendorId != null) _buildModelSelect(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVendorSelect() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'제조사',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
const Text(
|
||||
' *',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<int?>(
|
||||
placeholder: const Text('제조사를 선택하세요'),
|
||||
options: [
|
||||
if (!widget.isRequired)
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('선택 안함'),
|
||||
),
|
||||
...widget.controller.vendors.map(
|
||||
(vendor) => ShadOption(
|
||||
value: vendor.id,
|
||||
child: Text(vendor.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return const Text('선택 안함');
|
||||
}
|
||||
final vendor = widget.controller.vendors.firstWhere(
|
||||
(v) => v.id == value,
|
||||
orElse: () => const VendorDto(
|
||||
name: 'Unknown',
|
||||
),
|
||||
);
|
||||
return Text(vendor.name);
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedVendorId = value;
|
||||
_selectedModelId = null; // Vendor 변경 시 Model 초기화
|
||||
_availableModels.clear();
|
||||
});
|
||||
|
||||
if (value != null) {
|
||||
_loadModelsForVendor(value);
|
||||
}
|
||||
|
||||
// 콜백 호출
|
||||
widget.onChanged?.call(value, null);
|
||||
},
|
||||
initialValue: _selectedVendorId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelSelect() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'모델',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
const Text(
|
||||
' *',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_isLoadingModels)
|
||||
const ShadInput(
|
||||
placeholder: Text('모델 로딩 중...'),
|
||||
enabled: false,
|
||||
)
|
||||
else if (_availableModels.isEmpty)
|
||||
const ShadInput(
|
||||
placeholder: Text('해당 제조사에 등록된 모델이 없습니다'),
|
||||
enabled: false,
|
||||
)
|
||||
else
|
||||
ShadSelect<int?>(
|
||||
placeholder: const Text('모델을 선택하세요'),
|
||||
options: [
|
||||
if (!widget.isRequired)
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('선택 안함'),
|
||||
),
|
||||
..._availableModels.map(
|
||||
(model) => ShadOption(
|
||||
value: model.id,
|
||||
child: Text(model.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return const Text('선택 안함');
|
||||
}
|
||||
final model = _availableModels.firstWhere(
|
||||
(m) => m.id == value,
|
||||
orElse: () => ModelDto(
|
||||
id: value,
|
||||
vendorsId: _selectedVendorId!,
|
||||
name: 'Unknown',
|
||||
),
|
||||
);
|
||||
return Text(model.name);
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedModelId = value;
|
||||
});
|
||||
|
||||
// 콜백 호출
|
||||
widget.onChanged?.call(_selectedVendorId, value);
|
||||
},
|
||||
initialValue: _selectedModelId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
253
lib/screens/model/controllers/model_controller.dart
Normal file
253
lib/screens/model/controllers/model_controller.dart
Normal file
@@ -0,0 +1,253 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/data/models/vendor_dto.dart';
|
||||
import 'package:superport/domain/usecases/model_usecase.dart';
|
||||
import 'package:superport/domain/usecases/vendor_usecase.dart';
|
||||
|
||||
/// Model 관리 화면의 상태 관리 Controller
|
||||
@lazySingleton
|
||||
class ModelController extends ChangeNotifier {
|
||||
final ModelUseCase _modelUseCase;
|
||||
final VendorUseCase _vendorUseCase;
|
||||
|
||||
ModelController(this._modelUseCase, this._vendorUseCase);
|
||||
|
||||
// 상태 변수들
|
||||
List<ModelDto> _models = [];
|
||||
List<ModelDto> _filteredModels = [];
|
||||
List<VendorDto> _vendors = [];
|
||||
final Map<int, List<ModelDto>> _modelsByVendor = {};
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isSubmitting = false;
|
||||
String? _errorMessage;
|
||||
String _searchQuery = '';
|
||||
int? _selectedVendorId;
|
||||
|
||||
// Getters
|
||||
List<ModelDto> get models => _filteredModels;
|
||||
List<ModelDto> get allModels => _models;
|
||||
List<VendorDto> get vendors => _vendors;
|
||||
Map<int, List<ModelDto>> get modelsByVendor => _modelsByVendor;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isSubmitting => _isSubmitting;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String get searchQuery => _searchQuery;
|
||||
int? get selectedVendorId => _selectedVendorId;
|
||||
|
||||
/// 초기 데이터 로드
|
||||
Future<void> loadInitialData() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Vendor와 Model 데이터 병렬 로드
|
||||
final results = await Future.wait([
|
||||
_vendorUseCase.getVendors(),
|
||||
_modelUseCase.getModels(),
|
||||
]);
|
||||
|
||||
_vendors = List<VendorDto>.from(results[0] as List<VendorDto>);
|
||||
_models = List<ModelDto>.from(results[1] as List<ModelDto>);
|
||||
_filteredModels = List.from(_models);
|
||||
|
||||
// Vendor별로 모델 그룹핑
|
||||
await _groupModelsByVendor();
|
||||
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 목록 새로고침
|
||||
Future<void> refreshModels() async {
|
||||
_errorMessage = null;
|
||||
|
||||
try {
|
||||
_models = List.from(await _modelUseCase.getModels(vendorId: _selectedVendorId));
|
||||
_applyFilters();
|
||||
await _groupModelsByVendor();
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 생성
|
||||
Future<bool> createModel({
|
||||
required int vendorsId,
|
||||
required String name,
|
||||
}) async {
|
||||
_isSubmitting = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final newModel = await _modelUseCase.createModel(
|
||||
vendorsId: vendorsId,
|
||||
name: name,
|
||||
);
|
||||
|
||||
// 목록에 추가
|
||||
_models = [..._models, newModel];
|
||||
_applyFilters();
|
||||
await _groupModelsByVendor();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 수정
|
||||
Future<bool> updateModel({
|
||||
required int id,
|
||||
required int vendorsId,
|
||||
required String name,
|
||||
}) async {
|
||||
_isSubmitting = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final updatedModel = await _modelUseCase.updateModel(
|
||||
id: id,
|
||||
vendorsId: vendorsId,
|
||||
name: name,
|
||||
);
|
||||
|
||||
// 목록에서 업데이트
|
||||
final index = _models.indexWhere((m) => m.id == id);
|
||||
if (index != -1) {
|
||||
_models = _models.map((model) =>
|
||||
model.id == id ? updatedModel : model
|
||||
).toList();
|
||||
_applyFilters();
|
||||
await _groupModelsByVendor();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 삭제 (Soft Delete)
|
||||
Future<bool> deleteModel(int id) async {
|
||||
_isSubmitting = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _modelUseCase.deleteModel(id);
|
||||
|
||||
// 목록에서 제거 또는 비활성화 표시
|
||||
_models = _models.where((m) => m.id != id).toList();
|
||||
_applyFilters();
|
||||
await _groupModelsByVendor();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어 설정
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
_applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Vendor 필터 설정
|
||||
void setVendorFilter(int? vendorId) {
|
||||
_selectedVendorId = vendorId;
|
||||
_applyFilters();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 필터 적용
|
||||
void _applyFilters() {
|
||||
_filteredModels = _models.where((model) {
|
||||
// Vendor 필터
|
||||
if (_selectedVendorId != null && model.vendorsId != _selectedVendorId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 검색어 필터
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
return model.name.toLowerCase().contains(query);
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Vendor별 모델 그룹핑
|
||||
Future<void> _groupModelsByVendor() async {
|
||||
_modelsByVendor.clear();
|
||||
|
||||
for (final model in _models) {
|
||||
_modelsByVendor.putIfAbsent(model.vendorsId, () => []).add(model);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vendor ID로 Vendor 정보 가져오기
|
||||
VendorDto? getVendorById(int vendorId) {
|
||||
try {
|
||||
return _vendors.firstWhere((v) => v.id == vendorId);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 Vendor의 모델 목록 가져오기
|
||||
List<ModelDto> getModelsByVendor(int vendorId) {
|
||||
return _modelsByVendor[vendorId] ?? [];
|
||||
}
|
||||
|
||||
/// 에러 메시지 클리어
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 컨트롤러 리셋
|
||||
void reset() {
|
||||
_models.clear();
|
||||
_filteredModels.clear();
|
||||
_vendors.clear();
|
||||
_modelsByVendor.clear();
|
||||
_isLoading = false;
|
||||
_isSubmitting = false;
|
||||
_errorMessage = null;
|
||||
_searchQuery = '';
|
||||
_selectedVendorId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
reset();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
189
lib/screens/model/model_form_dialog.dart
Normal file
189
lib/screens/model/model_form_dialog.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
|
||||
class ModelFormDialog extends StatefulWidget {
|
||||
final ModelController controller;
|
||||
final ModelDto? model;
|
||||
|
||||
const ModelFormDialog({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.model,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ModelFormDialog> createState() => _ModelFormDialogState();
|
||||
}
|
||||
|
||||
class _ModelFormDialogState extends State<ModelFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameController;
|
||||
|
||||
int? _selectedVendorId;
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.model?.name);
|
||||
_selectedVendorId = widget.model?.vendorsId;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditMode = widget.model != null;
|
||||
|
||||
return ShadDialog(
|
||||
title: Text(isEditMode ? '모델 수정' : '새 모델 등록'),
|
||||
child: Container(
|
||||
width: 400,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Vendor 선택
|
||||
ShadSelect<int>(
|
||||
placeholder: const Text('제조사 선택'),
|
||||
options: widget.controller.vendors.map(
|
||||
(vendor) => ShadOption(
|
||||
value: vendor.id,
|
||||
child: Text(vendor.name),
|
||||
),
|
||||
).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
final vendor = widget.controller.vendors.firstWhere(
|
||||
(v) => v.id == value,
|
||||
);
|
||||
return Text(vendor.name);
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedVendorId = value;
|
||||
});
|
||||
},
|
||||
initialValue: _selectedVendorId,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 모델명 입력
|
||||
ShadInputFormField(
|
||||
controller: _nameController,
|
||||
label: const Text('모델명'),
|
||||
placeholder: const Text('모델명을 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return '모델명은 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 활성 상태는 백엔드에서 관리하므로 UI에서 제거
|
||||
|
||||
// 버튼들
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _isSubmitting ? null : () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton(
|
||||
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(isEditMode ? '수정' : '등록'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedVendorId == null) {
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('오류'),
|
||||
description: Text('제조사를 선택해주세요'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
bool success;
|
||||
if (widget.model != null) {
|
||||
// 수정
|
||||
success = await widget.controller.updateModel(
|
||||
id: widget.model!.id!,
|
||||
vendorsId: _selectedVendorId!,
|
||||
name: _nameController.text,
|
||||
);
|
||||
} else {
|
||||
// 생성
|
||||
success = await widget.controller.createModel(
|
||||
vendorsId: _selectedVendorId!,
|
||||
name: _nameController.text,
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('성공'),
|
||||
description: Text(
|
||||
widget.model != null
|
||||
? '모델이 수정되었습니다.'
|
||||
: '모델이 등록되었습니다.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('오류'),
|
||||
description: Text(widget.controller.errorMessage ?? '처리 실패'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
311
lib/screens/model/model_list_screen.dart
Normal file
311
lib/screens/model/model_list_screen.dart
Normal file
@@ -0,0 +1,311 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
import 'package:superport/screens/model/model_form_dialog.dart';
|
||||
import 'package:superport/injection_container.dart' as di;
|
||||
|
||||
class ModelListScreen extends StatefulWidget {
|
||||
const ModelListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ModelListScreen> createState() => _ModelListScreenState();
|
||||
}
|
||||
|
||||
class _ModelListScreenState extends State<ModelListScreen> {
|
||||
late final ModelController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = di.sl<ModelController>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.loadInitialData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<ModelController>(
|
||||
builder: (context, controller, _) {
|
||||
return ShadCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFilters(),
|
||||
const SizedBox(height: 16),
|
||||
if (controller.errorMessage != null) ...[
|
||||
ShadAlert(
|
||||
icon: const Icon(Icons.error),
|
||||
title: const Text('오류'),
|
||||
description: Text(controller.errorMessage!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildModelTable(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'모델 관리',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () => _showCreateDialog(),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.add, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('새 모델 등록'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters() {
|
||||
return Consumer<ModelController>(
|
||||
builder: (context, controller, _) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ShadInput(
|
||||
placeholder: const Text('모델명 검색...'),
|
||||
onChanged: controller.setSearchQuery,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ShadSelect<int?>(
|
||||
placeholder: const Text('제조사 선택'),
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('전체'),
|
||||
),
|
||||
...controller.vendors.map(
|
||||
(vendor) => ShadOption(
|
||||
value: vendor.id,
|
||||
child: Text(vendor.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return const Text('전체');
|
||||
}
|
||||
final vendor = controller.vendors.firstWhere((v) => v.id == value);
|
||||
return Text(vendor.name);
|
||||
},
|
||||
onChanged: controller.setVendorFilter,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ShadButton.outline(
|
||||
onPressed: controller.refreshModels,
|
||||
child: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelTable() {
|
||||
return Consumer<ModelController>(
|
||||
builder: (context, controller, _) {
|
||||
if (controller.models.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('등록된 모델이 없습니다.'),
|
||||
);
|
||||
}
|
||||
|
||||
return ShadTable(
|
||||
builder: (context, tableVicinity) {
|
||||
final row = tableVicinity.row;
|
||||
final column = tableVicinity.column;
|
||||
|
||||
// Header
|
||||
if (row == 0) {
|
||||
const headers = ['ID', '제조사', '모델명', '설명', '상태', '작업'];
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: Colors.grey.shade100,
|
||||
child: Text(
|
||||
headers[column],
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Data rows
|
||||
final modelIndex = row - 1;
|
||||
if (modelIndex < controller.models.length) {
|
||||
final model = controller.models[modelIndex];
|
||||
final vendor = controller.getVendorById(model.vendorsId);
|
||||
|
||||
switch (column) {
|
||||
case 0:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(model.id.toString()),
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(vendor?.name ?? 'Unknown'),
|
||||
),
|
||||
);
|
||||
case 2:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(model.name),
|
||||
),
|
||||
);
|
||||
case 3:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text('-'),
|
||||
),
|
||||
);
|
||||
case 4:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ShadBadge(
|
||||
backgroundColor: model.isActive
|
||||
? Colors.green.shade100
|
||||
: Colors.grey.shade200,
|
||||
child: Text(
|
||||
model.isActive ? '활성' : '비활성',
|
||||
style: TextStyle(
|
||||
color: model.isActive ? Colors.green.shade700 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
case 5:
|
||||
return ShadTableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEditDialog(model),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showDeleteConfirmation(model),
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return const ShadTableCell(child: SizedBox());
|
||||
}
|
||||
}
|
||||
return const ShadTableCell(child: SizedBox());
|
||||
},
|
||||
rowCount: controller.models.length + 1, // +1 for header
|
||||
columnCount: 6,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateDialog() {
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ModelFormDialog(
|
||||
controller: _controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditDialog(ModelDto model) {
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ModelFormDialog(
|
||||
controller: _controller,
|
||||
model: model,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(ModelDto model) {
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog(
|
||||
title: const Text('모델 삭제'),
|
||||
description: Text('${model.name} 모델을 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ShadButton.destructive(
|
||||
onPressed: () async {
|
||||
final success = await _controller.deleteModel(model.id!);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
if (success) {
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('성공'),
|
||||
description: Text('모델이 삭제되었습니다.'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('오류'),
|
||||
description: Text(_controller.errorMessage ?? '삭제 실패'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user