From cf5628580544a243df3674b4d9d2c9ec97f127cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 15 Feb 2026 16:50:20 +0900 Subject: [PATCH] =?UTF-8?q?fix:GCS=20=EC=84=9C=EB=AA=85=20URL=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A4=91=EB=B3=B5=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(gs://=20prefix=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upload()이 gs://bucket/path 형식으로 저장하지만 getSignedUrl()과 delete()는 path만 기대하여 URL에 경로가 중복됨 → 404 발생 stripGsPrefix()로 자동 변환하여 해결 Co-Authored-By: Claude Opus 4.6 --- app/Services/GoogleCloudStorageService.php | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/Services/GoogleCloudStorageService.php b/app/Services/GoogleCloudStorageService.php index c00e30b2..e65e5357 100644 --- a/app/Services/GoogleCloudStorageService.php +++ b/app/Services/GoogleCloudStorageService.php @@ -169,7 +169,7 @@ public function upload(string $filePath, string $objectName): ?string /** * GCS에서 서명된 다운로드 URL 생성 * - * @param string $objectName GCS 객체 이름 + * @param string $objectName GCS 객체 이름 또는 gs:// URI * @param int $expiresInMinutes URL 유효 시간 (분) * @return string|null 서명된 URL 또는 실패 시 null */ @@ -179,6 +179,8 @@ public function getSignedUrl(string $objectName, int $expiresInMinutes = 60): ?s return null; } + $objectName = $this->stripGsPrefix($objectName); + $expiration = time() + ($expiresInMinutes * 60); $stringToSign = "GET\n\n\n{$expiration}\n/{$this->bucketName}/{$objectName}"; @@ -205,7 +207,7 @@ public function getSignedUrl(string $objectName, int $expiresInMinutes = 60): ?s /** * GCS에서 파일 삭제 * - * @param string $objectName GCS 객체 이름 + * @param string $objectName GCS 객체 이름 또는 gs:// URI * @return bool 성공 여부 */ public function delete(string $objectName): bool @@ -214,6 +216,8 @@ public function delete(string $objectName): bool return false; } + $objectName = $this->stripGsPrefix($objectName); + $accessToken = $this->getAccessToken(); if (!$accessToken) { return false; @@ -297,6 +301,26 @@ private function base64UrlEncode(string $data): string return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } + /** + * gs://bucket-name/objectName → objectName 변환 + * + * upload()가 gs:// URI를 반환하므로, getSignedUrl/delete에서 자동 처리 + */ + private function stripGsPrefix(string $objectName): string + { + $prefix = 'gs://' . $this->bucketName . '/'; + if (str_starts_with($objectName, $prefix)) { + return substr($objectName, strlen($prefix)); + } + + // gs://다른-버킷/ 형태도 처리 + if (str_starts_with($objectName, 'gs://')) { + $objectName = preg_replace('#^gs://[^/]+/#', '', $objectName); + } + + return $objectName; + } + /** * 버킷 이름 반환 */