feat(frontend): 승인 템플릿 API 통합 및 디버그 로그인 확장

- docs 폴더 문서를 최신 API 계약으로 갱신하고 가이드를 다듬었다\n- approvals data/presentation 레이어를 API v4 스펙에 맞춰 리팩터링했다\n- approver 자동완성 위젯을 신규 공유 레포지토리에 맞춰 교체하고 UX를 보강했다\n- inventory/rental 페이지 테이블 초기화 시 승인 기준 연동을 정비했다\n- 로그인 페이지 디버그 버튼을 tera/exa 계정으로 분리해 QA 로그인을 단순화했다\n- get_it 등록과 테스트 케이스를 신규 공유 리포지토리에 맞춰 업데이트했다
This commit is contained in:
JiWoong Sul
2025-11-05 17:05:38 +09:00
parent 3e83408aa7
commit fa0bda5ea4
28 changed files with 1102 additions and 545 deletions

View File

@@ -1,5 +1,9 @@
import 'dart:async';
import 'package:get_it/get_it.dart';
import '../../../domain/entities/approval.dart';
import '../../../shared/approver_catalog.dart';
import '../../../shared/domain/repositories/approval_approver_repository.dart';
import '../controllers/approval_request_controller.dart';
import '../../../../inventory/transactions/domain/entities/stock_transaction_input.dart';
@@ -10,12 +14,13 @@ class ApprovalFormInitializer {
ApprovalFormInitializer._();
/// 결재 구성 컨트롤러에 기본값을 주입한다.
static void populate({
static Future<void> populate({
required ApprovalRequestController controller,
Approval? existingApproval,
StockTransactionApprovalInput? draft,
ApprovalRequestParticipant? defaultRequester,
}) {
ApprovalApproverRepository? repository,
}) async {
if (existingApproval != null) {
_applyExistingApproval(controller, existingApproval);
return;
@@ -24,7 +29,11 @@ class ApprovalFormInitializer {
controller.setRequester(defaultRequester);
}
if (draft != null) {
_applyDraft(controller, draft);
await _applyDraft(
controller,
draft,
repository ?? _resolveRepository(),
);
}
}
@@ -57,40 +66,62 @@ class ApprovalFormInitializer {
}
}
static void _applyDraft(
static Future<void> _applyDraft(
ApprovalRequestController controller,
StockTransactionApprovalInput draft,
) {
final requesterCatalog = ApprovalApproverCatalog.byId(draft.requestedById);
if (requesterCatalog != null) {
controller.setRequester(
ApprovalRequestParticipant(
id: requesterCatalog.id,
name: requesterCatalog.name,
employeeNo: requesterCatalog.employeeNo,
),
);
ApprovalApproverRepository? repository,
) async {
final repo = repository;
if (repo == null) {
return;
}
final steps = draft.steps
.map((step) {
final catalog = ApprovalApproverCatalog.byId(step.approverId);
if (catalog == null) {
final requester = await _fetchParticipant(repo, draft.requestedById);
if (requester != null) {
controller.setRequester(requester);
}
final futures = draft.steps
.map((step) async {
final participant = await _fetchParticipant(repo, step.approverId);
if (participant == null) {
return null;
}
return ApprovalRequestStep(
stepOrder: step.stepOrder,
approver: ApprovalRequestParticipant(
id: catalog.id,
name: catalog.name,
employeeNo: catalog.employeeNo,
),
approver: participant,
note: step.note,
);
})
.whereType<ApprovalRequestStep>()
.toList(growable: false);
final resolvedSteps = await Future.wait(futures);
final steps = resolvedSteps.whereType<ApprovalRequestStep>().toList();
if (steps.isNotEmpty) {
controller.applyTemplateSteps(steps);
}
}
static Future<ApprovalRequestParticipant?> _fetchParticipant(
ApprovalApproverRepository repository,
int id,
) async {
final candidate = await repository.fetchById(id);
if (candidate == null) {
return null;
}
return ApprovalRequestParticipant(
id: candidate.id,
name: candidate.name,
employeeNo: candidate.employeeNo,
);
}
static ApprovalApproverRepository? _resolveRepository() {
final getIt = GetIt.I;
if (!getIt.isRegistered<ApprovalApproverRepository>()) {
return null;
}
return getIt<ApprovalApproverRepository>();
}
}

View File

@@ -4,7 +4,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../widgets/components/feedback.dart';
import '../../../../../widgets/components/superport_dialog.dart';
import '../../../shared/approver_catalog.dart';
import '../../../shared/domain/entities/approval_approver_candidate.dart';
import '../../../shared/widgets/approver_autocomplete_field.dart';
import '../controllers/approval_request_controller.dart';
import 'approval_step_row.dart';
@@ -435,7 +435,7 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
}
Future<void> _openAddStepDialog() async {
ApprovalApproverCatalogItem? selected;
ApprovalApproverCandidate? selected;
final idController = TextEditingController();
final result = await SuperportDialog.show<bool>(
@@ -483,13 +483,19 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
return;
}
final participant = _resolveParticipant(selected, idController.text.trim());
if (participant == null) {
final selection = selected;
if (selection == null) {
SuperportToast.warning(context, '유효한 승인자를 선택해주세요.');
idController.dispose();
return;
}
final participant = ApprovalRequestParticipant(
id: selection.id,
name: selection.name,
employeeNo: selection.employeeNo,
);
final added = widget.controller.addStep(approver: participant);
if (!added) {
final message = widget.controller.errorMessage ?? '결재 단계를 추가하지 못했습니다.';
@@ -503,31 +509,6 @@ class _ConfiguratorDialogBodyState extends State<_ConfiguratorDialogBody> {
idController.dispose();
}
ApprovalRequestParticipant? _resolveParticipant(
ApprovalApproverCatalogItem? selected,
String manualInput,
) {
if (selected != null) {
return ApprovalRequestParticipant(
id: selected.id,
name: selected.name,
employeeNo: selected.employeeNo,
);
}
final manualId = int.tryParse(manualInput);
if (manualId == null) {
return null;
}
final match = ApprovalApproverCatalog.byId(manualId);
if (match == null) {
return null;
}
return ApprovalRequestParticipant(
id: match.id,
name: match.name,
employeeNo: match.employeeNo,
);
}
}
class _InfoBadge extends StatelessWidget {

View File

@@ -3,7 +3,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../widgets/components/feedback.dart';
import '../../../shared/approver_catalog.dart';
import '../../../shared/domain/entities/approval_approver_candidate.dart';
import '../../../shared/widgets/approver_autocomplete_field.dart';
import '../controllers/approval_request_controller.dart';
@@ -96,37 +96,23 @@ class _ApprovalStepRowState extends State<ApprovalStepRow> {
Future<void> _handleApproverSelected(
BuildContext context,
ApprovalApproverCatalogItem? item,
ApprovalApproverCandidate? candidate,
) async {
if (widget.readOnly) {
return;
}
ApprovalRequestParticipant? nextParticipant;
if (item != null) {
if (candidate != null) {
nextParticipant = ApprovalRequestParticipant(
id: item.id,
name: item.name,
employeeNo: item.employeeNo,
id: candidate.id,
name: candidate.name,
employeeNo: candidate.employeeNo,
);
} else {
final manualId = int.tryParse(_approverIdController.text.trim());
if (manualId == null) {
SuperportToast.warning(context, '승인자를 다시 선택해주세요.');
_restorePreviousApprover();
return;
}
final catalogMatch = ApprovalApproverCatalog.byId(manualId);
if (catalogMatch == null) {
SuperportToast.warning(context, '등록되지 않은 승인자입니다.');
_restorePreviousApprover();
return;
}
nextParticipant = ApprovalRequestParticipant(
id: catalogMatch.id,
name: catalogMatch.name,
employeeNo: catalogMatch.employeeNo,
);
SuperportToast.warning(context, '승인자를 다시 선택해주세요.');
_restorePreviousApprover();
return;
}
final updated = widget.controller.updateStep(