error($messageKey, 404, null, $messageParams); } /** * Unauthorized 응답을 생성합니다. * * @param string $messageKey 메시지 키 * @param array $messageParams 메시지 매개변수 * @return JsonResponse */ protected function unauthorized( string $messageKey = 'common.unauthorized', array $messageParams = [] ) { return $this->error($messageKey, 401, null, $messageParams); } /** * Forbidden 응답을 생성합니다. * * @param string $messageKey 메시지 키 * @param array $messageParams 메시지 매개변수 * @return JsonResponse */ protected function forbidden( string $messageKey = 'common.forbidden', array $messageParams = [] ) { return $this->error($messageKey, 403, null, $messageParams); } /** * Validation Error 응답을 생성합니다. * * @param mixed $errors 검증 오류 * @param string $messageKey 메시지 키 * @param array $messageParams 메시지 매개변수 * @return JsonResponse */ protected function validationError( mixed $errors, string $messageKey = 'common.validation_failed', array $messageParams = [] ) { return $this->error($messageKey, 422, $errors, $messageParams); } /** * 파일 응답을 반환합니다 (ETag 및 캐싱 헤더 포함). * * @param string $filePath 파일 경로 * @param string $mimeType MIME 타입 * @param int $maxAge 캐시 유지 시간 (초, 기본: 1년) */ protected function fileResponse(string $filePath, string $mimeType, int $maxAge = 31536000): BinaryFileResponse|Response { // ETag 생성 (파일 수정 시간 + 파일 크기 기반) $etag = md5(filemtime($filePath).filesize($filePath)); // If-None-Match 헤더 확인 (ETag 비교) if (request()->header('If-None-Match') === $etag) { return response('', 304)->header('ETag', $etag); // 304 Not Modified } // 환경별 캐싱 정책 $cacheControl = app()->environment('production') ? "public, max-age={$maxAge}, immutable" // 프로덕션: immutable 추가 : 'no-cache'; // 개발: 캐싱 비활성화 $response = response()->file($filePath, [ 'Content-Type' => $mimeType, 'Expires' => gmdate('D, d M Y H:i:s', time() + $maxAge).' GMT', 'ETag' => $etag, ]); // Cache-Control 헤더를 수동으로 설정 (기본 헤더 덮어쓰기) $response->headers->set('Cache-Control', $cacheControl); return $response; } /** * JSON 응답을 반환합니다 (캐싱 헤더 포함). * * @param mixed $data JSON으로 변환할 데이터 * @param int $maxAge 캐시 유지 시간 (초, 기본: 1시간) * @param int $status HTTP 상태 코드 * @return JsonResponse JSON 응답 */ protected function cachedJsonResponse(mixed $data, int $maxAge = 3600, int $status = 200): JsonResponse { return response()->json($data, $status, [ 'Cache-Control' => "public, max-age={$maxAge}", ], ResponseHelper::JSON_ENCODE_OPTIONS); } /** * ETag를 생성합니다. * * @param mixed $data 해시할 데이터 * @return string ETag 값 (따옴표 포함) */ protected function generateETag(mixed $data): string { $content = is_string($data) ? $data : json_encode($data); return '"'.md5($content).'"'; } /** * 클라이언트 캐시가 유효한지 확인합니다. * * @param string $etag 현재 ETag 값 * @return bool 캐시가 유효하면 true */ protected function isNotModified(string $etag): bool { $clientEtag = request()->header('If-None-Match'); return $clientEtag !== null && $clientEtag === $etag; } /** * 304 Not Modified 응답을 반환합니다. * * @param string $etag ETag 값 * @param int $maxAge 캐시 TTL (초) * @return Response 304 응답 */ protected function notModifiedResponse(string $etag, int $maxAge = 3600): Response { return response('', 304) ->header('ETag', $etag) ->header('Cache-Control', "public, max-age={$maxAge}"); } /** * 캐시 헤더와 함께 성공 응답을 반환합니다. * * 클라이언트의 ETag가 일치하면 304 Not Modified를 반환합니다. * * @param string $messageKey 메시지 키 * @param mixed $data 응답 데이터 * @param int $maxAge 캐시 TTL (초, 기본: 1시간) * @param array $messageParams 메시지 매개변수 * @return JsonResponse|Response JSON 응답 또는 304 응답 */ protected function successWithCache( string $messageKey = 'common.success', mixed $data = null, int $maxAge = 3600, array $messageParams = [] ): JsonResponse|Response { $etag = $this->generateETag($data); // 304 Not Modified 처리 if ($this->isNotModified($etag)) { return $this->notModifiedResponse($etag, $maxAge); } return response()->json([ 'success' => true, 'message' => __($messageKey, $messageParams), 'data' => $data, ], 200, [], ResponseHelper::JSON_ENCODE_OPTIONS) ->header('ETag', $etag) ->header('Cache-Control', "public, max-age={$maxAge}") ->header('Vary', 'Accept-Encoding, Accept-Language'); } }