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

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

@@ -211,19 +211,13 @@ class ApprovalStepActionInput {
/// 결재 단계를 일괄 등록/재배치하기 위한 입력 모델
class ApprovalStepAssignmentInput {
ApprovalStepAssignmentInput({
required this.approvalId,
required this.steps,
});
ApprovalStepAssignmentInput({required this.approvalId, required this.steps});
final int approvalId;
final List<ApprovalStepAssignmentItem> steps;
Map<String, dynamic> toPayload() {
return {
'id': approvalId,
'steps': steps.map((e) => e.toJson()).toList(),
};
return {'id': approvalId, 'steps': steps.map((e) => e.toJson()).toList()};
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart' as intl;
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
@@ -7,6 +8,8 @@ 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_date_picker.dart';
import '../../../../../widgets/components/superport_table.dart';
import '../../../../../widgets/spec_page.dart';
import '../../domain/entities/approval_history_record.dart';
import '../../domain/repositories/approval_history_repository.dart';
@@ -145,6 +148,19 @@ class _ApprovalHistoryEnabledPageState
),
],
toolbar: FilterBar(
actions: [
ShadButton.outline(
onPressed: _controller.isLoading ? null : _applyFilters,
child: const Text('검색 적용'),
),
ShadButton.ghost(
onPressed:
_controller.isLoading || !_controller.hasActiveFilters
? null
: _resetFilters,
child: const Text('필터 초기화'),
),
],
children: [
SizedBox(
width: 240,
@@ -180,21 +196,24 @@ class _ApprovalHistoryEnabledPageState
),
SizedBox(
width: 220,
child: ShadButton.outline(
onPressed: _pickDateRange,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(lucide.LucideIcons.calendar, size: 16),
const SizedBox(width: 8),
Text(
_dateRange == null
? '기간 선택'
: '${_formatDate(_dateRange!.start)} ~ ${_formatDate(_dateRange!.end)}',
child: SuperportDateRangePickerButton(
value: _dateRange,
dateFormat: intl.DateFormat('yyyy-MM-dd'),
enabled: !_controller.isLoading,
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 1),
initialDateRange:
_dateRange ??
DateTimeRange(
start: DateTime.now().subtract(const Duration(days: 7)),
end: DateTime.now(),
),
],
),
onChanged: (range) {
if (range == null) return;
setState(() => _dateRange = range);
_controller.updateDateRange(range.start, range.end);
_controller.fetch(page: 1);
},
),
),
if (_dateRange != null)
@@ -202,17 +221,6 @@ class _ApprovalHistoryEnabledPageState
onPressed: _controller.isLoading ? null : _clearDateRange,
child: const Text('기간 초기화'),
),
ShadButton.outline(
onPressed: _controller.isLoading ? null : _applyFilters,
child: const Text('검색 적용'),
),
ShadButton.ghost(
onPressed:
_controller.isLoading || !_controller.hasActiveFilters
? null
: _resetFilters,
child: const Text('필터 초기화'),
),
],
),
child: ShadCard(
@@ -283,27 +291,6 @@ class _ApprovalHistoryEnabledPageState
_controller.fetch(page: 1);
}
Future<void> _pickDateRange() async {
final now = DateTime.now();
final initial =
_dateRange ??
DateTimeRange(
start: DateTime(now.year, now.month, now.day - 7),
end: now,
);
final range = await showDateRangePicker(
context: context,
initialDateRange: initial,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 1),
);
if (range != null) {
setState(() => _dateRange = range);
_controller.updateDateRange(range.start, range.end);
_controller.fetch(page: 1);
}
}
void _clearDateRange() {
setState(() => _dateRange = null);
_controller.updateDateRange(null, null);
@@ -318,10 +305,6 @@ class _ApprovalHistoryEnabledPageState
_controller.fetch(page: 1);
}
String _formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date.toLocal());
}
String _actionLabel(ApprovalHistoryActionFilter filter) {
switch (filter) {
case ApprovalHistoryActionFilter.all:
@@ -349,58 +332,60 @@ class _ApprovalHistoryTable extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final normalizedQuery = query.trim().toLowerCase();
final header = [
'ID',
'결재번호',
'단계순서',
'승인자',
'행위',
'변경전 상태',
'변경 상태',
'작업일시',
'비고',
].map((label) => ShadTableCell.header(child: Text(label))).toList();
final columns = const [
Text('ID'),
Text('결재번호'),
Text('단계순서'),
Text('승인자'),
Text('행위'),
Text('변경 상태'),
Text('변경후 상태'),
Text('작업일시'),
Text('비고'),
];
final rows = histories.map((history) {
final isHighlighted =
normalizedQuery.isNotEmpty &&
history.approvalNo.toLowerCase().contains(normalizedQuery);
return [
ShadTableCell(child: Text(history.id.toString())),
ShadTableCell(
child: Text(
history.approvalNo,
style: isHighlighted
? ShadTheme.of(
context,
).textTheme.small.copyWith(fontWeight: FontWeight.w600)
: null,
),
),
ShadTableCell(
child: Text(
history.stepOrder == null ? '-' : history.stepOrder.toString(),
),
),
ShadTableCell(child: Text(history.approver.name)),
ShadTableCell(child: Text(history.action.name)),
ShadTableCell(child: Text(history.fromStatus?.name ?? '-')),
ShadTableCell(child: Text(history.toStatus.name)),
ShadTableCell(
child: Text(dateFormat.format(history.actionAt.toLocal())),
),
ShadTableCell(
child: Text(
history.note?.trim().isEmpty ?? true ? '-' : history.note!,
),
final highlightStyle = theme.textTheme.small.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.foreground,
);
final noteText = history.note?.trim();
final noteContent = noteText?.isNotEmpty == true ? noteText : null;
final subLabelStyle = theme.textTheme.muted.copyWith(
fontSize: (theme.textTheme.muted.fontSize ?? 14) - 1,
);
return <Widget>[
Text(history.id.toString()),
Text(history.approvalNo, style: isHighlighted ? highlightStyle : null),
Text(history.stepOrder == null ? '-' : history.stepOrder.toString()),
Text(history.approver.name),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(history.action.name),
if (noteContent != null) Text(noteContent, style: subLabelStyle),
],
),
Text(history.fromStatus?.name ?? '-'),
Text(history.toStatus.name),
Text(dateFormat.format(history.actionAt.toLocal())),
Text(noteContent ?? '-'),
];
}).toList();
return ShadTable.list(
header: header,
children: rows,
return SuperportTable(
columns: columns,
rows: rows,
rowHeight: 64,
maxHeight: 520,
columnSpanExtent: (index) {
switch (index) {
case 1:

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_dialog.dart';
import '../../../../../widgets/spec_page.dart';
import '../controllers/approval_step_controller.dart';
import '../../domain/entities/approval_step_input.dart';
@@ -528,73 +529,50 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
if (!mounted) return;
Navigator.of(context, rootNavigator: true).pop();
if (detail == null) return;
await showDialog<void>(
final step = detail.step;
await SuperportDialog.show<void>(
context: context,
builder: (dialogContext) {
final step = detail.step;
final theme = ShadTheme.of(dialogContext);
return Dialog(
insetPadding: const EdgeInsets.all(24),
clipBehavior: Clip.antiAlias,
child: ShadCard(
title: Text('결재 단계 상세', style: theme.textTheme.h3),
description: Text(
'결재번호 ${detail.approvalNo}',
style: theme.textTheme.muted,
dialog: SuperportDialog(
title: '결재 단계 상세',
description: '결재번호 ${detail.approvalNo}',
constraints: const BoxConstraints(maxWidth: 560),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 18,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_DetailRow(label: '단계 순서', value: '${step.stepOrder}'),
_DetailRow(label: '승인자', value: step.approver.name),
_DetailRow(label: '상태', value: step.status.name),
_DetailRow(label: '배정일시', value: _formatDate(step.assignedAt)),
_DetailRow(
label: '결정일시',
value: step.decidedAt == null
? '-'
: _formatDate(step.decidedAt!),
),
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ShadButton.ghost(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('닫기'),
),
],
_DetailRow(label: '템플릿', value: detail.templateName ?? '-'),
_DetailRow(label: '트랜잭션번호', value: detail.transactionNo ?? '-'),
const SizedBox(height: 12),
Text(
'비고',
style: ShadTheme.of(
context,
).textTheme.small.copyWith(fontWeight: FontWeight.w600),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_DetailRow(label: '단계 순서', value: '${step.stepOrder}'),
_DetailRow(label: '승인자', value: step.approver.name),
_DetailRow(label: '상태', value: step.status.name),
_DetailRow(
label: '배정일시',
value: _formatDate(step.assignedAt),
),
_DetailRow(
label: '결정일시',
value: step.decidedAt == null
? '-'
: _formatDate(step.decidedAt!),
),
_DetailRow(label: '템플릿', value: detail.templateName ?? '-'),
_DetailRow(
label: '트랜잭션번호',
value: detail.transactionNo ?? '-',
),
const SizedBox(height: 12),
Text(
'비고',
style: theme.textTheme.small.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
ShadTextarea(
initialValue: step.note ?? '',
readOnly: true,
minHeight: 80,
maxHeight: 200,
),
],
),
const SizedBox(height: 8),
ShadTextarea(
initialValue: step.note ?? '',
readOnly: true,
minHeight: 80,
maxHeight: 200,
),
),
);
},
],
),
),
);
}
@@ -724,102 +702,93 @@ class _StepFormDialogState extends State<_StepFormDialog> {
final theme = ShadTheme.of(context);
final materialTheme = Theme.of(context);
return Dialog(
insetPadding: const EdgeInsets.all(24),
clipBehavior: Clip.antiAlias,
child: ShadCard(
title: Text(widget.title, style: theme.textTheme.h3),
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
return SuperportDialog(
title: widget.title,
constraints: const BoxConstraints(maxWidth: 560),
primaryAction: ShadButton(
key: const ValueKey('step_form_submit'),
onPressed: _handleSubmit,
child: Text(widget.submitLabel),
),
secondaryAction: ShadButton.ghost(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ShadButton.ghost(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
const SizedBox(width: 12),
ShadButton(
key: const ValueKey('step_form_submit'),
onPressed: _handleSubmit,
child: Text(widget.submitLabel),
),
],
),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!widget.isEditing)
_FormFieldBlock(
label: '결재 ID',
errorText: _errors['approvalId'],
child: ShadInput(
key: const ValueKey('step_form_approval_id'),
controller: _approvalIdController,
onChanged: (_) => _clearError('approvalId'),
),
)
else ...[
_FormFieldBlock(
label: '결재 ID',
child: ShadInput(
controller: _approvalIdController,
readOnly: true,
),
),
const SizedBox(height: 16),
_FormFieldBlock(
label: '결재번호',
child: ShadInput(
controller: _approvalNoController,
readOnly: true,
),
),
],
if (!widget.isEditing) const SizedBox(height: 16),
if (!widget.isEditing)
_FormFieldBlock(
label: '단계 순서',
errorText: _errors['stepOrder'],
label: '결재 ID',
errorText: _errors['approvalId'],
child: ShadInput(
key: const ValueKey('step_form_step_order'),
controller: _stepOrderController,
onChanged: (_) => _clearError('stepOrder'),
key: const ValueKey('step_form_approval_id'),
controller: _approvalIdController,
onChanged: (_) => _clearError('approvalId'),
),
)
else ...[
_FormFieldBlock(
label: '결재 ID',
child: ShadInput(
controller: _approvalIdController,
readOnly: true,
),
),
const SizedBox(height: 16),
_FormFieldBlock(
label: '승인자 ID',
errorText: _errors['approverId'],
label: '결재번호',
child: ShadInput(
key: const ValueKey('step_form_approver_id'),
controller: _approverIdController,
onChanged: (_) => _clearError('approverId'),
controller: _approvalNoController,
readOnly: true,
),
),
const SizedBox(height: 16),
_FormFieldBlock(
label: '비고',
helperText: '필요 시 단계에 대한 참고 내용을 남길 수 있습니다.',
child: ShadTextarea(
key: const ValueKey('step_form_note'),
controller: _noteController,
minHeight: 100,
maxHeight: 200,
),
),
if (_errors['form'] != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
_errors['form']!,
style: theme.textTheme.small.copyWith(
color: materialTheme.colorScheme.error,
),
),
),
],
),
if (!widget.isEditing) const SizedBox(height: 16),
_FormFieldBlock(
label: '단계 순서',
errorText: _errors['stepOrder'],
child: ShadInput(
key: const ValueKey('step_form_step_order'),
controller: _stepOrderController,
onChanged: (_) => _clearError('stepOrder'),
),
),
const SizedBox(height: 16),
_FormFieldBlock(
label: '승인자 ID',
errorText: _errors['approverId'],
child: ShadInput(
key: const ValueKey('step_form_approver_id'),
controller: _approverIdController,
onChanged: (_) => _clearError('approverId'),
),
),
const SizedBox(height: 16),
_FormFieldBlock(
label: '비고',
helperText: '필요 시 단계에 대한 참고 내용을 남길 수 있습니다.',
child: ShadTextarea(
key: const ValueKey('step_form_note'),
controller: _noteController,
minHeight: 100,
maxHeight: 200,
),
),
if (_errors['form'] != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
_errors['form']!,
style: theme.textTheme.small.copyWith(
color: materialTheme.colorScheme.error,
),
),
),
],
),
),
);

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 ? '수정 완료' : '생성 완료'),
),
],