결재 템플릿 단계 적용 구현

- ApprovalTemplate 엔티티·DTO·원격 리포지토리 추가
- ApprovalController에 템플릿 로딩/적용 상태와 assignSteps 호출 연동
- ApprovalPage 단계 탭에 템플릿 선택 UI 및 적용 확인 다이얼로그 구현
- 템플릿 적용 단위 테스트와 IMPLEMENTATION_TASKS 현황 갱신
This commit is contained in:
JiWoong Sul
2025-09-25 00:21:12 +09:00
parent b6e50464d2
commit c3010965ad
63 changed files with 10179 additions and 1436 deletions

View File

@@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/core/constants/app_sections.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/filter_bar.dart';
import '../../../../../core/config/environment.dart';
import '../../../../../widgets/spec_page.dart';
import '../../domain/entities/warehouse.dart';
@@ -133,149 +137,122 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
? false
: (result.page * result.pageSize) < result.total;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
final showReset = _searchController.text.isNotEmpty ||
_controller.statusFilter != WarehouseStatusFilter.all;
return AppLayout(
title: '입고지(창고) 관리',
subtitle: '창고 코드, 주소, 사용여부를 관리합니다.',
breadcrumbs: const [
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
AppBreadcrumbItem(label: '마스터', path: '/masters/warehouses'),
AppBreadcrumbItem(label: '창고'),
],
actions: [
ShadButton(
leading: const Icon(LucideIcons.plus, size: 16),
onPressed: _controller.isSubmitting
? null
: () => _openWarehouseForm(context),
child: const Text('신규 등록'),
),
],
toolbar: FilterBar(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('입고지(창고) 관리', style: theme.textTheme.h2),
const SizedBox(height: 6),
Text(
'창고 코드, 주소, 사용여부를 관리합니다.',
style: theme.textTheme.muted,
),
],
),
),
const SizedBox(width: 16),
ShadButton(
leading: const Icon(LucideIcons.plus, size: 16),
onPressed: _controller.isSubmitting
? null
: () => _openWarehouseForm(context),
child: const Text('신규 등록'),
),
],
),
const SizedBox(height: 24),
ShadCard(
title: Text('검색 및 필터', style: theme.textTheme.h3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
runSpacing: 16,
children: [
SizedBox(
width: 260,
child: ShadInput(
controller: _searchController,
focusNode: _searchFocus,
placeholder: const Text('창고코드, 창고명 검색'),
leading: const Icon(LucideIcons.search, size: 16),
onSubmitted: (_) => _applyFilters(),
),
),
SizedBox(
width: 200,
child: ShadSelect<WarehouseStatusFilter>(
key: ValueKey(_controller.statusFilter),
initialValue: _controller.statusFilter,
selectedOptionBuilder: (context, filter) =>
Text(_statusLabel(filter)),
onChanged: (value) {
if (value == null) return;
_controller.updateStatusFilter(value);
},
options: WarehouseStatusFilter.values
.map(
(filter) => ShadOption(
value: filter,
child: Text(_statusLabel(filter)),
),
)
.toList(),
),
),
ShadButton.outline(
onPressed: _controller.isLoading
? null
: _applyFilters,
child: const Text('검색 적용'),
),
if (_searchController.text.isNotEmpty ||
_controller.statusFilter !=
WarehouseStatusFilter.all)
ShadButton.ghost(
onPressed: _controller.isLoading
? null
: () {
_searchController.clear();
_searchFocus.requestFocus();
_controller.updateQuery('');
_controller.updateStatusFilter(
WarehouseStatusFilter.all,
);
_controller.fetch(page: 1);
},
child: const Text('초기화'),
),
],
),
],
SizedBox(
width: 260,
child: ShadInput(
controller: _searchController,
focusNode: _searchFocus,
placeholder: const Text('창고코드, 창고명 검색'),
leading: const Icon(LucideIcons.search, size: 16),
onSubmitted: (_) => _applyFilters(),
),
),
const SizedBox(height: 24),
ShadCard(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('창고 목록', style: theme.textTheme.h3),
Text('$totalCount건', style: theme.textTheme.muted),
],
),
footer: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'페이지 $currentPage / $totalPages',
style: theme.textTheme.small,
),
Row(
children: [
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || currentPage <= 1
? null
: () => _controller.fetch(page: currentPage - 1),
child: const Text('이전'),
SizedBox(
width: 200,
child: ShadSelect<WarehouseStatusFilter>(
key: ValueKey(_controller.statusFilter),
initialValue: _controller.statusFilter,
selectedOptionBuilder: (context, filter) =>
Text(_statusLabel(filter)),
onChanged: (value) {
if (value == null) return;
_controller.updateStatusFilter(value);
},
options: WarehouseStatusFilter.values
.map(
(filter) => ShadOption(
value: filter,
child: Text(_statusLabel(filter)),
),
const SizedBox(width: 8),
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || !hasNext
? null
: () => _controller.fetch(page: currentPage + 1),
child: const Text('다음'),
),
],
),
],
),
child: _controller.isLoading
? const Padding(
padding: EdgeInsets.all(48),
child: Center(child: CircularProgressIndicator()),
)
: warehouses.isEmpty
.toList(),
),
),
ShadButton.outline(
onPressed: _controller.isLoading ? null : _applyFilters,
child: const Text('검색 적용'),
),
if (showReset)
ShadButton.ghost(
onPressed: _controller.isLoading
? null
: () {
_searchController.clear();
_searchFocus.requestFocus();
_controller.updateQuery('');
_controller.updateStatusFilter(
WarehouseStatusFilter.all,
);
_controller.fetch(page: 1);
},
child: const Text('초기화'),
),
],
),
child: ShadCard(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('창고 목록', style: theme.textTheme.h3),
Text('$totalCount건', style: theme.textTheme.muted),
],
),
footer: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'페이지 $currentPage / $totalPages',
style: theme.textTheme.small,
),
Row(
children: [
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || currentPage <= 1
? null
: () => _controller.fetch(page: currentPage - 1),
child: const Text('이전'),
),
const SizedBox(width: 8),
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: _controller.isLoading || !hasNext
? null
: () => _controller.fetch(page: currentPage + 1),
child: const Text('다음'),
),
],
),
],
),
child: _controller.isLoading
? const Padding(
padding: EdgeInsets.all(48),
child: Center(child: CircularProgressIndicator()),
)
: warehouses.isEmpty
? Padding(
padding: const EdgeInsets.all(32),
child: Text(
@@ -288,10 +265,8 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
dateFormat: _dateFormat,
onEdit: _controller.isSubmitting
? null
: (warehouse) => _openWarehouseForm(
context,
warehouse: warehouse,
),
: (warehouse) =>
_openWarehouseForm(context, warehouse: warehouse),
onDelete: _controller.isSubmitting
? null
: _confirmDelete,
@@ -299,8 +274,6 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
? null
: _restoreWarehouse,
),
),
],
),
);
},