From 6d6781f55281d81685c0d3be8819420a4a3d4dd8 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 25 Sep 2025 17:18:08 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=B0=EC=9E=AC=20=EB=8B=A8=EA=B3=84=20UI=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84=20=EB=B0=8F=20=EB=B6=84=EC=84=9D=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/pages/approval_page.dart | 4 +- .../data/dtos/approval_step_record_dto.dart | 7 +- .../approval_step_repository_remote.dart | 8 +- .../pages/approval_step_page.dart | 26 ++-- .../pages/approval_template_page.dart | 137 +++++++++--------- .../pages/approval_step_page_test.dart | 12 +- .../pages/approval_template_page_test.dart | 8 +- 7 files changed, 99 insertions(+), 103 deletions(-) diff --git a/lib/features/approvals/presentation/pages/approval_page.dart b/lib/features/approvals/presentation/pages/approval_page.dart index 11a3a29..66d8bdb 100644 --- a/lib/features/approvals/presentation/pages/approval_page.dart +++ b/lib/features/approvals/presentation/pages/approval_page.dart @@ -513,8 +513,8 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> { const SizedBox(height: 8), ShadTextarea( controller: noteController, - minLines: 3, - maxLines: 5, + minHeight: 120, + maxHeight: 220, ), if (requireNote) Padding( diff --git a/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart b/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart index f0d43e3..b9213a8 100644 --- a/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart +++ b/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart @@ -1,6 +1,7 @@ -import '../../../../core/common/models/paginated_result.dart'; -import '../../../domain/entities/approval.dart'; -import '../../data/dtos/approval_dto.dart'; +import 'package:superport_v2/core/common/models/paginated_result.dart'; +import 'package:superport_v2/features/approvals/data/dtos/approval_dto.dart'; +import 'package:superport_v2/features/approvals/domain/entities/approval.dart'; + import '../../domain/entities/approval_step_record.dart'; class ApprovalStepRecordDto { diff --git a/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart b/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart index b10bd08..dfc921e 100644 --- a/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart +++ b/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart @@ -1,9 +1,9 @@ import 'package:dio/dio.dart'; -import '../../../../core/common/models/paginated_result.dart'; -import '../../../../core/network/api_client.dart'; -import '../../domain/entities/approval_step_record.dart'; -import '../../domain/repositories/approval_step_repository.dart'; +import 'package:superport_v2/core/common/models/paginated_result.dart'; +import 'package:superport_v2/core/network/api_client.dart'; +import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart'; +import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart'; import '../dtos/approval_step_record_dto.dart'; class ApprovalStepRepositoryRemote implements ApprovalStepRepository { diff --git a/lib/features/approvals/step/presentation/pages/approval_step_page.dart b/lib/features/approvals/step/presentation/pages/approval_step_page.dart index 2e75591..85edf74 100644 --- a/lib/features/approvals/step/presentation/pages/approval_step_page.dart +++ b/lib/features/approvals/step/presentation/pages/approval_step_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.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:shadcn_ui/shadcn_ui.dart'; @@ -321,6 +320,9 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> { child: Align( alignment: Alignment.centerRight, child: ShadButton.outline( + key: ValueKey( + 'step_detail_${step.id ?? record.approvalId}_${step.stepOrder}', + ), size: ShadButtonSize.sm, onPressed: step.id == null ? null @@ -382,9 +384,7 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> { options.add(const _StatusOption(id: -1, name: '전체')); for (final record in records) { 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)); } @@ -444,6 +444,15 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> { '결재번호 ${detail.approvalNo}', style: theme.textTheme.muted, ), + footer: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ShadButton.ghost( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('닫기'), + ), + ], + ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), 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('닫기'), - ), - ], - ), ), ); }, diff --git a/lib/features/approvals/template/presentation/pages/approval_template_page.dart b/lib/features/approvals/template/presentation/pages/approval_template_page.dart index 15bc610..319b00d 100644 --- a/lib/features/approvals/template/presentation/pages/approval_template_page.dart +++ b/lib/features/approvals/template/presentation/pages/approval_template_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.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:shadcn_ui/shadcn_ui.dart'; @@ -442,6 +441,7 @@ class _ApprovalTemplateEnabledPageState Future _openTemplateForm({ApprovalTemplate? template}) async { final isEdit = template != null; + final existingTemplate = template; final codeController = TextEditingController(text: template?.code ?? ''); final nameController = TextEditingController(text: template?.name ?? ''); final descriptionController = TextEditingController( @@ -587,77 +587,76 @@ class _ApprovalTemplateEnabledPageState ), actions: [ ShadButton.ghost( - onPressed: isSaving ? null : () => Navigator.of(context).pop(false), + onPressed: () { + if (isSaving) return; + Navigator.of(context).pop(false); + }, child: const Text('취소'), ), ShadButton( - onPressed: isSaving - ? null - : () async { - 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 ? template!.code : codeValue, - name: nameValue, - description: descriptionController.text.trim().isEmpty + 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 - : descriptionController.text.trim(), - note: noteController.text.trim().isEmpty - ? null - : noteController.text.trim(), - isActive: statusNotifier.value, - ); - modalSetState?.call(() => isSaving = true); - final success = isEdit - ? await _controller.update( - template!.id, - input, - stepInputs, - ) - : await _controller.create(input, stepInputs); - if (success != null && mounted) { - Navigator.of(context).pop(true); - } else { - modalSetState?.call(() => isSaving = false); - } - }, - child: isSaving - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + : field.noteController.text.trim(), + ), ) - : Text(isEdit ? '수정 완료' : '생성 완료'), + .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); + } + }, + child: Text(isEdit ? '수정 완료' : '생성 완료'), ), ], ); @@ -780,7 +779,9 @@ class _StepEditorRow extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(12), 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), ), child: Column( diff --git a/test/features/approvals/step/presentation/pages/approval_step_page_test.dart b/test/features/approvals/step/presentation/pages/approval_step_page_test.dart index 629a904..4965773 100644 --- a/test/features/approvals/step/presentation/pages/approval_step_page_test.dart +++ b/test/features/approvals/step/presentation/pages/approval_step_page_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mocktail/mocktail.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/features/approvals/domain/entities/approval.dart'; @@ -95,14 +94,9 @@ void main() { expect(find.text('APP-2024-0001'), findsOneWidget); expect(find.text('최승인'), findsOneWidget); - await tester.dragUntilVisible( - find.widgetWithText(ShadButton, '상세'), - find.byType(TwoDimensionalScrollable), - const Offset(-200, 0), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.widgetWithText(ShadButton, '상세').first); + final detailButtonFinder = find.byKey(const ValueKey('step_detail_501_1')); + final detailButton = tester.widget(detailButtonFinder); + detailButton.onPressed?.call(); await tester.pump(); await tester.pumpAndSettle(); diff --git a/test/features/approvals/template/presentation/pages/approval_template_page_test.dart b/test/features/approvals/template/presentation/pages/approval_template_page_test.dart index 73548d1..af60ab7 100644 --- a/test/features/approvals/template/presentation/pages/approval_template_page_test.dart +++ b/test/features/approvals/template/presentation/pages/approval_template_page_test.dart @@ -65,7 +65,7 @@ void main() { ); }); - ApprovalTemplate _template({bool isActive = true}) { + ApprovalTemplate buildTemplate({bool isActive = true}) { return ApprovalTemplate( id: 10, code: 'AP_INBOUND', @@ -101,7 +101,7 @@ void main() { ), ).thenAnswer( (_) async => PaginatedResult( - items: [_template()], + items: [buildTemplate()], page: 1, pageSize: 20, total: 1, @@ -140,7 +140,7 @@ void main() { when( () => repository.create(any(), steps: any(named: 'steps')), - ).thenAnswer((_) async => _template()); + ).thenAnswer((_) async => buildTemplate()); await tester.pumpWidget(_buildApp(const ApprovalTemplatePage())); await tester.pump(); @@ -192,7 +192,7 @@ void main() { }); testWidgets('수정 플로우에서 fetchDetail 후 update를 호출한다', (tester) async { - final activeTemplate = _template(); + final activeTemplate = buildTemplate(); when( () => repository.list(