결재 API 계약 보완 및 테스트 정리

This commit is contained in:
JiWoong Sul
2025-10-16 18:53:22 +09:00
parent 9e2244f260
commit efed3c1a6f
44 changed files with 1969 additions and 293 deletions

View File

@@ -0,0 +1,162 @@
import '../../../../core/common/utils/json_utils.dart';
import '../../domain/entities/auth_permission.dart';
import '../../domain/entities/auth_session.dart';
import '../../domain/entities/authenticated_user.dart';
/// 로그인/토큰 갱신 응답을 역직렬화하는 DTO.
class AuthSessionDto {
AuthSessionDto({
required this.accessToken,
required this.refreshToken,
required this.user,
this.expiresAt,
this.permissions = const [],
});
final String accessToken;
final String refreshToken;
final DateTime? expiresAt;
final AuthenticatedUser user;
final List<AuthPermissionDto> permissions;
factory AuthSessionDto.fromJson(Map<String, dynamic> json) {
final token = _readString(json, 'access_token');
final refresh = _readString(json, 'refresh_token');
final expires = _parseDate(_readString(json, 'expires_at'));
final userMap = _readMap(json, 'user');
final permissionList = _readList(json, 'permissions');
return AuthSessionDto(
accessToken: token ?? '',
refreshToken: refresh ?? '',
expiresAt: expires,
user: _parseUser(userMap),
permissions: permissionList
.map(AuthPermissionDto.fromJson)
.toList(growable: false),
);
}
AuthSession toEntity() {
return AuthSession(
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: expiresAt,
user: user,
permissions: permissions
.map((dto) => dto.toEntity())
.toList(growable: false),
);
}
}
class AuthPermissionDto {
const AuthPermissionDto({required this.resource, required this.actions});
final String resource;
final List<String> actions;
factory AuthPermissionDto.fromJson(Map<String, dynamic>? json) {
if (json == null) {
throw const FormatException('권한 정보가 비어 있습니다.');
}
final resource = _readString(json, 'resource') ?? '';
final actions = <String>[];
final rawActions = json['actions'];
if (rawActions is List) {
for (final item in rawActions) {
if (item is String) {
final normalized = item.trim().toLowerCase();
if (normalized.isNotEmpty) {
actions.add(normalized);
}
continue;
}
if (item is Map<String, dynamic>) {
for (final entry in item.entries) {
final normalized = entry.value.toString().trim().toLowerCase();
if (normalized.isNotEmpty) {
actions.add(normalized);
}
}
}
}
}
return AuthPermissionDto(
resource: resource,
actions: actions.toList(growable: false),
);
}
AuthPermission toEntity() =>
AuthPermission(resource: resource, actions: actions);
}
AuthenticatedUser _parseUser(Map<String, dynamic> json) {
final id = _readOptionalInt(json, 'id') ?? 0;
final name = _readString(json, 'name') ?? '';
final employeeNo = _readString(json, 'employee_no');
final email = _readString(json, 'email');
final group = JsonUtils.extractMap(
json,
keys: const ['group', 'primary_group'],
);
return AuthenticatedUser(
id: id,
name: name,
employeeNo: employeeNo,
email: email,
primaryGroupId: _readOptionalInt(group, 'id'),
primaryGroupName: _readString(group, 'name'),
);
}
String? _readString(Map<String, dynamic>? source, String key) {
if (source == null) {
return null;
}
final value = source[key];
if (value is String) {
return value.trim();
}
return null;
}
List<Map<String, dynamic>> _readList(Map<String, dynamic> source, String key) {
final value = source[key];
if (value is List) {
return value.whereType<Map<String, dynamic>>().toList(growable: false);
}
return const [];
}
Map<String, dynamic> _readMap(Map<String, dynamic> source, String key) {
final value = source[key];
if (value is Map<String, dynamic>) {
return value;
}
return const {};
}
DateTime? _parseDate(String? value) {
if (value == null || value.isEmpty) {
return null;
}
return DateTime.tryParse(value);
}
int? _readOptionalInt(Map<String, dynamic>? source, String key) {
if (source == null) {
return null;
}
final value = source[key];
if (value is int) {
return value;
}
if (value is String) {
return int.tryParse(value);
}
if (value is double) {
return value.round();
}
return null;
}

View File

@@ -0,0 +1,43 @@
import 'package:dio/dio.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/network/api_routes.dart';
import '../../domain/entities/auth_session.dart';
import '../../domain/entities/login_request.dart';
import '../../domain/repositories/auth_repository.dart';
import '../dtos/auth_session_dto.dart';
/// 인증 관련 엔드포인트를 호출하는 원격 저장소 구현체.
class AuthRepositoryRemote implements AuthRepository {
AuthRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
final ApiClient _api;
static const _basePath = '${ApiRoutes.apiV1}/auth';
@override
Future<AuthSession> login(LoginRequest request) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/login',
data: {
'identifier': request.identifier,
'password': request.password,
'remember_me': request.rememberMe,
},
options: Options(responseType: ResponseType.json),
);
final json = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return AuthSessionDto.fromJson(json).toEntity();
}
@override
Future<AuthSession> refresh(String refreshToken) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/refresh',
data: {'refresh_token': refreshToken},
options: Options(responseType: ResponseType.json),
);
final json = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return AuthSessionDto.fromJson(json).toEntity();
}
}