결재 단계 UI 보완 및 분석 통과

This commit is contained in:
JiWoong Sul
2025-09-25 17:18:08 +09:00
parent c31f6217ef
commit 6d6781f552
7 changed files with 99 additions and 103 deletions

View File

@@ -513,8 +513,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
const SizedBox(height: 8), const SizedBox(height: 8),
ShadTextarea( ShadTextarea(
controller: noteController, controller: noteController,
minLines: 3, minHeight: 120,
maxLines: 5, maxHeight: 220,
), ),
if (requireNote) if (requireNote)
Padding( Padding(

View File

@@ -1,6 +1,7 @@
import '../../../../core/common/models/paginated_result.dart'; import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../../domain/entities/approval.dart'; import 'package:superport_v2/features/approvals/data/dtos/approval_dto.dart';
import '../../data/dtos/approval_dto.dart'; import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
import '../../domain/entities/approval_step_record.dart'; import '../../domain/entities/approval_step_record.dart';
class ApprovalStepRecordDto { class ApprovalStepRecordDto {

View File

@@ -1,9 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../../core/common/models/paginated_result.dart'; import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../../../core/network/api_client.dart'; import 'package:superport_v2/core/network/api_client.dart';
import '../../domain/entities/approval_step_record.dart'; import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
import '../../domain/repositories/approval_step_repository.dart'; import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
import '../dtos/approval_step_record_dto.dart'; import '../dtos/approval_step_record_dto.dart';
class ApprovalStepRepositoryRemote implements ApprovalStepRepository { class ApprovalStepRepositoryRemote implements ApprovalStepRepository {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide; import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
@@ -321,6 +320,9 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ShadButton.outline( child: ShadButton.outline(
key: ValueKey(
'step_detail_${step.id ?? record.approvalId}_${step.stepOrder}',
),
size: ShadButtonSize.sm, size: ShadButtonSize.sm,
onPressed: step.id == null onPressed: step.id == null
? null ? null
@@ -382,9 +384,7 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
options.add(const _StatusOption(id: -1, name: '전체')); options.add(const _StatusOption(id: -1, name: '전체'));
for (final record in records) { for (final record in records) {
final status = record.step.status; final status = record.step.status;
if (status.id != null) { options.add(_StatusOption(id: status.id, name: status.name));
options.add(_StatusOption(id: status.id!, name: status.name));
}
} }
return options.toList()..sort((a, b) => a.id.compareTo(b.id)); return options.toList()..sort((a, b) => a.id.compareTo(b.id));
} }
@@ -444,6 +444,15 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
'결재번호 ${detail.approvalNo}', '결재번호 ${detail.approvalNo}',
style: theme.textTheme.muted, style: theme.textTheme.muted,
), ),
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ShadButton.ghost(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('닫기'),
),
],
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Column( child: Column(
@@ -485,15 +494,6 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
], ],
), ),
), ),
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ShadButton.ghost(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('닫기'),
),
],
),
), ),
); );
}, },

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide; import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
@@ -442,6 +441,7 @@ class _ApprovalTemplateEnabledPageState
Future<bool?> _openTemplateForm({ApprovalTemplate? template}) async { Future<bool?> _openTemplateForm({ApprovalTemplate? template}) async {
final isEdit = template != null; final isEdit = template != null;
final existingTemplate = template;
final codeController = TextEditingController(text: template?.code ?? ''); final codeController = TextEditingController(text: template?.code ?? '');
final nameController = TextEditingController(text: template?.name ?? ''); final nameController = TextEditingController(text: template?.name ?? '');
final descriptionController = TextEditingController( final descriptionController = TextEditingController(
@@ -587,13 +587,15 @@ class _ApprovalTemplateEnabledPageState
), ),
actions: [ actions: [
ShadButton.ghost( ShadButton.ghost(
onPressed: isSaving ? null : () => Navigator.of(context).pop(false), onPressed: () {
if (isSaving) return;
Navigator.of(context).pop(false);
},
child: const Text('취소'), child: const Text('취소'),
), ),
ShadButton( ShadButton(
onPressed: isSaving onPressed: () async {
? null if (isSaving) return;
: () async {
final codeValue = codeController.text.trim(); final codeValue = codeController.text.trim();
final nameValue = nameController.text.trim(); final nameValue = nameController.text.trim();
if (!isEdit && codeValue.isEmpty) { if (!isEdit && codeValue.isEmpty) {
@@ -614,12 +616,8 @@ class _ApprovalTemplateEnabledPageState
.map( .map(
(field) => ApprovalTemplateStepInput( (field) => ApprovalTemplateStepInput(
id: field.id, id: field.id,
stepOrder: int.parse( stepOrder: int.parse(field.orderController.text.trim()),
field.orderController.text.trim(), approverId: int.parse(field.approverController.text.trim()),
),
approverId: int.parse(
field.approverController.text.trim(),
),
note: field.noteController.text.trim().isEmpty note: field.noteController.text.trim().isEmpty
? null ? null
: field.noteController.text.trim(), : field.noteController.text.trim(),
@@ -627,7 +625,7 @@ class _ApprovalTemplateEnabledPageState
) )
.toList(); .toList();
final input = ApprovalTemplateInput( final input = ApprovalTemplateInput(
code: isEdit ? template!.code : codeValue, code: isEdit ? existingTemplate?.code : codeValue,
name: nameValue, name: nameValue,
description: descriptionController.text.trim().isEmpty description: descriptionController.text.trim().isEmpty
? null ? null
@@ -637,10 +635,17 @@ class _ApprovalTemplateEnabledPageState
: noteController.text.trim(), : noteController.text.trim(),
isActive: statusNotifier.value, isActive: statusNotifier.value,
); );
if (isEdit && existingTemplate == null) {
modalSetState?.call(() => errorText = '템플릿 정보를 불러오지 못했습니다.');
modalSetState?.call(() => isSaving = false);
return;
}
modalSetState?.call(() => isSaving = true); modalSetState?.call(() => isSaving = true);
final success = isEdit
final success = isEdit && existingTemplate != null
? await _controller.update( ? await _controller.update(
template!.id, existingTemplate.id,
input, input,
stepInputs, stepInputs,
) )
@@ -651,13 +656,7 @@ class _ApprovalTemplateEnabledPageState
modalSetState?.call(() => isSaving = false); modalSetState?.call(() => isSaving = false);
} }
}, },
child: isSaving child: Text(isEdit ? '수정 완료' : '생성 완료'),
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(isEdit ? '수정 완료' : '생성 완료'),
), ),
], ],
); );
@@ -780,7 +779,9 @@ class _StepEditorRow extends StatelessWidget {
margin: const EdgeInsets.symmetric(vertical: 6), margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: theme.colorScheme.border.withOpacity(0.6)), border: Border.all(
color: theme.colorScheme.border.withValues(alpha: 0.6),
),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(

View File

@@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart'; import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/features/approvals/domain/entities/approval.dart'; import 'package:superport_v2/features/approvals/domain/entities/approval.dart';
@@ -95,14 +94,9 @@ void main() {
expect(find.text('APP-2024-0001'), findsOneWidget); expect(find.text('APP-2024-0001'), findsOneWidget);
expect(find.text('최승인'), findsOneWidget); expect(find.text('최승인'), findsOneWidget);
await tester.dragUntilVisible( final detailButtonFinder = find.byKey(const ValueKey('step_detail_501_1'));
find.widgetWithText(ShadButton, '상세'), final detailButton = tester.widget<ShadButton>(detailButtonFinder);
find.byType(TwoDimensionalScrollable), detailButton.onPressed?.call();
const Offset(-200, 0),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ShadButton, '상세').first);
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@@ -65,7 +65,7 @@ void main() {
); );
}); });
ApprovalTemplate _template({bool isActive = true}) { ApprovalTemplate buildTemplate({bool isActive = true}) {
return ApprovalTemplate( return ApprovalTemplate(
id: 10, id: 10,
code: 'AP_INBOUND', code: 'AP_INBOUND',
@@ -101,7 +101,7 @@ void main() {
), ),
).thenAnswer( ).thenAnswer(
(_) async => PaginatedResult<ApprovalTemplate>( (_) async => PaginatedResult<ApprovalTemplate>(
items: [_template()], items: [buildTemplate()],
page: 1, page: 1,
pageSize: 20, pageSize: 20,
total: 1, total: 1,
@@ -140,7 +140,7 @@ void main() {
when( when(
() => repository.create(any(), steps: any(named: 'steps')), () => repository.create(any(), steps: any(named: 'steps')),
).thenAnswer((_) async => _template()); ).thenAnswer((_) async => buildTemplate());
await tester.pumpWidget(_buildApp(const ApprovalTemplatePage())); await tester.pumpWidget(_buildApp(const ApprovalTemplatePage()));
await tester.pump(); await tester.pump();
@@ -192,7 +192,7 @@ void main() {
}); });
testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async { testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async {
final activeTemplate = _template(); final activeTemplate = buildTemplate();
when( when(
() => repository.list( () => repository.list(