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:
@@ -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('템플릿 경로 후보가 정의되지 않았습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user