번호 자동 부여 대응 및 API 공통 처리 보강
This commit is contained in:
@@ -12,6 +12,7 @@ class ApiClient {
|
||||
|
||||
final Dio _dio;
|
||||
final ApiErrorMapper _errorMapper;
|
||||
static const _dataKey = 'data';
|
||||
|
||||
/// 내부에서 사용하는 Dio 인스턴스
|
||||
/// 외부에서 Dio 직접 사용을 최소화하고, 가능하면 아래 헬퍼 메서드를 사용한다.
|
||||
@@ -99,4 +100,37 @@ class ApiClient {
|
||||
throw _errorMapper.map(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// `{ "data": ... }` 형태의 응답에서 내부 데이터를 추출한다.
|
||||
///
|
||||
/// - `data` 키가 존재하면 해당 값을 반환한다.
|
||||
/// - `data`가 맵 타입이 아니거나 null이면 원본 본문을 그대로 돌려준다.
|
||||
/// - 최종적으로 맵이 아니면 빈 맵을 반환한다.
|
||||
Map<String, dynamic> unwrapAsMap(Response<dynamic> response) {
|
||||
final payload = _maybeUnwrap(response.data);
|
||||
if (payload is Map<String, dynamic>) {
|
||||
return payload;
|
||||
}
|
||||
final original = response.data;
|
||||
if (original is Map<String, dynamic>) {
|
||||
return original;
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
/// 응답 본문에서 envelope을 제거해 반환한다.
|
||||
///
|
||||
/// - `{ "data": [...] }`는 내부 리스트를 돌려준다.
|
||||
/// - `data` 구조가 아니면 원본을 그대로 반환한다.
|
||||
dynamic unwrap(Response<dynamic> response) => _maybeUnwrap(response.data);
|
||||
|
||||
dynamic _maybeUnwrap(dynamic body) {
|
||||
if (body is Map<String, dynamic> && body.containsKey(_dataKey)) {
|
||||
final nested = body[_dataKey];
|
||||
if (nested != null) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class ApiErrorMapper {
|
||||
final status = error.response?.statusCode;
|
||||
final data = error.response?.data;
|
||||
final message = _resolveMessage(error, data);
|
||||
final details = _extractDetails(data);
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout ||
|
||||
@@ -76,7 +77,6 @@ class ApiErrorMapper {
|
||||
}
|
||||
|
||||
if (status != null) {
|
||||
final details = _extractDetails(data);
|
||||
switch (status) {
|
||||
case 400:
|
||||
return ApiException(
|
||||
@@ -89,8 +89,9 @@ class ApiErrorMapper {
|
||||
case 401:
|
||||
return ApiException(
|
||||
code: ApiErrorCode.unauthorized,
|
||||
message: '세션이 만료되었습니다. 다시 로그인해 주세요.',
|
||||
message: _localizeUnauthorizedMessage(message),
|
||||
statusCode: status,
|
||||
details: details,
|
||||
cause: error,
|
||||
);
|
||||
case 403:
|
||||
@@ -98,7 +99,7 @@ class ApiErrorMapper {
|
||||
code: ApiErrorCode.forbidden,
|
||||
message: message,
|
||||
statusCode: status,
|
||||
details: _extractDetails(data),
|
||||
details: details,
|
||||
cause: error,
|
||||
);
|
||||
case 404:
|
||||
@@ -106,6 +107,7 @@ class ApiErrorMapper {
|
||||
code: ApiErrorCode.notFound,
|
||||
message: '요청한 리소스를 찾을 수 없습니다.',
|
||||
statusCode: status,
|
||||
details: details,
|
||||
cause: error,
|
||||
);
|
||||
case 409:
|
||||
@@ -133,6 +135,7 @@ class ApiErrorMapper {
|
||||
code: ApiErrorCode.unknown,
|
||||
message: message,
|
||||
statusCode: status,
|
||||
details: details,
|
||||
cause: error,
|
||||
);
|
||||
}
|
||||
@@ -332,4 +335,25 @@ class ApiErrorMapper {
|
||||
}
|
||||
return <dynamic>[value];
|
||||
}
|
||||
|
||||
/// 인증 실패 응답 메시지를 한글 안내로 정규화한다.
|
||||
String _localizeUnauthorizedMessage(String message) {
|
||||
final trimmed = message.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return '세션이 만료되었습니다. 다시 로그인해 주세요.';
|
||||
}
|
||||
final normalized = trimmed.toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'invalid credentials':
|
||||
return '아이디 또는 비밀번호가 올바르지 않습니다.';
|
||||
case 'account is inactive':
|
||||
return '비활성 계정입니다. 관리자에게 문의하세요.';
|
||||
case 'token expired':
|
||||
return '세션이 만료되었습니다. 다시 로그인해 주세요.';
|
||||
case 'invalid token':
|
||||
return '유효하지 않은 토큰입니다. 다시 로그인해 주세요.';
|
||||
default:
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/approvals/history/presentation/pages/approval_history_page.dart';
|
||||
import '../../features/auth/application/auth_service.dart';
|
||||
import '../../features/approvals/request/presentation/pages/approval_request_page.dart';
|
||||
import '../../features/approvals/step/presentation/pages/approval_step_page.dart';
|
||||
import '../../features/approvals/template/presentation/pages/approval_template_page.dart';
|
||||
@@ -34,6 +36,18 @@ final _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final appRouter = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: loginRoutePath,
|
||||
redirect: (context, state) {
|
||||
final authService = GetIt.I<AuthService>();
|
||||
final loggedIn = authService.session != null;
|
||||
final loggingIn = state.uri.path == loginRoutePath;
|
||||
if (!loggedIn && !loggingIn) {
|
||||
return loginRoutePath;
|
||||
}
|
||||
if (loggedIn && loggingIn) {
|
||||
return dashboardRoutePath;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/', redirect: (_, __) => loginRoutePath),
|
||||
GoRoute(
|
||||
|
||||
Reference in New Issue
Block a user