feat(approvals): Approval Flow v2 프런트엔드 전면 개편
- 환경/라우터 모듈에 approval_flow_v2 토글을 추가하고 FeatureFlags 초기화를 연결 (.env*, lib/core/**) - ApiClient 빌더·ApiRoutes 확장과 ApprovalRepositoryRemote 리팩터링으로 include·액션 시그니처를 정합화 - ApprovalFlow·ApprovalDraft 엔티티/레포/유즈케이스를 도입해 서버 초안과 단계 액션(승인·회수·재상신)을 지원 - Approval 컨트롤러·히스토리·템플릿 페이지와 공유 위젯을 재작성해 감사 로그·회수 UX·템플릿 CRUD를 반영 - Inbound/Outbound/Rental 컨트롤러·페이지에 결재 섹션을 삽입하고 대시보드 pending 카드 요약을 갱신 - SuperportDialog·FormField 등 공통 위젯을 보강하고 승인 위젯 가이드를 추가해 UI 가이드를 정리 - 결재/재고 테스트 픽스처와 단위·위젯·통합 테스트를 확장하고 flutter_test_config로 스테이징 호스트를 허용 - Approval Flow 레포트/플랜 문서를 업데이트하고 ApprovalFlow_System_Integration_and_ChangePlan.md를 추가 - 실행: flutter analyze, flutter test
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:superport_v2/features/approvals/request/presentation/controllers/approval_request_controller.dart';
|
||||
|
||||
void main() {
|
||||
ApprovalRequestParticipant buildParticipant(int id) {
|
||||
return ApprovalRequestParticipant(
|
||||
id: id,
|
||||
name: '사용자$id',
|
||||
employeeNo: 'EMP$id',
|
||||
);
|
||||
}
|
||||
|
||||
group('ApprovalRequestController', () {
|
||||
late ApprovalRequestController controller;
|
||||
|
||||
setUp(() {
|
||||
controller = ApprovalRequestController();
|
||||
});
|
||||
|
||||
test('addStep 성공 시 최종 승인자로 바인딩된다', () {
|
||||
final approver = buildParticipant(10);
|
||||
|
||||
final added = controller.addStep(approver: approver, note: '검토 필요');
|
||||
|
||||
expect(added, isTrue);
|
||||
expect(controller.steps, hasLength(1));
|
||||
expect(controller.steps.first.stepOrder, 1);
|
||||
expect(controller.finalApprover?.id, approver.id);
|
||||
expect(controller.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('최대 단계 수를 초과하면 추가를 거부한다', () {
|
||||
for (var i = 0; i < controller.maxSteps; i++) {
|
||||
final success = controller.addStep(approver: buildParticipant(i + 1));
|
||||
expect(success, isTrue);
|
||||
}
|
||||
|
||||
final result = controller.addStep(approver: buildParticipant(999));
|
||||
|
||||
expect(result, isFalse);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.steps, hasLength(controller.maxSteps));
|
||||
});
|
||||
|
||||
test('동일한 승인자를 중복 추가할 수 없다', () {
|
||||
final approver = buildParticipant(3);
|
||||
controller.addStep(approver: approver);
|
||||
|
||||
final duplicated = controller.addStep(approver: approver);
|
||||
|
||||
expect(duplicated, isFalse);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.steps, hasLength(1));
|
||||
});
|
||||
|
||||
test('상신자는 승인자로 추가할 수 없다', () {
|
||||
final requester = buildParticipant(7);
|
||||
controller.setRequester(requester);
|
||||
|
||||
final added = controller.addStep(approver: requester);
|
||||
|
||||
expect(added, isFalse);
|
||||
expect(controller.steps, isEmpty);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
|
||||
test('updateStep에서 승인자 중복을 감지한다', () {
|
||||
controller
|
||||
..addStep(approver: buildParticipant(1))
|
||||
..addStep(approver: buildParticipant(2));
|
||||
|
||||
final updated = controller.updateStep(1, approver: buildParticipant(1));
|
||||
|
||||
expect(updated, isFalse);
|
||||
expect(controller.steps[1].approver.id, 2);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
|
||||
test('removeStepAt 호출 시 순번을 재정렬한다', () {
|
||||
controller
|
||||
..addStep(approver: buildParticipant(1))
|
||||
..addStep(approver: buildParticipant(2))
|
||||
..addStep(approver: buildParticipant(3));
|
||||
|
||||
controller.removeStepAt(1);
|
||||
|
||||
expect(controller.steps, hasLength(2));
|
||||
expect(controller.steps.first.stepOrder, 1);
|
||||
expect(controller.steps.last.stepOrder, 2);
|
||||
});
|
||||
|
||||
test('moveStep 호출 시 순서를 변경한다', () {
|
||||
controller
|
||||
..addStep(approver: buildParticipant(1))
|
||||
..addStep(approver: buildParticipant(2))
|
||||
..addStep(approver: buildParticipant(3));
|
||||
|
||||
controller.moveStep(2, 0);
|
||||
|
||||
expect(controller.steps.first.approver.id, 3);
|
||||
expect(controller.steps.first.stepOrder, 1);
|
||||
expect(controller.steps.last.approver.id, 2);
|
||||
expect(controller.steps.last.stepOrder, 3);
|
||||
});
|
||||
|
||||
test('setFinalApprover는 단계가 없을 때 새 단계를 추가한다', () {
|
||||
final approver = buildParticipant(55);
|
||||
|
||||
final result = controller.setFinalApprover(approver);
|
||||
|
||||
expect(result, isTrue);
|
||||
expect(controller.steps, hasLength(1));
|
||||
expect(controller.finalApprover?.id, approver.id);
|
||||
});
|
||||
|
||||
test('buildTransactionApprovalInput은 필수 정보를 누락하면 예외를 던진다', () {
|
||||
controller.addStep(approver: buildParticipant(1));
|
||||
|
||||
expect(
|
||||
() => controller.buildTransactionApprovalInput(),
|
||||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test('buildTransactionApprovalInput은 요청 데이터를 변환한다', () {
|
||||
final requester = buildParticipant(77);
|
||||
controller.setRequester(requester);
|
||||
controller
|
||||
..addStep(approver: buildParticipant(101), note: '검토')
|
||||
..addStep(approver: buildParticipant(102));
|
||||
|
||||
final approvalInput = controller.buildTransactionApprovalInput(
|
||||
approvalStatusId: 9,
|
||||
title: '입고 결재',
|
||||
summary: '재고 입고 승인 요청',
|
||||
note: '긴급 승인 필요',
|
||||
metadata: const {'priority': 'high'},
|
||||
);
|
||||
|
||||
final payload = approvalInput.toJson();
|
||||
expect(payload['requested_by_id'], requester.id);
|
||||
expect(payload['approval_status_id'], 9);
|
||||
expect(payload['final_approver_id'], 102);
|
||||
expect(payload['title'], '입고 결재');
|
||||
expect(payload['summary'], '재고 입고 승인 요청');
|
||||
expect(payload['note'], '긴급 승인 필요');
|
||||
final steps = payload['steps'] as List<dynamic>;
|
||||
expect(steps, hasLength(2));
|
||||
expect(steps.first['step_order'], 1);
|
||||
expect(steps.first['approver_id'], 101);
|
||||
expect(steps.first['note'], '검토');
|
||||
expect(steps.last['step_order'], 2);
|
||||
expect(steps.last['approver_id'], 102);
|
||||
});
|
||||
|
||||
test('buildSubmissionInput 변환 시 finalApproverId를 채운다', () {
|
||||
controller
|
||||
..setRequester(buildParticipant(2))
|
||||
..addStep(approver: buildParticipant(10))
|
||||
..addStep(approver: buildParticipant(11), note: '최종 검토');
|
||||
|
||||
final submission = controller.buildSubmissionInput(statusId: 4);
|
||||
|
||||
expect(submission.requesterId, 2);
|
||||
expect(submission.finalApproverId, 11);
|
||||
expect(submission.steps, hasLength(2));
|
||||
});
|
||||
|
||||
test('applyTemplateSteps는 전달된 순서를 유지한다', () {
|
||||
final steps = [
|
||||
ApprovalRequestStep(stepOrder: 3, approver: buildParticipant(90)),
|
||||
ApprovalRequestStep(stepOrder: 9, approver: buildParticipant(91)),
|
||||
];
|
||||
|
||||
controller.applyTemplateSteps(steps);
|
||||
|
||||
expect(controller.steps.first.stepOrder, 1);
|
||||
expect(controller.steps.last.stepOrder, 2);
|
||||
expect(controller.finalApproverId, 91);
|
||||
});
|
||||
|
||||
test('중복 승인자가 있는 상태에서 빌드 시 예외가 발생한다', () {
|
||||
controller.setRequester(buildParticipant(5));
|
||||
controller.applyTemplateSteps([
|
||||
ApprovalRequestStep(stepOrder: 1, approver: buildParticipant(8)),
|
||||
ApprovalRequestStep(stepOrder: 2, approver: buildParticipant(8)),
|
||||
]);
|
||||
|
||||
expect(
|
||||
() => controller.buildSubmissionInput(statusId: 1),
|
||||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test('상신자와 동일한 승인자가 포함된 템플릿은 적용되지 않는다', () {
|
||||
final requester = buildParticipant(20);
|
||||
controller.setRequester(requester);
|
||||
|
||||
controller.applyTemplateSteps([
|
||||
ApprovalRequestStep(stepOrder: 1, approver: requester),
|
||||
ApprovalRequestStep(stepOrder: 2, approver: buildParticipant(21)),
|
||||
]);
|
||||
|
||||
expect(controller.steps, isEmpty);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
|
||||
test('상신자 변경 시 승인자와 겹치면 오류가 발생한다', () {
|
||||
controller
|
||||
..addStep(approver: buildParticipant(40))
|
||||
..addStep(approver: buildParticipant(41));
|
||||
|
||||
controller.setRequester(buildParticipant(41));
|
||||
|
||||
expect(controller.requester?.id, 41);
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user