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

@@ -17,7 +17,10 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
final ApiClient _api;
static const _basePath = '${ApiRoutes.apiV1}/approval-templates';
static const _basePaths = <String>[
ApiRoutes.approvalTemplates,
ApiRoutes.approvalTemplatesLegacy,
];
/// 결재 템플릿 목록을 조회한다. 검색/활성 여부 필터를 지원한다.
@override
@@ -27,17 +30,19 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
String? query,
bool? isActive,
}) async {
final response = await _api.get<Map<String, dynamic>>(
_basePath,
query: {
'page': page,
'page_size': pageSize,
if (query != null && query.isNotEmpty) 'q': query,
if (isActive != null) 'active': isActive,
},
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.parsePaginated(response.data);
return _withTemplateRoute((basePath) async {
final response = await _api.get<Map<String, dynamic>>(
basePath,
query: {
'page': page,
'page_size': pageSize,
if (query != null && query.isNotEmpty) 'q': query,
if (isActive != null) 'active': isActive,
},
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.parsePaginated(response.data);
});
}
/// 템플릿 상세 정보를 조회한다. 필요 시 단계 포함 여부를 지정한다.
@@ -46,14 +51,17 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
int id, {
bool includeSteps = true,
}) async {
final response = await _api.get<Map<String, dynamic>>(
'$_basePath/$id',
query: {if (includeSteps) 'include': 'steps'},
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: includeSteps);
return _withTemplateRoute((basePath) async {
final path = ApiClient.buildPath(basePath, [id]);
final response = await _api.get<Map<String, dynamic>>(
path,
query: {if (includeSteps) 'include': 'steps'},
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: includeSteps);
});
}
/// 템플릿을 생성하고 필요하면 단계까지 함께 등록한다.
@@ -62,18 +70,20 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
ApprovalTemplateInput input, {
List<ApprovalTemplateStepInput> steps = const [],
}) async {
final response = await _api.post<Map<String, dynamic>>(
_basePath,
data: input.toCreatePayload(),
options: Options(responseType: ResponseType.json),
);
final created = ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
if (steps.isNotEmpty) {
await _postSteps(created.id, steps);
}
return fetchDetail(created.id, includeSteps: true);
return _withTemplateRoute((basePath) async {
final response = await _api.post<Map<String, dynamic>>(
basePath,
data: input.toCreatePayload(),
options: Options(responseType: ResponseType.json),
);
final created = ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
if (steps.isNotEmpty) {
await _postSteps(created.id, steps, basePath: basePath);
}
return fetchDetail(created.id, includeSteps: true);
});
}
/// 템플릿 기본 정보와 단계 구성을 수정한다.
@@ -83,43 +93,54 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
ApprovalTemplateInput input, {
List<ApprovalTemplateStepInput>? steps,
}) async {
await _api.patch<Map<String, dynamic>>(
'$_basePath/$id',
data: input.toUpdatePayload(id),
options: Options(responseType: ResponseType.json),
);
if (steps != null) {
await _patchSteps(id, steps);
}
return fetchDetail(id, includeSteps: true);
return _withTemplateRoute((basePath) async {
final path = ApiClient.buildPath(basePath, [id]);
await _api.patch<Map<String, dynamic>>(
path,
data: input.toUpdatePayload(id),
options: Options(responseType: ResponseType.json),
);
if (steps != null) {
await _patchSteps(id, steps, basePath: basePath);
}
return fetchDetail(id, includeSteps: true);
});
}
/// 템플릿을 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
await _withTemplateRoute((basePath) async {
final path = ApiClient.buildPath(basePath, [id]);
await _api.delete<void>(path);
});
}
/// 삭제된 템플릿을 복구한다.
@override
Future<ApprovalTemplate> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/restore',
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
return _withTemplateRoute((basePath) async {
final path = ApiClient.buildPath(basePath, [id, 'restore']);
final response = await _api.post<Map<String, dynamic>>(
path,
options: Options(responseType: ResponseType.json),
);
return ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
});
}
/// 템플릿 단계 전체를 신규로 등록한다.
Future<void> _postSteps(
int templateId,
List<ApprovalTemplateStepInput> steps,
) async {
List<ApprovalTemplateStepInput> steps, {
required String basePath,
}) async {
if (steps.isEmpty) return;
final path = ApiClient.buildPath(basePath, [templateId, 'steps']);
await _api.post<Map<String, dynamic>>(
'$_basePath/$templateId/steps',
path,
data: {
'id': templateId,
'steps': steps.map((step) => step.toJson(includeId: false)).toList(),
@@ -131,10 +152,12 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
/// 템플릿 단계 정보를 부분 수정한다.
Future<void> _patchSteps(
int templateId,
List<ApprovalTemplateStepInput> steps,
) async {
List<ApprovalTemplateStepInput> steps, {
required String basePath,
}) async {
final path = ApiClient.buildPath(basePath, [templateId, 'steps']);
await _api.patch<Map<String, dynamic>>(
'$_basePath/$templateId/steps',
path,
data: {
'id': templateId,
'steps': steps.map((step) => step.toJson()).toList(),
@@ -142,4 +165,25 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
options: Options(responseType: ResponseType.json),
);
}
Future<T> _withTemplateRoute<T>(
Future<T> Function(String basePath) operation,
) async {
DioException? lastNotFound;
for (final basePath in _basePaths) {
try {
return await operation(basePath);
} on DioException catch (error) {
if (error.response?.statusCode == 404) {
lastNotFound = error;
continue;
}
rethrow;
}
}
if (lastNotFound != null) {
throw lastNotFound;
}
throw StateError('템플릿 경로 후보가 정의되지 않았습니다.');
}
}