chore: 통합 테스트 환경과 보고서 리모트 구성
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_download_result.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_export_format.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/entities/report_export_request.dart';
|
||||
import 'package:superport_v2/features/reporting/domain/repositories/reporting_repository.dart';
|
||||
|
||||
/// 보고서 다운로드 API를 호출하는 원격 리포지토리 구현체.
|
||||
class ReportingRepositoryRemote implements ReportingRepository {
|
||||
ReportingRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _transactionsPath =
|
||||
'${ApiRoutes.apiV1}/reports/transactions/export';
|
||||
static const _approvalsPath = '${ApiRoutes.apiV1}/reports/approvals/export';
|
||||
|
||||
@override
|
||||
Future<ReportDownloadResult> exportTransactions(
|
||||
ReportExportRequest request,
|
||||
) async {
|
||||
final response = await _api.get<Uint8List>(
|
||||
_transactionsPath,
|
||||
query: _buildQuery(request),
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return _mapResponse(response, format: request.format);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReportDownloadResult> exportApprovals(
|
||||
ReportExportRequest request,
|
||||
) async {
|
||||
final response = await _api.get<Uint8List>(
|
||||
_approvalsPath,
|
||||
query: _buildQuery(request),
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return _mapResponse(response, format: request.format);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildQuery(ReportExportRequest request) {
|
||||
return {
|
||||
'from': request.from.toIso8601String(),
|
||||
'to': request.to.toIso8601String(),
|
||||
'format': request.format.apiValue,
|
||||
if (request.transactionTypeId != null)
|
||||
'type_id': request.transactionTypeId,
|
||||
if (request.statusId != null) 'status_id': request.statusId,
|
||||
if (request.warehouseId != null) 'warehouse_id': request.warehouseId,
|
||||
};
|
||||
}
|
||||
|
||||
ReportDownloadResult _mapResponse(
|
||||
Response<Uint8List> response, {
|
||||
required ReportExportFormat format,
|
||||
}) {
|
||||
final contentType = response.headers.value('content-type') ?? '';
|
||||
final disposition = response.headers.value('content-disposition') ?? '';
|
||||
|
||||
if (contentType.contains('application/json')) {
|
||||
final body = _decodeJson(response.data);
|
||||
final map = body is Map<String, dynamic> ? body : <String, dynamic>{};
|
||||
final data = map['data'] is Map<String, dynamic>
|
||||
? map['data'] as Map<String, dynamic>
|
||||
: map;
|
||||
final url = _readString(data, 'download_url');
|
||||
return ReportDownloadResult(
|
||||
downloadUrl: url == null ? null : Uri.tryParse(url),
|
||||
filename: _readString(data, 'filename'),
|
||||
mimeType: _readString(data, 'mime_type') ?? _defaultMimeType(format),
|
||||
expiresAt: _parseDateTime(_readString(data, 'expires_at')),
|
||||
);
|
||||
}
|
||||
|
||||
final filename = _parseFilename(disposition) ?? 'report.${format.apiValue}';
|
||||
final bytes = response.data ?? Uint8List(0);
|
||||
|
||||
return ReportDownloadResult(
|
||||
bytes: bytes,
|
||||
filename: filename,
|
||||
mimeType: contentType.isEmpty ? _defaultMimeType(format) : contentType,
|
||||
);
|
||||
}
|
||||
|
||||
dynamic _decodeJson(Uint8List? bytes) {
|
||||
if (bytes == null || bytes.isEmpty) {
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
try {
|
||||
final decoded = utf8.decode(bytes);
|
||||
return jsonDecode(decoded);
|
||||
} catch (_) {
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
}
|
||||
|
||||
String? _readString(Map<String, dynamic> map, String key) {
|
||||
final value = map[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? _parseDateTime(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.tryParse(value);
|
||||
}
|
||||
|
||||
String? _parseFilename(String disposition) {
|
||||
if (disposition.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final parts = disposition.split(';');
|
||||
for (final part in parts) {
|
||||
final trimmed = part.trim();
|
||||
if (trimmed.toLowerCase().startsWith('filename=')) {
|
||||
final encoded = trimmed.substring('filename='.length);
|
||||
return encoded.replaceAll('"', '');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _defaultMimeType(ReportExportFormat format) {
|
||||
return switch (format) {
|
||||
ReportExportFormat.xlsx =>
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
ReportExportFormat.pdf => 'application/pdf',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user