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,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:intl/intl.dart' as intl;
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../../core/config/environment.dart';
@@ -15,6 +16,7 @@ import '../../../domain/entities/approval_template.dart';
import '../../../domain/repositories/approval_template_repository.dart';
import '../../../domain/usecases/apply_approval_template_use_case.dart';
import '../../../domain/usecases/save_approval_template_use_case.dart';
import '../../../../auth/application/auth_service.dart';
import '../controllers/approval_template_controller.dart';
/// 결재 템플릿 관리 페이지. 기능 플래그에 따라 준비중 화면을 노출한다.
@@ -69,7 +71,7 @@ class _ApprovalTemplateEnabledPageState
late final ApprovalTemplateController _controller;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocus = FocusNode();
final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy-MM-dd HH:mm');
String? _lastError;
static const _pageSizeOptions = [10, 20, 50];
@@ -354,6 +356,50 @@ class _ApprovalTemplateEnabledPageState
_searchFocus.requestFocus();
}
String _generateTemplateCode() {
final authService = GetIt.I<AuthService>();
final session = authService.session;
String normalizedEmployee = '';
final candidateValues = <String?>[
session?.user.employeeNo,
session?.user.email,
session?.user.name,
];
for (final candidate in candidateValues) {
if (candidate == null) {
continue;
}
var source = candidate.trim();
final atIndex = source.indexOf('@');
if (atIndex > 0) {
source = source.substring(0, atIndex);
}
final normalized = source.toUpperCase().replaceAll(
RegExp(r'[^A-Z0-9]'),
'',
);
if (normalized.isNotEmpty) {
normalizedEmployee = normalized;
break;
}
}
if (normalizedEmployee.isEmpty && session?.user.id != null) {
normalizedEmployee = session!.user.id.toString();
}
final suffixSource = normalizedEmployee.isEmpty
? '0000'
: normalizedEmployee;
final suffix = suffixSource.length >= 4
? suffixSource.substring(suffixSource.length - 4)
: suffixSource.padLeft(4, '0');
final timestamp = intl.DateFormat(
'yyMMddHHmmssSSS',
).format(DateTime.now().toUtc());
return 'AP_TEMP_${suffix}_$timestamp';
}
Future<void> _openTemplatePreview(int templateId) async {
showDialog<void>(
context: context,
@@ -530,7 +576,9 @@ class _ApprovalTemplateEnabledPageState
Future<bool?> _openTemplateForm({ApprovalTemplate? template}) async {
final isEdit = template != null;
final existingTemplate = template;
final codeController = TextEditingController(text: template?.code ?? '');
final codeController = TextEditingController(
text: isEdit ? existingTemplate!.code : _generateTemplateCode(),
);
final nameController = TextEditingController(text: template?.name ?? '');
final descriptionController = TextEditingController(
text: template?.description ?? '',
@@ -573,7 +621,7 @@ class _ApprovalTemplateEnabledPageState
)
.toList();
final input = ApprovalTemplateInput(
code: isEdit ? existingTemplate?.code : codeValue,
code: isEdit ? existingTemplate!.code : codeValue,
name: nameValue,
description: descriptionController.text.trim().isEmpty
? null
@@ -583,16 +631,11 @@ class _ApprovalTemplateEnabledPageState
: 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)
final success = isEdit
? await _controller.update(existingTemplate!.id, input, stepInputs)
: await _controller.create(input, stepInputs);
if (success != null && mounted) {
Navigator.of(context, rootNavigator: true).pop(true);
@@ -622,6 +665,8 @@ class _ApprovalTemplateEnabledPageState
label: '템플릿 코드',
child: ShadInput(
controller: codeController,
readOnly: true,
enabled: false,
placeholder: const Text('예: AP_INBOUND'),
),
),