전역 구조 리팩터링 및 테스트 확장

This commit is contained in:
JiWoong Sul
2025-09-29 01:51:47 +09:00
parent c00c0c9ab2
commit fef7108479
70 changed files with 7709 additions and 3185 deletions

View File

@@ -7,6 +7,7 @@ import '../../../../../core/config/environment.dart';
import '../../../../../core/constants/app_sections.dart';
import '../../../../../widgets/app_layout.dart';
import '../../../../../widgets/components/filter_bar.dart';
import '../../../../../widgets/components/superport_table.dart';
import '../../../../../widgets/components/superport_dialog.dart';
import '../../../../../widgets/spec_page.dart';
import '../../../domain/entities/approval_template.dart';
@@ -151,6 +152,18 @@ class _ApprovalTemplateEnabledPageState
),
],
toolbar: FilterBar(
actions: [
ShadButton.outline(
onPressed: _controller.isLoading ? null : _applyFilters,
child: const Text('검색 적용'),
),
ShadButton.ghost(
onPressed: !_controller.isLoading && showReset
? _resetFilters
: null,
child: const Text('필터 초기화'),
),
],
children: [
SizedBox(
width: 260,
@@ -183,16 +196,6 @@ class _ApprovalTemplateEnabledPageState
.toList(),
),
),
ShadButton.outline(
onPressed: _controller.isLoading ? null : _applyFilters,
child: const Text('검색 적용'),
),
ShadButton.ghost(
onPressed: !_controller.isLoading && showReset
? _resetFilters
: null,
child: const Text('필터 초기화'),
),
],
),
child: ShadCard(
@@ -213,97 +216,95 @@ class _ApprovalTemplateEnabledPageState
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 480,
child: ShadTable.list(
header:
['ID', '템플릿코드', '템플릿명', '설명', '사용', '변경일시', '동작']
.map(
(e) => ShadTableCell.header(child: Text(e)),
)
.toList(),
columnSpanExtent: (index) {
switch (index) {
case 2:
return const FixedTableSpanExtent(220);
case 3:
return const FixedTableSpanExtent(260);
case 4:
return const FixedTableSpanExtent(100);
case 5:
return const FixedTableSpanExtent(180);
case 6:
return const FixedTableSpanExtent(160);
default:
return const FixedTableSpanExtent(140);
}
},
children: templates.map((template) {
return [
ShadTableCell(child: Text('${template.id}')),
ShadTableCell(child: Text(template.code)),
ShadTableCell(child: Text(template.name)),
ShadTableCell(
child: Text(
template.description?.isNotEmpty == true
? template.description!
: '-',
),
SuperportTable.fromCells(
header: const [
ShadTableCell.header(child: Text('ID')),
ShadTableCell.header(child: Text('템플릿코드')),
ShadTableCell.header(child: Text('템플릿명')),
ShadTableCell.header(child: Text('설명')),
ShadTableCell.header(child: Text('사용')),
ShadTableCell.header(child: Text('변경일시')),
ShadTableCell.header(child: Text('동작')),
],
rows: templates.map((template) {
return [
ShadTableCell(child: Text('${template.id}')),
ShadTableCell(child: Text(template.code)),
ShadTableCell(child: Text(template.name)),
ShadTableCell(
child: Text(
template.description?.isNotEmpty == true
? template.description!
: '-',
),
ShadTableCell(
child: template.isActive
? const ShadBadge(child: Text('사용'))
: const ShadBadge.outline(
child: Text('미사용'),
),
ShadTableCell(
child: template.isActive
? const ShadBadge(child: Text('사용'))
: const ShadBadge.outline(child: Text('미사용')),
),
ShadTableCell(
child: Text(
template.updatedAt == null
? '-'
: _dateFormat.format(
template.updatedAt!.toLocal(),
),
),
ShadTableCell(
child: Text(
template.updatedAt == null
? '-'
: _dateFormat.format(
template.updatedAt!.toLocal(),
),
),
),
ShadTableCell(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ShadButton.ghost(
key: ValueKey(
'template_edit_${template.id}',
),
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () => _openEditTemplate(template),
child: const Text('수정'),
),
ShadTableCell(
alignment: Alignment.centerRight,
child: Wrap(
spacing: 8,
children: [
ShadButton.ghost(
key: ValueKey(
'template_edit_${template.id}',
),
const SizedBox(width: 8),
template.isActive
? ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () =>
_confirmDelete(template),
child: const Text('삭제'),
)
: ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () =>
_confirmRestore(template),
child: const Text('복구'),
),
],
),
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () => _openEditTemplate(template),
child: const Text('수정'),
),
template.isActive
? ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () => _confirmDelete(template),
child: const Text('삭제'),
)
: ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isSubmitting
? null
: () => _confirmRestore(template),
child: const Text('복구'),
),
],
),
];
}).toList(),
),
),
];
}).toList(),
rowHeight: 56,
maxHeight: 480,
columnSpanExtent: (index) {
switch (index) {
case 2:
return const FixedTableSpanExtent(220);
case 3:
return const FixedTableSpanExtent(260);
case 4:
return const FixedTableSpanExtent(100);
case 5:
return const FixedTableSpanExtent(180);
case 6:
return const FixedTableSpanExtent(160);
default:
return const FixedTableSpanExtent(140);
}
},
),
const SizedBox(height: 16),
Row(
@@ -382,26 +383,23 @@ class _ApprovalTemplateEnabledPageState
}
Future<void> _confirmDelete(ApprovalTemplate template) async {
final confirmed = await showDialog<bool>(
final confirmed = await SuperportDialog.show<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('템플릿 삭제'),
content: Text(
dialog: SuperportDialog(
title: '템플릿 삭제',
description:
'"${template.name}" 템플릿을 삭제하시겠습니까?\n삭제 시 템플릿은 미사용 상태로 전환됩니다.',
actions: [
ShadButton.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('취소'),
),
FilledButton.tonal(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('삭제'),
),
],
);
},
ShadButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('삭제'),
),
],
),
);
if (confirmed != true) return;
final ok = await _controller.delete(template.id);
@@ -412,24 +410,22 @@ class _ApprovalTemplateEnabledPageState
}
Future<void> _confirmRestore(ApprovalTemplate template) async {
final confirmed = await showDialog<bool>(
final confirmed = await SuperportDialog.show<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('템플릿 복구'),
content: Text('"${template.name}" 템플릿을 복구하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('취소'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('복구'),
),
],
);
},
dialog: SuperportDialog(
title: '템플릿 복구',
description: '"${template.name}" 템플릿 복구하시겠습니까?',
actions: [
ShadButton.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
ShadButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('복구'),
),
],
),
);
if (confirmed != true) return;
final restored = await _controller.restore(template.id);
@@ -454,10 +450,74 @@ class _ApprovalTemplateEnabledPageState
String? errorText;
StateSetter? modalSetState;
Future<void> handleSubmit() async {
if (isSaving) return;
final codeValue = codeController.text.trim();
final nameValue = nameController.text.trim();
if (!isEdit && codeValue.isEmpty) {
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
return;
}
if (nameValue.isEmpty) {
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
return;
}
final validation = _validateSteps(steps);
if (validation != null) {
modalSetState?.call(() => errorText = validation);
return;
}
modalSetState?.call(() => errorText = null);
final stepInputs = steps
.map(
(field) => ApprovalTemplateStepInput(
id: field.id,
stepOrder: int.parse(field.orderController.text.trim()),
approverId: int.parse(field.approverController.text.trim()),
note: field.noteController.text.trim().isEmpty
? null
: field.noteController.text.trim(),
),
)
.toList();
final input = ApprovalTemplateInput(
code: isEdit ? existingTemplate?.code : codeValue,
name: nameValue,
description: descriptionController.text.trim().isEmpty
? null
: descriptionController.text.trim(),
note: noteController.text.trim().isEmpty
? null
: noteController.text.trim(),
isActive: statusNotifier.value,
);
if (isEdit && existingTemplate == null) {
modalSetState?.call(() => errorText = '템플릿 정보를 불러오지 못했습니다.');
modalSetState?.call(() => isSaving = false);
return;
}
modalSetState?.call(() => isSaving = true);
final success = isEdit && existingTemplate != null
? await _controller.update(
existingTemplate.id,
input,
stepInputs,
)
: await _controller.create(input, stepInputs);
if (success != null && mounted) {
Navigator.of(context).pop(true);
} else {
modalSetState?.call(() => isSaving = false);
}
}
final result = await showSuperportDialog<bool>(
context: context,
title: isEdit ? '템플릿 수정' : '템플릿 생성',
barrierDismissible: !isSaving,
onSubmit: handleSubmit,
body: StatefulBuilder(
builder: (dialogContext, setModalState) {
modalSetState = setModalState;
@@ -594,68 +654,7 @@ class _ApprovalTemplateEnabledPageState
child: const Text('취소'),
),
ShadButton(
onPressed: () async {
if (isSaving) return;
final codeValue = codeController.text.trim();
final nameValue = nameController.text.trim();
if (!isEdit && codeValue.isEmpty) {
modalSetState?.call(() => errorText = '템플릿 코드를 입력하세요.');
return;
}
if (nameValue.isEmpty) {
modalSetState?.call(() => errorText = '템플릿명을 입력하세요.');
return;
}
final validation = _validateSteps(steps);
if (validation != null) {
modalSetState?.call(() => errorText = validation);
return;
}
modalSetState?.call(() => errorText = null);
final stepInputs = steps
.map(
(field) => ApprovalTemplateStepInput(
id: field.id,
stepOrder: int.parse(field.orderController.text.trim()),
approverId: int.parse(field.approverController.text.trim()),
note: field.noteController.text.trim().isEmpty
? null
: field.noteController.text.trim(),
),
)
.toList();
final input = ApprovalTemplateInput(
code: isEdit ? existingTemplate?.code : codeValue,
name: nameValue,
description: descriptionController.text.trim().isEmpty
? null
: descriptionController.text.trim(),
note: noteController.text.trim().isEmpty
? null
: noteController.text.trim(),
isActive: statusNotifier.value,
);
if (isEdit && existingTemplate == null) {
modalSetState?.call(() => errorText = '템플릿 정보를 불러오지 못했습니다.');
modalSetState?.call(() => isSaving = false);
return;
}
modalSetState?.call(() => isSaving = true);
final success = isEdit && existingTemplate != null
? await _controller.update(
existingTemplate.id,
input,
stepInputs,
)
: await _controller.create(input, stepInputs);
if (success != null && mounted) {
Navigator.of(context).pop(true);
} else {
modalSetState?.call(() => isSaving = false);
}
},
onPressed: handleSubmit,
child: Text(isEdit ? '수정 완료' : '생성 완료'),
),
],