From ae79a101a21f0e1227617f34e5876c5bff00e46e Mon Sep 17 00:00:00 2001 From: Wisesaturn Date: Sun, 27 Sep 2026 18:10:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix/#131:=20=EC=A0=9C=EB=AA=A9=EC=97=90=20?= =?UTF-8?q?=EA=B4=84=ED=98=B8=EA=B0=80=20=EB=93=A0=20=EA=B8=80=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EA=B0=80=20=EB=B0=9C=ED=96=89=20?= =?UTF-8?q?=EB=95=8C=EB=A7=88=EB=8B=A4=20=EC=A7=80=EC=9B=8C=EC=A7=80?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๐Ÿ“‹ ์ž‘์—… ๋‚ด์—ญ - `features/publish/lib/toSafePathSegment.ts` ์ถ”๊ฐ€ - NFC ์ •๊ทœํ™” ํ›„ ๋ฌธ์ž์™€ ์ˆซ์ž, `-`, `_` ์ด์™ธ์˜ ๋ฌธ์ž๋ฅผ `-` ๋กœ ๋ฐ”๊พธ๊ณ  ์ด์–ด์ง„ `-` ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ๋‹ค - `features/publish/lib/getStorageFolder.ts` ์ถ”๊ฐ€ - `collection/category/title` ์˜ ๊ฐ ์กฐ๊ฐ์„ `toSafePathSegment` ๋กœ ์ •๋ฆฌํ•ด ํด๋” ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ ๋‹ค - `uploadImage.ts` ์™€ `deleteStore.ts` ๊ฐ€ `getStorageFolder` ๋กœ ๊ฐ™์€ ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค - `toStorageFileName.ts` ์˜ ์ •๋ฆฌ ๋‹จ๊ณ„๋ฅผ `toSafePathSegment` ๋กœ ๋ฐ”๊ฟจ๋‹ค - spec: `toSafePathSegment` 7๊ฑด, `getStorageFolder` 3๊ฑด. ์˜ฌ๋ฆฐ ํŒŒ์ผ์˜ ์ฃผ์†Œ์—์„œ `getStoragePaths` ๊ฐ€ ๊บผ๋‚ธ ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ ๊ฒฝ๋กœ์™€ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค - ๐Ÿ’ก ์„ค๊ณ„ ์ด์œ  - #129 ๋Š” ํŒŒ์ผ ์ด๋ฆ„๋งŒ ์ •๋ฆฌํ–ˆ๋‹ค. ํด๋” ์ด๋ฆ„(`title`)์€ `spaceToDash` ๋กœ ๊ณต๋ฐฑ๋งŒ ๋ฐ”๋€Œ์–ด ๊ด„ํ˜ธ๊ฐ€ ๋‚จ๊ณ , `getStoragePaths` ๊ฐ€ `)` ์—์„œ ํด๋”์งธ ์ž˜๋ผ ๊ทธ ๊ธ€์˜ ์ธ๋„ค์ผ๊ณผ ๋ณธ๋ฌธ ์ด๋ฏธ์ง€๊ฐ€ ๋ฐœํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ๋ชจ๋‘ ์ง€์›Œ์กŒ๋‹ค. ์šด์˜์˜ `์Œฉ npm์œผ๋กœ MFE ๊ตฌ์ถ•ํ•˜๊ธฐ (1) : ๊ฐœ๋…` ์ธ๋„ค์ผ์ด 404 ์ธ ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค - ๋ผ์šฐํŠธ๊ฐ€ ๋„˜๊ธฐ๋Š” `title` ์€ Firestore ๋ฌธ์„œ id ๋กœ๋„ ์“ฐ์—ฌ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ž˜์„œ Storage ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“œ๋Š” ๊ณณ์—์„œ๋งŒ ์ •๋ฆฌํ•˜๊ณ , ์˜ฌ๋ฆฌ๋Š” ์ชฝ๊ณผ ์ง€์šฐ๋Š” ์ชฝ์ด ์–ด๊ธ‹๋‚˜์ง€ ์•Š๊ฒŒ ํ•œ ํ•จ์ˆ˜๋ฅผ ํ•จ๊ป˜ ์“ด๋‹ค - ํŒŒ์ผ ์ด๋ฆ„๊ณผ ํด๋” ์ด๋ฆ„์ด ๊ฐ™์€ ๊ทœ์น™์„ ์“ฐ๋„๋ก #129 ์˜ ์น˜ํ™˜์„ `toSafePathSegment` ๋กœ ๋–ผ์—ˆ๋‹ค. ์ œ๋ชฉ์€ ๊ณต๋ฐฑ์ด ์ด๋ฏธ `-` ๋กœ ๋ฐ”๋€Œ์–ด ์žˆ์–ด `-(1)-:-` ๊ฐ€ `---` ์ฒ˜๋Ÿผ ์ด์–ด์ง€๋ฏ€๋กœ ์—ฐ์†๋œ `-` ๋ฅผ ํ•ฉ์นœ๋‹ค - ๊ด„ํ˜ธ๊ฐ€ ๋“  ์˜› ํด๋”๋Š” `deleteStore` ๊ฐ€ ์ƒˆ ํด๋”๋งŒ ๋ณด๋ฏ€๋กœ ๋‚จ๋Š”๋‹ค. ์‚ฌ์šฉ์ž์™€ ํ™•์ธํ•ด ์ด ์ด์Šˆ ๋ฒ”์œ„์—์„œ ๋บ๋‹ค --- src/features/publish/api/deleteStore.ts | 4 ++- src/features/publish/api/uploadImage.ts | 7 ++-- .../publish/lib/getStorageFolder.spec.ts | 35 +++++++++++++++++++ src/features/publish/lib/getStorageFolder.ts | 17 +++++++++ .../publish/lib/toSafePathSegment.spec.ts | 34 ++++++++++++++++++ src/features/publish/lib/toSafePathSegment.ts | 22 ++++++++++++ src/features/publish/lib/toStorageFileName.ts | 9 +++-- 7 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 src/features/publish/lib/getStorageFolder.spec.ts create mode 100644 src/features/publish/lib/getStorageFolder.ts create mode 100644 src/features/publish/lib/toSafePathSegment.spec.ts create mode 100644 src/features/publish/lib/toSafePathSegment.ts diff --git a/src/features/publish/api/deleteStore.ts b/src/features/publish/api/deleteStore.ts index b15f9a7..bc9aef9 100644 --- a/src/features/publish/api/deleteStore.ts +++ b/src/features/publish/api/deleteStore.ts @@ -4,6 +4,8 @@ import Logger from '@/commons/lib/logger'; import { storage } from '@/commons/api/firebase.server'; import { IFireStore } from '@/commons/types/global'; +import getStorageFolder from '../lib/getStorageFolder'; + interface Props extends IFireStore { /** ์ง€์šฐ์ง€ ์•Š์„ ํŒŒ์ผ์˜ Storage ๊ฒฝ๋กœ. ๋ฐฉ๊ธˆ ์ €์žฅํ•œ ๋ฌธ์„œ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ํŒŒ์ผ์ด๋‹ค (`getStoragePaths`) */ keep: Set; @@ -17,7 +19,7 @@ interface Props extends IFireStore { */ export default async function deleteStore(props: Props): Promise { const { category, title, collection, keep } = props; - const folder = `${collection}/${category}/${title}`; + const folder = getStorageFolder({ collection, category, title }); try { const res = await listAll(ref(storage, folder)); diff --git a/src/features/publish/api/uploadImage.ts b/src/features/publish/api/uploadImage.ts index 207967b..88390ac 100644 --- a/src/features/publish/api/uploadImage.ts +++ b/src/features/publish/api/uploadImage.ts @@ -5,6 +5,7 @@ import Logger from '@/commons/lib/logger'; import { storage } from '@/commons/api/firebase.server'; import { IFireStore } from '@/commons/types/global'; +import getStorageFolder from '../lib/getStorageFolder'; import toStorageFileName from '../lib/toStorageFileName'; interface Props extends IFireStore { @@ -63,10 +64,8 @@ export default async function uploadImage(props: Props): Promise { // ์œ ๋‹‰์Šค ํƒ€์ž„ const hashTime = new Date().getTime(); - const collectionRef = ref( - storage, - `${collection}/${category}/${title}/${filename}-${hashTime}.${formatExt}`, - ); + const folder = getStorageFolder({ collection, category, title }); + const collectionRef = ref(storage, `${folder}/${filename}-${hashTime}.${formatExt}`); // firebase์— ์˜ฌ๋ฆฐ ํŒŒ์ผ ์ฃผ์†Œ ์–ป๊ธฐ const imgFirebaseUrl = await uploadBytes(collectionRef, data, metadata).then(async () => { diff --git a/src/features/publish/lib/getStorageFolder.spec.ts b/src/features/publish/lib/getStorageFolder.spec.ts new file mode 100644 index 0000000..4e1de75 --- /dev/null +++ b/src/features/publish/lib/getStorageFolder.spec.ts @@ -0,0 +1,35 @@ +/** + * `getStorageFolder` ๋Š” `uploadImage` ๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์˜ฌ๋ฆฌ๊ณ  `deleteStore` ๊ฐ€ ์˜› ํŒŒ์ผ์„ ์ง€์šฐ๋Š” ํด๋”๋ฅผ ์ •ํ•œ๋‹ค. + * + * ์ œ๋ชฉ์— ๊ด„ํ˜ธ๊ฐ€ ๋“ค์–ด๊ฐ„ ๊ธ€์€ ํด๋” ์ด๋ฆ„์— ๊ด„ํ˜ธ๊ฐ€ ๋‚จ์•„, ๋ฐœํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ๊ทธ ๊ธ€์˜ ์ธ๋„ค์ผ๊ณผ ๋ณธ๋ฌธ ์ด๋ฏธ์ง€๊ฐ€ ๋ชจ๋‘ ์ง€์›Œ์กŒ๋‹ค (#131). + * ์˜ฌ๋ฆฐ ํŒŒ์ผ์˜ ์ฃผ์†Œ์—์„œ `getStoragePaths` ๊ฐ€ ๊บผ๋‚ธ ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ ๊ฒฝ๋กœ์™€ ๊ฐ™์•„์•ผ `deleteStore` ๊ฐ€ ์ƒˆ ํŒŒ์ผ์„ ๋‚จ๊ธด๋‹ค. + */ +import { describe, expect, it } from 'vitest'; + +import getStorageFolder from './getStorageFolder'; +import getStoragePaths from './getStoragePaths'; + +const MFE = { + collection: 'post', + category: 'frontend', + title: '์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-(1)-:-๊ฐœ๋…', +}; + +describe('getStorageFolder ๋Š” collection/category/title ํด๋” ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ ๋‹ค', () => { + it('์ œ๋ชฉ์˜ ๊ด„ํ˜ธ์™€ ์ฝœ๋ก ์„ - ๋กœ ๋ฐ”๊พผ๋‹ค', () => { + expect(getStorageFolder(MFE)).toBe('post/frontend/์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-1-๊ฐœ๋…'); + }); + + it('์•ˆ์ „ํ•œ ์ œ๋ชฉ์€ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค', () => { + expect( + getStorageFolder({ collection: 'project', category: 'team-projects', title: '์œ ํด๋Ÿฌ๋ฒ„์Šค' }), + ).toBe('project/team-projects/์œ ํด๋Ÿฌ๋ฒ„์Šค'); + }); + + it('์˜ฌ๋ฆฐ ํŒŒ์ผ์˜ ์ฃผ์†Œ์—์„œ ๊บผ๋‚ธ ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ ๊ฒฝ๋กœ์™€ ๊ฐ™๋‹ค', () => { + const fullPath = `${getStorageFolder(MFE)}/thumbnail-mfe-1790488653891.webp`; + const url = `https://storage.googleapis.com/jaehan-flow.appspot.com/${encodeURIComponent(fullPath)}`; + + expect(getStoragePaths(url)).toEqual(new Set([fullPath])); + }); +}); diff --git a/src/features/publish/lib/getStorageFolder.ts b/src/features/publish/lib/getStorageFolder.ts new file mode 100644 index 0000000..3045384 --- /dev/null +++ b/src/features/publish/lib/getStorageFolder.ts @@ -0,0 +1,17 @@ +import { type IFireStore } from '@/commons/types/global'; + +import toSafePathSegment from './toSafePathSegment'; + +/** + * @description ๋ฌธ์„œ์˜ ์ด๋ฏธ์ง€๋ฅผ ์˜ฌ๋ฆฌ๊ณ  ์ง€์šฐ๋Š” Storage ํด๋” ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ ๋‹ค. + * `uploadImage` ์™€ `deleteStore` ๊ฐ€ ๊ฐ™์ด ์“ด๋‹ค. ๋‘˜์ด ๋‹ค๋ฅธ ํด๋”๋ฅผ ๋ณด๋ฉด ์ƒˆ ํŒŒ์ผ์ด ์˜› ํŒŒ์ผ๋กœ ์ง€์›Œ์ง€๊ฑฐ๋‚˜ ์˜› ํŒŒ์ผ์ด ๋‚จ๋Š”๋‹ค. + * `title` ์€ Firestore ๋ฌธ์„œ id ๋กœ๋„ ์“ฐ๋Š” ๊ฐ’์ด๋ผ ๊ทธ๋Œ€๋กœ ๋ฐ›๊ณ , Storage ๊ฒฝ๋กœ์—์„œ๋งŒ ์ •๋ฆฌํ•œ๋‹ค (#131) + * @param props ์ปฌ๋ ‰์…˜, ์นดํ…Œ๊ณ ๋ฆฌ, `spaceToDash` ๋ฅผ ์ ์šฉํ•œ ์ œ๋ชฉ + * @returns `collection/category/title` ํ˜•์‹์˜ ํด๋” ๊ฒฝ๋กœ + * @example + * getStorageFolder({ collection: 'post', category: 'frontend', title: '์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-(1)-:-๊ฐœ๋…' }); + * // 'post/frontend/์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-1-๊ฐœ๋…' + */ +export default function getStorageFolder({ collection, category, title }: IFireStore): string { + return [collection, category, title].map(toSafePathSegment).join('/'); +} diff --git a/src/features/publish/lib/toSafePathSegment.spec.ts b/src/features/publish/lib/toSafePathSegment.spec.ts new file mode 100644 index 0000000..8dd5735 --- /dev/null +++ b/src/features/publish/lib/toSafePathSegment.spec.ts @@ -0,0 +1,34 @@ +/** + * `toSafePathSegment` ๋Š” Storage ๊ฒฝ๋กœ์˜ ํด๋” ์ด๋ฆ„๊ณผ ํŒŒ์ผ ์ด๋ฆ„์„ ์ •๋ฆฌํ•˜๋Š” ๊ทœ์น™ ํ•˜๋‚˜๋‹ค. + * + * ์—ฌ๊ธฐ์„œ ๊ด„ํ˜ธ ๊ฐ™์€ ๋ฌธ์ž๊ฐ€ ๋‚จ์œผ๋ฉด `getStoragePaths` ๊ฐ€ ์ฃผ์†Œ์—์„œ ๊ฒฝ๋กœ๋ฅผ ์ž˜๋ชป ์ž˜๋ผ, ๋ฐœํ–‰ ์งํ›„ ๋ฐฉ๊ธˆ ์˜ฌ๋ฆฐ + * ์ด๋ฏธ์ง€๋ฅผ ์˜› ํŒŒ์ผ๋กœ ๋ณด๊ณ  ์ง€์šด๋‹ค. ์‘๋‹ต์€ 200 ์ด๊ณ  ์ด๋ฏธ์ง€๋งŒ ๊นจ์ง€๋ฏ€๋กœ ์กฐ์šฉํžˆ ์‹คํŒจํ•œ๋‹ค (#129, #131). + */ +import { describe, expect, it } from 'vitest'; + +import toSafePathSegment from './toSafePathSegment'; + +describe('toSafePathSegment ๋Š” ๊ฒฝ๋กœ ์กฐ๊ฐ์— ๋ฌธ์ž, ์ˆซ์ž, -, _ ๋งŒ ๋‚จ๊ธด๋‹ค', () => { + it.each([ + ['๊ด„ํ˜ธ', 'team_(1)', 'team_-1-'], + ['์ฝœ๋ก ', 'a:b', 'a-b'], + ['๊ณต๋ฐฑ', 'a b', 'a-b'], + ['์Šฌ๋ž˜์‹œ', 'a/b', 'a-b'], + ])('%s ๋ฅผ - ๋กœ ๋ฐ”๊พผ๋‹ค', (_name, input, expected) => { + expect(toSafePathSegment(input)).toBe(expected); + }); + + it('๋ฐ”๊พผ ๋’ค ์ด์–ด์ง„ - ๋Š” ํ•˜๋‚˜๋กœ ํ•ฉ์นœ๋‹ค', () => { + expect(toSafePathSegment('์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-(1)-:-๊ฐœ๋…')).toBe( + '์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-1-๊ฐœ๋…', + ); + }); + + it('์ด๋ฏธ ์•ˆ์ „ํ•œ ์กฐ๊ฐ์€ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค', () => { + expect(toSafePathSegment('ํ•จ์ˆ˜-ํƒ€์ž…-์„ ์–ธํ•˜๊ธฐ')).toBe('ํ•จ์ˆ˜-ํƒ€์ž…-์„ ์–ธํ•˜๊ธฐ'); + }); + + it('์ž๋ชจ๊ฐ€ ๋‚˜๋‰œ ํ•œ๊ธ€๋„ ํ•ฉ์ณ์„œ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค', () => { + expect(toSafePathSegment('์œ ํด๋Ÿฌ๋ฒ„์Šค'.normalize('NFD'))).toBe('์œ ํด๋Ÿฌ๋ฒ„์Šค'); + }); +}); diff --git a/src/features/publish/lib/toSafePathSegment.ts b/src/features/publish/lib/toSafePathSegment.ts new file mode 100644 index 0000000..69daf4f --- /dev/null +++ b/src/features/publish/lib/toSafePathSegment.ts @@ -0,0 +1,22 @@ +/** ๊ฒฝ๋กœ ์กฐ๊ฐ์— ๋‚จ๊ธธ ๋ฌธ์ž. ์ด ๋ฐ–์˜ ๋ฌธ์ž๊ฐ€ ์ด์–ด์ง€๋ฉด `-` ํ•˜๋‚˜๋กœ ๋ฐ”๊พผ๋‹ค */ +const UNSAFE_CHARS_PATTERN = /[^\p{L}\p{N}_-]+/gu; + +/** + * @description Storage ๊ฒฝ๋กœ ์กฐ๊ฐ(ํด๋” ์ด๋ฆ„, ํŒŒ์ผ ์ด๋ฆ„)์—์„œ ๋ฌธ์ž, ์ˆซ์ž, `-`, `_` ๋งŒ ๋‚จ๊ธฐ๊ณ  ๋‚˜๋จธ์ง€๋ฅผ `-` ๋กœ ๋ฐ”๊พผ๋‹ค. + * ๊ด„ํ˜ธ์ฒ˜๋Ÿผ `encodeURIComponent` ๊ฐ€ ์ธ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ž๊ฐ€ ์ฃผ์†Œ์— ๋‚จ์œผ๋ฉด `getStoragePaths` ๊ฐ€ + * ๊ฒฝ๋กœ๋ฅผ ์ž˜๋ชป ์ž˜๋ผ, ๋ฐฉ๊ธˆ ์˜ฌ๋ฆฐ ํŒŒ์ผ์ด ์˜› ํŒŒ์ผ๋กœ ๋ถ„๋ฅ˜๋˜์–ด ์ง€์›Œ์ง„๋‹ค (#129, #131) + * @param name ๊ฒฝ๋กœ ์กฐ๊ฐ ํ•˜๋‚˜. `/` ๊ฐ€ ๋“ค์–ด ์žˆ์œผ๋ฉด `-` ๋กœ ๋ฐ”๋€๋‹ค + * @returns ์ฃผ์†Œ์™€ Storage ๊ฒฝ๋กœ์—์„œ ๋ชจ์–‘์ด ๊ฐ™์€ ๊ฒฝ๋กœ ์กฐ๊ฐ + * @example + * toSafePathSegment('์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-(1)-:-๊ฐœ๋…'); // '์Œฉ-npm์œผ๋กœ-MFE-๊ตฌ์ถ•ํ•˜๊ธฐ-1-๊ฐœ๋…' + */ +export default function toSafePathSegment(name: string): string { + // ์ž๋ชจ๊ฐ€ ๋‚˜๋‰œ(NFD) ์ด๋ฆ„์€ ๊ฒฐํ•ฉ ๋ฌธ์ž๊ฐ€ `-` ๋กœ ๋ฐ”๋€Œ์ง€ ์•Š๊ฒŒ ๋จผ์ € ํ•ฉ์นœ๋‹ค + return ( + name + .normalize('NFC') + .replace(UNSAFE_CHARS_PATTERN, '-') + // ๊ณต๋ฐฑ์„ `-` ๋กœ ๋ฐ”๊พผ ์ œ๋ชฉ(`-(1)-:-`)์—์„œ `-` ๊ฐ€ ์—ฌ๋Ÿฟ ์ด์–ด์ง€์ง€ ์•Š๊ฒŒ ํ•ฉ์นœ๋‹ค + .replace(/-{2,}/g, '-') + ); +} diff --git a/src/features/publish/lib/toStorageFileName.ts b/src/features/publish/lib/toStorageFileName.ts index 163f2e5..07b52a7 100644 --- a/src/features/publish/lib/toStorageFileName.ts +++ b/src/features/publish/lib/toStorageFileName.ts @@ -1,5 +1,4 @@ -/** ํŒŒ์ผ ์ด๋ฆ„์— ๋‚จ๊ธธ ๋ฌธ์ž. ์ด ๋ฐ–์˜ ๋ฌธ์ž๊ฐ€ ์ด์–ด์ง€๋ฉด `-` ํ•˜๋‚˜๋กœ ๋ฐ”๊พผ๋‹ค */ -const UNSAFE_CHARS_PATTERN = /[^\p{L}\p{N}_-]+/gu; +import toSafePathSegment from './toSafePathSegment'; /** * @description ์ด๋ฏธ์ง€ ์ฃผ์†Œ์—์„œ Storage ์— ์˜ฌ๋ฆด ํŒŒ์ผ ์ด๋ฆ„์„ ๋งŒ๋“ ๋‹ค. @@ -18,9 +17,9 @@ export default function toStorageFileName(src: string): string { // 2. ์ฒซ `.` ์•ž๊นŒ์ง€๋งŒ ๋‚จ๊ฒจ ํ™•์žฅ์ž๋ฅผ ๋—€๋‹ค const basename = lastSegment.split('.')[0]; - // 3. ํผ์„ผํŠธ ์ธ์ฝ”๋”ฉ์„ ํ’€๊ณ , ์ž๋ชจ๊ฐ€ ๋‚˜๋‰œ(NFD) ์ด๋ฆ„์€ ๊ฒฐํ•ฉ ๋ฌธ์ž๊ฐ€ `-` ๋กœ ๋ฐ”๋€Œ์ง€ ์•Š๊ฒŒ ๋จผ์ € ํ•ฉ์นœ๋‹ค - const decoded = decodeURIComponent(basename).normalize('NFC'); + // 3. ํผ์„ผํŠธ ์ธ์ฝ”๋”ฉ์„ ํ‘ผ๋‹ค + const decoded = decodeURIComponent(basename); // 4. ๋ฌธ์ž, ์ˆซ์ž, `-`, `_` ์ด์™ธ์˜ ๋ฌธ์ž๋ฅผ `-` ๋กœ ๋ฐ”๊พผ๋‹ค - return decoded.replace(UNSAFE_CHARS_PATTERN, '-'); + return toSafePathSegment(decoded); } From 939fa9798959e4d88d1cf3ac74ab7eb0ae6038cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:10:50 +0000 Subject: [PATCH 2/3] chore/#132: bump version to 3.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3d908e..d7d1d26 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "sideEffects": true, "type": "module", - "version": "3.0.3", + "version": "3.0.4", "packageManager": "pnpm@9.15.0", "keywords": [ "react-router", From be4b29168487a9c60a287d794eb801f241c37eed Mon Sep 17 00:00:00 2001 From: Wisesaturn Date: Sun, 27 Sep 2026 18:14:44 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs/#131:=20uploadImage=20JSDoc=20?= =?UTF-8?q?=EC=9D=84=20@description=20=EC=9C=BC=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๐Ÿ“‹ ์ž‘์—… ๋‚ด์—ญ - `features/publish/api/uploadImage.ts` ์˜ `@summary` ๋ฅผ `@description` ์œผ๋กœ ๋ฐ”๊พธ๊ณ , ๋น„์–ด ์žˆ๋˜ `@param`, `@returns` ๋ฅผ ์ฑ„์šฐ๊ณ  `@throws` ๋ฅผ ์ถ”๊ฐ€ - ๐Ÿ’ก ์„ค๊ณ„ ์ด์œ  - PR #132 ์ฝ”๋“œ ๋ฆฌ๋ทฐ์—์„œ ๋‚˜์˜จ P4. `jsdoc.md` ๋Š” ๊ธฐ์กด ํŒŒ์ผ์„ ๊ณ ์น  ๋•Œ ๊ทธ ํŒŒ์ผ์˜ `@summary` ๋ฅผ ํ•จ๊ป˜ ๋ฐ”๊พธ๋ผ๊ณ  ํ•œ๋‹ค - ์ฃผ์†Œ๊ฐ€ ๋น„์—ˆ๊ฑฐ๋‚˜ ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹Œ ์‘๋‹ต์ด๋ฉด ์—๋Ÿฌ๋ฅผ ๋˜์ง€๋ฏ€๋กœ `jsdoc.md` ์— ๋”ฐ๋ผ `@throws` ๋ฅผ ์ ์—ˆ๋‹ค --- src/features/publish/api/uploadImage.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/publish/api/uploadImage.ts b/src/features/publish/api/uploadImage.ts index 88390ac..2b481ee 100644 --- a/src/features/publish/api/uploadImage.ts +++ b/src/features/publish/api/uploadImage.ts @@ -13,9 +13,10 @@ interface Props extends IFireStore { } /** - * @summary firebase์— image๋ฅผ ์—…๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜ - * @param props - * @returns + * @description ์›๋ณธ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„ webp(gif ๋Š” ๊ทธ๋Œ€๋กœ)๋กœ ๋ฐ”๊พธ๊ณ  Firebase Storage ์˜ ๋ฌธ์„œ ํด๋”์— ์˜ฌ๋ฆฐ๋‹ค + * @param props ์›๋ณธ ์ด๋ฏธ์ง€ ์ฃผ์†Œ์™€ ๋ฌธ์„œ ํด๋”(collection/category/title) + * @returns ์˜ฌ๋ฆฐ ํŒŒ์ผ์˜ GCP ๊ณต๊ฐœ ์ฃผ์†Œ + * @throws ์ฃผ์†Œ๊ฐ€ ๋น„์—ˆ๊ฑฐ๋‚˜ ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹Œ ์‘๋‹ต์„ ๋ฐ›์œผ๋ฉด ์—๋Ÿฌ */ export default async function uploadImage(props: Props): Promise { const { src, collection, category, title } = props;