@withwiz/cms-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +146 -0
- package/README.md +146 -0
- package/dist/chunk-2REYWBQ6.js +32 -0
- package/dist/chunk-2REYWBQ6.js.map +1 -0
- package/dist/chunk-2SYUEQ6J.mjs +17 -0
- package/dist/chunk-2SYUEQ6J.mjs.map +1 -0
- package/dist/chunk-3ILEMZAL.mjs +32 -0
- package/dist/chunk-3ILEMZAL.mjs.map +1 -0
- package/dist/chunk-3Y2TQCHU.mjs +33 -0
- package/dist/chunk-3Y2TQCHU.mjs.map +1 -0
- package/dist/chunk-4LSFAAZW.js +1 -0
- package/dist/chunk-4LSFAAZW.js.map +1 -0
- package/dist/chunk-4W3X7OKF.js +100 -0
- package/dist/chunk-4W3X7OKF.js.map +1 -0
- package/dist/chunk-5JUIMXNF.js +108 -0
- package/dist/chunk-5JUIMXNF.js.map +1 -0
- package/dist/chunk-5RDMODGV.mjs +34 -0
- package/dist/chunk-5RDMODGV.mjs.map +1 -0
- package/dist/chunk-6ES4W2DL.mjs +1 -0
- package/dist/chunk-6ES4W2DL.mjs.map +1 -0
- package/dist/chunk-6EZ3QE5N.js +71 -0
- package/dist/chunk-6EZ3QE5N.js.map +1 -0
- package/dist/chunk-6JFERXNE.mjs +16 -0
- package/dist/chunk-6JFERXNE.mjs.map +1 -0
- package/dist/chunk-7SWW3ZIN.js +17 -0
- package/dist/chunk-7SWW3ZIN.js.map +1 -0
- package/dist/chunk-AXR56AUD.mjs +1 -0
- package/dist/chunk-AXR56AUD.mjs.map +1 -0
- package/dist/chunk-BE7JDL64.js +154 -0
- package/dist/chunk-BE7JDL64.js.map +1 -0
- package/dist/chunk-BHN2SRZC.js +51 -0
- package/dist/chunk-BHN2SRZC.js.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +11 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/chunk-BWKKHCDG.mjs +164 -0
- package/dist/chunk-BWKKHCDG.mjs.map +1 -0
- package/dist/chunk-C5WFZJM4.js +218 -0
- package/dist/chunk-C5WFZJM4.js.map +1 -0
- package/dist/chunk-CGVFB23E.mjs +123 -0
- package/dist/chunk-CGVFB23E.mjs.map +1 -0
- package/dist/chunk-D2VDTRXO.js +1 -0
- package/dist/chunk-D2VDTRXO.js.map +1 -0
- package/dist/chunk-D75ADHVM.mjs +181 -0
- package/dist/chunk-D75ADHVM.mjs.map +1 -0
- package/dist/chunk-DCIF4XJL.js +309 -0
- package/dist/chunk-DCIF4XJL.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DSORCGJK.js +44 -0
- package/dist/chunk-DSORCGJK.js.map +1 -0
- package/dist/chunk-E5YUAOJI.js +27 -0
- package/dist/chunk-E5YUAOJI.js.map +1 -0
- package/dist/chunk-EG6NVPP2.js +164 -0
- package/dist/chunk-EG6NVPP2.js.map +1 -0
- package/dist/chunk-EL7YGOTY.js +1 -0
- package/dist/chunk-EL7YGOTY.js.map +1 -0
- package/dist/chunk-EM4MQEWV.mjs +100 -0
- package/dist/chunk-EM4MQEWV.mjs.map +1 -0
- package/dist/chunk-FCCCG5Z7.mjs +108 -0
- package/dist/chunk-FCCCG5Z7.mjs.map +1 -0
- package/dist/chunk-FEPGJ7WN.mjs +218 -0
- package/dist/chunk-FEPGJ7WN.mjs.map +1 -0
- package/dist/chunk-HXVWBFCN.js +16 -0
- package/dist/chunk-HXVWBFCN.js.map +1 -0
- package/dist/chunk-I5YMTJV6.mjs +51 -0
- package/dist/chunk-I5YMTJV6.mjs.map +1 -0
- package/dist/chunk-IMNWW7Q5.mjs +13 -0
- package/dist/chunk-IMNWW7Q5.mjs.map +1 -0
- package/dist/chunk-J5N76ASB.mjs +114 -0
- package/dist/chunk-J5N76ASB.mjs.map +1 -0
- package/dist/chunk-JBV26CUG.mjs +51 -0
- package/dist/chunk-JBV26CUG.mjs.map +1 -0
- package/dist/chunk-JMWXVXK5.js +53 -0
- package/dist/chunk-JMWXVXK5.js.map +1 -0
- package/dist/chunk-KWKV2GAH.js +60 -0
- package/dist/chunk-KWKV2GAH.js.map +1 -0
- package/dist/chunk-LDFE33ML.mjs +53 -0
- package/dist/chunk-LDFE33ML.mjs.map +1 -0
- package/dist/chunk-LF5LOI7K.mjs +174 -0
- package/dist/chunk-LF5LOI7K.mjs.map +1 -0
- package/dist/chunk-MMKAMH4U.mjs +16 -0
- package/dist/chunk-MMKAMH4U.mjs.map +1 -0
- package/dist/chunk-MUHFEEYW.js +33 -0
- package/dist/chunk-MUHFEEYW.js.map +1 -0
- package/dist/chunk-MUQYK5EO.js +1 -0
- package/dist/chunk-MUQYK5EO.js.map +1 -0
- package/dist/chunk-OBNALAMG.mjs +71 -0
- package/dist/chunk-OBNALAMG.mjs.map +1 -0
- package/dist/chunk-OLEYNPW6.js +25 -0
- package/dist/chunk-OLEYNPW6.js.map +1 -0
- package/dist/chunk-OYJRR3G2.js +123 -0
- package/dist/chunk-OYJRR3G2.js.map +1 -0
- package/dist/chunk-PLVG7AW3.js +16 -0
- package/dist/chunk-PLVG7AW3.js.map +1 -0
- package/dist/chunk-PYD4CES4.js +1 -0
- package/dist/chunk-PYD4CES4.js.map +1 -0
- package/dist/chunk-QACD2U6P.mjs +1 -0
- package/dist/chunk-QACD2U6P.mjs.map +1 -0
- package/dist/chunk-R5GGIVD4.js +10 -0
- package/dist/chunk-R5GGIVD4.js.map +1 -0
- package/dist/chunk-RCK5WCU5.mjs +25 -0
- package/dist/chunk-RCK5WCU5.mjs.map +1 -0
- package/dist/chunk-S6UUSM7S.js +13 -0
- package/dist/chunk-S6UUSM7S.js.map +1 -0
- package/dist/chunk-S7GOXCRM.js +114 -0
- package/dist/chunk-S7GOXCRM.js.map +1 -0
- package/dist/chunk-SMV5D5LO.mjs +28 -0
- package/dist/chunk-SMV5D5LO.mjs.map +1 -0
- package/dist/chunk-T7EVFD4G.mjs +60 -0
- package/dist/chunk-T7EVFD4G.mjs.map +1 -0
- package/dist/chunk-TLH3V4SY.mjs +1 -0
- package/dist/chunk-TLH3V4SY.mjs.map +1 -0
- package/dist/chunk-TUQ2CVAK.mjs +10 -0
- package/dist/chunk-TUQ2CVAK.mjs.map +1 -0
- package/dist/chunk-U6PVJKTB.js +51 -0
- package/dist/chunk-U6PVJKTB.js.map +1 -0
- package/dist/chunk-UXMMEEX4.mjs +27 -0
- package/dist/chunk-UXMMEEX4.mjs.map +1 -0
- package/dist/chunk-VVXNFFKI.mjs +44 -0
- package/dist/chunk-VVXNFFKI.mjs.map +1 -0
- package/dist/chunk-VZZ3RU3Q.js +28 -0
- package/dist/chunk-VZZ3RU3Q.js.map +1 -0
- package/dist/chunk-W4FWIZ2Y.js +34 -0
- package/dist/chunk-W4FWIZ2Y.js.map +1 -0
- package/dist/chunk-YEQFKOWD.mjs +154 -0
- package/dist/chunk-YEQFKOWD.mjs.map +1 -0
- package/dist/chunk-YPBKY4KY.mjs +1 -0
- package/dist/chunk-YPBKY4KY.mjs.map +1 -0
- package/dist/chunk-YY7K5CXX.js +174 -0
- package/dist/chunk-YY7K5CXX.js.map +1 -0
- package/dist/chunk-ZFORODOX.js +181 -0
- package/dist/chunk-ZFORODOX.js.map +1 -0
- package/dist/chunk-ZULSFSDG.mjs +309 -0
- package/dist/chunk-ZULSFSDG.mjs.map +1 -0
- package/dist/components/AdminManagerBase.d.mts +26 -0
- package/dist/components/AdminManagerBase.d.ts +26 -0
- package/dist/components/AdminManagerBase.js +13 -0
- package/dist/components/AdminManagerBase.js.map +1 -0
- package/dist/components/AdminManagerBase.mjs +12 -0
- package/dist/components/AdminManagerBase.mjs.map +1 -0
- package/dist/components/AdminManagerConfig.d.mts +83 -0
- package/dist/components/AdminManagerConfig.d.ts +83 -0
- package/dist/components/AdminManagerConfig.js +2 -0
- package/dist/components/AdminManagerConfig.js.map +1 -0
- package/dist/components/AdminManagerConfig.mjs +2 -0
- package/dist/components/AdminManagerConfig.mjs.map +1 -0
- package/dist/components/AdminShell.d.mts +30 -0
- package/dist/components/AdminShell.d.ts +30 -0
- package/dist/components/AdminShell.js +12 -0
- package/dist/components/AdminShell.js.map +1 -0
- package/dist/components/AdminShell.mjs +11 -0
- package/dist/components/AdminShell.mjs.map +1 -0
- package/dist/components/JsonLd.d.mts +32 -0
- package/dist/components/JsonLd.d.ts +32 -0
- package/dist/components/JsonLd.js +11 -0
- package/dist/components/JsonLd.js.map +1 -0
- package/dist/components/JsonLd.mjs +11 -0
- package/dist/components/JsonLd.mjs.map +1 -0
- package/dist/components/ResizableImage.d.mts +18 -0
- package/dist/components/ResizableImage.d.ts +18 -0
- package/dist/components/ResizableImage.js +10 -0
- package/dist/components/ResizableImage.js.map +1 -0
- package/dist/components/ResizableImage.mjs +9 -0
- package/dist/components/ResizableImage.mjs.map +1 -0
- package/dist/components/ToggleSwitch.d.mts +11 -0
- package/dist/components/ToggleSwitch.d.ts +11 -0
- package/dist/components/ToggleSwitch.js +10 -0
- package/dist/components/ToggleSwitch.js.map +1 -0
- package/dist/components/ToggleSwitch.mjs +9 -0
- package/dist/components/ToggleSwitch.mjs.map +1 -0
- package/dist/components/image-drop-zone.css +115 -0
- package/dist/components/index.d.mts +26 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.js +35 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +35 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/components/toggle-switch.css +55 -0
- package/dist/hooks/index.d.mts +45 -0
- package/dist/hooks/index.d.ts +45 -0
- package/dist/hooks/index.js +25 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +25 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/hooks/useImageDropZone.d.mts +37 -0
- package/dist/hooks/useImageDropZone.d.ts +37 -0
- package/dist/hooks/useImageDropZone.js +13 -0
- package/dist/hooks/useImageDropZone.js.map +1 -0
- package/dist/hooks/useImageDropZone.mjs +12 -0
- package/dist/hooks/useImageDropZone.mjs.map +1 -0
- package/dist/hooks/useScrollReveal.d.mts +6 -0
- package/dist/hooks/useScrollReveal.d.ts +6 -0
- package/dist/hooks/useScrollReveal.js +12 -0
- package/dist/hooks/useScrollReveal.js.map +1 -0
- package/dist/hooks/useScrollReveal.mjs +11 -0
- package/dist/hooks/useScrollReveal.mjs.map +1 -0
- package/dist/image-drop-zone.css +115 -0
- package/dist/index-00a40-FQ.d.mts +136 -0
- package/dist/index-00a40-FQ.d.ts +136 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +169 -0
- package/dist/index.mjs.map +1 -0
- package/dist/infrastructure/index.d.mts +4 -0
- package/dist/infrastructure/index.d.ts +4 -0
- package/dist/infrastructure/index.js +20 -0
- package/dist/infrastructure/index.js.map +1 -0
- package/dist/infrastructure/index.mjs +20 -0
- package/dist/infrastructure/index.mjs.map +1 -0
- package/dist/infrastructure/middleware/index.d.mts +3 -0
- package/dist/infrastructure/middleware/index.d.ts +3 -0
- package/dist/infrastructure/middleware/index.js +15 -0
- package/dist/infrastructure/middleware/index.js.map +1 -0
- package/dist/infrastructure/middleware/index.mjs +15 -0
- package/dist/infrastructure/middleware/index.mjs.map +1 -0
- package/dist/infrastructure/middleware/wrappers.d.mts +10 -0
- package/dist/infrastructure/middleware/wrappers.d.ts +10 -0
- package/dist/infrastructure/middleware/wrappers.js +15 -0
- package/dist/infrastructure/middleware/wrappers.js.map +1 -0
- package/dist/infrastructure/middleware/wrappers.mjs +15 -0
- package/dist/infrastructure/middleware/wrappers.mjs.map +1 -0
- package/dist/infrastructure/prisma.d.mts +5 -0
- package/dist/infrastructure/prisma.d.ts +5 -0
- package/dist/infrastructure/prisma.js +12 -0
- package/dist/infrastructure/prisma.js.map +1 -0
- package/dist/infrastructure/prisma.mjs +12 -0
- package/dist/infrastructure/prisma.mjs.map +1 -0
- package/dist/services/index.d.mts +22 -0
- package/dist/services/index.d.ts +22 -0
- package/dist/services/index.js +36 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/index.mjs +36 -0
- package/dist/services/index.mjs.map +1 -0
- package/dist/toggle-switch.css +55 -0
- package/dist/types/common.d.mts +14 -0
- package/dist/types/common.d.ts +14 -0
- package/dist/types/common.js +8 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/common.mjs +8 -0
- package/dist/types/common.mjs.map +1 -0
- package/dist/types/index.d.mts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +9 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/utils/admin-fetch.d.mts +11 -0
- package/dist/utils/admin-fetch.d.ts +11 -0
- package/dist/utils/admin-fetch.js +13 -0
- package/dist/utils/admin-fetch.js.map +1 -0
- package/dist/utils/admin-fetch.mjs +13 -0
- package/dist/utils/admin-fetch.mjs.map +1 -0
- package/dist/utils/api-helpers.d.mts +20 -0
- package/dist/utils/api-helpers.d.ts +20 -0
- package/dist/utils/api-helpers.js +12 -0
- package/dist/utils/api-helpers.js.map +1 -0
- package/dist/utils/api-helpers.mjs +12 -0
- package/dist/utils/api-helpers.mjs.map +1 -0
- package/dist/utils/date.d.mts +5 -0
- package/dist/utils/date.d.ts +5 -0
- package/dist/utils/date.js +12 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.mjs +12 -0
- package/dist/utils/date.mjs.map +1 -0
- package/dist/utils/html-sanitizer.d.mts +56 -0
- package/dist/utils/html-sanitizer.d.ts +56 -0
- package/dist/utils/html-sanitizer.js +11 -0
- package/dist/utils/html-sanitizer.js.map +1 -0
- package/dist/utils/html-sanitizer.mjs +11 -0
- package/dist/utils/html-sanitizer.mjs.map +1 -0
- package/dist/utils/image-variant-utils.d.mts +14 -0
- package/dist/utils/image-variant-utils.d.ts +14 -0
- package/dist/utils/image-variant-utils.js +10 -0
- package/dist/utils/image-variant-utils.js.map +1 -0
- package/dist/utils/image-variant-utils.mjs +10 -0
- package/dist/utils/image-variant-utils.mjs.map +1 -0
- package/dist/utils/image-variants.d.mts +13 -0
- package/dist/utils/image-variants.d.ts +13 -0
- package/dist/utils/image-variants.js +12 -0
- package/dist/utils/image-variants.js.map +1 -0
- package/dist/utils/image-variants.mjs +12 -0
- package/dist/utils/image-variants.mjs.map +1 -0
- package/dist/utils/index.d.mts +88 -0
- package/dist/utils/index.d.ts +88 -0
- package/dist/utils/index.js +92 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +92 -0
- package/dist/utils/index.mjs.map +1 -0
- package/dist/utils/jwt.d.mts +19 -0
- package/dist/utils/jwt.d.ts +19 -0
- package/dist/utils/jwt.js +9 -0
- package/dist/utils/jwt.js.map +1 -0
- package/dist/utils/jwt.mjs +9 -0
- package/dist/utils/jwt.mjs.map +1 -0
- package/dist/utils/r2-helpers.d.mts +9 -0
- package/dist/utils/r2-helpers.d.ts +9 -0
- package/dist/utils/r2-helpers.js +20 -0
- package/dist/utils/r2-helpers.js.map +1 -0
- package/dist/utils/r2-helpers.mjs +20 -0
- package/dist/utils/r2-helpers.mjs.map +1 -0
- package/dist/utils/r2-storage.d.mts +38 -0
- package/dist/utils/r2-storage.d.ts +38 -0
- package/dist/utils/r2-storage.js +17 -0
- package/dist/utils/r2-storage.js.map +1 -0
- package/dist/utils/r2-storage.mjs +17 -0
- package/dist/utils/r2-storage.mjs.map +1 -0
- package/dist/utils/route-params.d.mts +3 -0
- package/dist/utils/route-params.d.ts +3 -0
- package/dist/utils/route-params.js +8 -0
- package/dist/utils/route-params.js.map +1 -0
- package/dist/utils/route-params.mjs +8 -0
- package/dist/utils/route-params.mjs.map +1 -0
- package/dist/validators/index.d.mts +2 -0
- package/dist/validators/index.d.ts +2 -0
- package/dist/validators/index.js +11 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/index.mjs +11 -0
- package/dist/validators/index.mjs.map +1 -0
- package/dist/validators/shared.d.mts +6 -0
- package/dist/validators/shared.d.ts +6 -0
- package/dist/validators/shared.js +10 -0
- package/dist/validators/shared.js.map +1 -0
- package/dist/validators/shared.mjs +10 -0
- package/dist/validators/shared.mjs.map +1 -0
- package/package.json +257 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
[English](./README.md) | [한국어](./README.ko.md)
|
|
2
|
+
|
|
3
|
+
# @withwiz/cms-kit
|
|
4
|
+
|
|
5
|
+
**Performance Management System** — Next.js + React 기반 웹 어드민 패널을 위한 CMS 프레임워크 패키지.
|
|
6
|
+
|
|
7
|
+
Withwiz 프로젝트들의 어드민 공통 레이어(인프라, 베이스 서비스, 공용 UI 컴포넌트, 훅, 유틸리티, 검증기)를 모아 둔 패키지입니다. 도메인 특화 코드(뉴스·공연·아티스트 등)는 애플리케이션의 `src/`에 유지하면서, 이 패키지가 제공하는 베이스 구조물을 재사용하도록 설계되었습니다.
|
|
8
|
+
|
|
9
|
+
## 주요 특징
|
|
10
|
+
|
|
11
|
+
- **AdminManagerBase** — 목록 / 편집 / 프리뷰 3분할 어드민 페이지 스캐폴드
|
|
12
|
+
- **AdminShell** — 인증 · 사이드바 · 레이아웃을 포함한 어드민 전체 셸
|
|
13
|
+
- **미들웨어 래퍼** — `withPublicApi` / `withAuthApi` / `withAdminApi` (`@withwiz/toolkit` 기반 Next.js 타입 호환 레이어)
|
|
14
|
+
- **베이스 서비스** — Prisma 의존성 주입, 페이지네이션, HTML 새니타이저, R2 키 수집/삭제
|
|
15
|
+
- **이미지 파이프라인** — R2 업로드 시 `lg` / `md` / `sm` / `thumb` WebP variant 자동 생성
|
|
16
|
+
- **공용 훅** — `useAdminList`, `useAdminForm`, `useImageDropZone`, `useScrollReveal`
|
|
17
|
+
- **보안 하드닝** — DOMPurify 기반 새니타이저, JSON-LD 이스케이프, JWT 시크릿 fail-fast 정책, 안전한 rate-limit identity 추출
|
|
18
|
+
- **설정 경계** — brand · routes · JWT · sanitizer · storage · rate-limit identity 를 `setCmsConfig` 로 주입
|
|
19
|
+
|
|
20
|
+
## 기술 스택
|
|
21
|
+
|
|
22
|
+
- TypeScript (strict) — `target: ES2022`, `module: ESNext`
|
|
23
|
+
- React 19 / Next.js 16 (peer dependency, `>=18` / `>=15` 지원)
|
|
24
|
+
- Tiptap 3 (리치 텍스트 에디터)
|
|
25
|
+
- Zod 4 (검증)
|
|
26
|
+
- tsup (빌드) + Vitest (테스트)
|
|
27
|
+
- 옵셔널 피어: `@aws-sdk/client-s3`, `sharp`, `isomorphic-dompurify`, `sonner`, `@tanstack/react-virtual`
|
|
28
|
+
|
|
29
|
+
## 설치
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @withwiz/cms-kit
|
|
33
|
+
# 또는
|
|
34
|
+
pnpm add @withwiz/cms-kit
|
|
35
|
+
# 또는
|
|
36
|
+
yarn add @withwiz/cms-kit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`@withwiz/toolkit` 은 peer dependency 입니다 (`>=0.7.1`). 패키지 매니저와 해석 전략에 따라 명시 설치가 필요할 수 있습니다.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @withwiz/toolkit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
모노레포 내부 개발 시에는 `file:` 프로토콜도 지원합니다.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@withwiz/cms-kit": "file:packages/cms-kit"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 진입점 (Exports)
|
|
56
|
+
|
|
57
|
+
| 경로 | 설명 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `@withwiz/cms-kit` | 전체 barrel export |
|
|
60
|
+
| `@withwiz/cms-kit/components` | `AdminShell`, `AdminManagerBase`, `ImageDropUpload`, `ToggleSwitch` 등 |
|
|
61
|
+
| `@withwiz/cms-kit/hooks` | `useAdminList`, `useAdminForm`, `useImageDropZone`, `useScrollReveal` |
|
|
62
|
+
| `@withwiz/cms-kit/infrastructure` | Prisma proxy, 미들웨어 래퍼 |
|
|
63
|
+
| `@withwiz/cms-kit/infrastructure/middleware` | `withPublicApi` / `withAuthApi` / `withAdminApi` |
|
|
64
|
+
| `@withwiz/cms-kit/services` | `base-service`, pagination |
|
|
65
|
+
| `@withwiz/cms-kit/types` | `PaginatedResult`, `SortOrder` |
|
|
66
|
+
| `@withwiz/cms-kit/utils` | `adminFetch`, `r2-storage`, `image-variants`, `jwt`, `date`, `html-sanitizer` |
|
|
67
|
+
| `@withwiz/cms-kit/validators` | `slugSchema`, `optionalUrlSchema` |
|
|
68
|
+
|
|
69
|
+
## 사용법
|
|
70
|
+
|
|
71
|
+
### 1. Prisma 클라이언트 주입
|
|
72
|
+
|
|
73
|
+
패키지는 Prisma 스키마를 모릅니다. 애플리케이션 부트스트랩 시점에 주입해야 합니다.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/lib/prisma.ts
|
|
77
|
+
import { PrismaClient } from '@prisma/client';
|
|
78
|
+
import { setPrismaClient } from '@withwiz/cms-kit/infrastructure';
|
|
79
|
+
|
|
80
|
+
const prisma = new PrismaClient();
|
|
81
|
+
setPrismaClient(prisma);
|
|
82
|
+
export { prisma };
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
주입 전에 proxy 를 호출하면 `Error('Prisma client not initialized')` 가 발생합니다.
|
|
86
|
+
|
|
87
|
+
### 2. 패키지 설정 경계 구성
|
|
88
|
+
|
|
89
|
+
소비자 고유 값(brand, route, JWT 시크릿, 신뢰 새니타이저 origin, 스토리지 공개 URL, rate-limit identity)은 하드코딩하지 않고 모두 주입합니다.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { setCmsConfig } from '@withwiz/cms-kit/utils';
|
|
93
|
+
|
|
94
|
+
setCmsConfig({
|
|
95
|
+
brand: { brandLabel: 'ACME', navItems: [{ label: 'Home', href: '/x', glyph: 'H' }] },
|
|
96
|
+
routes: { loginPath: '/signin', uploadEndpoint: '/files/upload' },
|
|
97
|
+
jwt: { secret: process.env.MY_JWT_SECRET },
|
|
98
|
+
sanitizer: { trustedIframeOrigins: ['https://www.loom.com/'] },
|
|
99
|
+
storage: { publicBaseUrl: 'https://cdn.example.com' },
|
|
100
|
+
rateLimit: { identityExtractor: (h) => myTrustedClientIp(h) },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
해석 우선순위 (모든 표면 동일):
|
|
105
|
+
|
|
106
|
+
1. 명시적 주입
|
|
107
|
+
2. 레거시 환경변수 (현재 이름 유지)
|
|
108
|
+
3. 내장 기본값
|
|
109
|
+
|
|
110
|
+
실패는 lazy 하게 발생하여 import 시점이 아닌 자원 사용 시점에 드러납니다. JWT 시크릿 누락은 **fail-fast** 에러입니다(서명 키에는 안전한 기본값이 없습니다).
|
|
111
|
+
|
|
112
|
+
### 3. 매니저 페이지 구성
|
|
113
|
+
|
|
114
|
+
`AdminManagerBase` 와 공용 훅을 조합하여 목록 / 편집 / 프리뷰 3분할 어드민 페이지를 만듭니다.
|
|
115
|
+
|
|
116
|
+
## 스크립트
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run build # tsup 빌드 → dist/
|
|
120
|
+
npm test # vitest run
|
|
121
|
+
npm run test:watch # vitest watch 모드
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 의존성 규칙
|
|
125
|
+
|
|
126
|
+
- `src/` (애플리케이션) → `@withwiz/cms-kit/*` ✓
|
|
127
|
+
- `@withwiz/cms-kit` → `@withwiz/toolkit/*` ✓
|
|
128
|
+
- `@withwiz/cms-kit` → `src/` ✗ (패키지 독립 유지)
|
|
129
|
+
|
|
130
|
+
전체 레이어링 다이어그램은 [docs/architecture.md](./docs/architecture.md) 참고.
|
|
131
|
+
|
|
132
|
+
## 문서
|
|
133
|
+
|
|
134
|
+
- [docs/architecture.md](./docs/architecture.md) — 패키지 구조와 모듈 경계
|
|
135
|
+
- [docs/components.md](./docs/components.md) — UI 컴포넌트 레퍼런스
|
|
136
|
+
- [docs/hooks.md](./docs/hooks.md) — React 훅 레퍼런스
|
|
137
|
+
- [docs/services.md](./docs/services.md) — 베이스 서비스와 페이지네이션
|
|
138
|
+
- [docs/infrastructure.md](./docs/infrastructure.md) — 미들웨어 래퍼와 Prisma DI
|
|
139
|
+
- [docs/utils.md](./docs/utils.md) — 유틸리티 함수 레퍼런스
|
|
140
|
+
- [docs/validators.md](./docs/validators.md) — 공용 Zod 스키마
|
|
141
|
+
- [docs/testing.md](./docs/testing.md) — 테스트 전략과 실행 방법
|
|
142
|
+
- [docs/plans/](./docs/plans/) — 패키지 설계 / 리팩토링 계획 이력
|
|
143
|
+
|
|
144
|
+
## 라이선스
|
|
145
|
+
|
|
146
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
[English](./README.md) | [한국어](./README.ko.md)
|
|
2
|
+
|
|
3
|
+
# @withwiz/cms-kit
|
|
4
|
+
|
|
5
|
+
**Performance Management System** — a CMS framework package for building web admin panels with Next.js and React.
|
|
6
|
+
|
|
7
|
+
`@withwiz/cms-kit` consolidates the common admin layer (infrastructure, base services, shared UI components, hooks, utilities, and validators) used across Withwiz projects. Domain-specific code (news, performances, artists, etc.) stays in your application's `src/`, while this package provides the reusable scaffolding underneath.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **AdminManagerBase** — three-pane (list / editor / preview) scaffold for admin pages
|
|
12
|
+
- **AdminShell** — complete admin shell with authentication, sidebar, and layout
|
|
13
|
+
- **Middleware wrappers** — `withPublicApi` / `withAuthApi` / `withAdminApi`, a Next.js–typed compatibility layer over `@withwiz/toolkit`
|
|
14
|
+
- **Base services** — Prisma dependency injection, pagination, HTML sanitizer, R2 key collection/deletion
|
|
15
|
+
- **Image pipeline** — automatic `lg` / `md` / `sm` / `thumb` WebP variant generation on R2 upload
|
|
16
|
+
- **Shared hooks** — `useAdminList`, `useAdminForm`, `useImageDropZone`, `useScrollReveal`
|
|
17
|
+
- **Security hardening** — DOMPurify-based sanitization, JSON-LD escaping, fail-fast JWT secret policy, safe rate-limit identity extraction
|
|
18
|
+
- **Config boundary** — explicit `setCmsConfig` injection for brand, routes, JWT, sanitizer, storage, and rate-limit identity
|
|
19
|
+
|
|
20
|
+
## Tech Stack
|
|
21
|
+
|
|
22
|
+
- TypeScript (strict) — `target: ES2022`, `module: ESNext`
|
|
23
|
+
- React 19 / Next.js 16 (peer dependencies, `>=18` / `>=15` supported)
|
|
24
|
+
- Tiptap 3 (rich text editing)
|
|
25
|
+
- Zod 4 (validation)
|
|
26
|
+
- tsup (build) + Vitest (test)
|
|
27
|
+
- Optional peers: `@aws-sdk/client-s3`, `sharp`, `isomorphic-dompurify`, `sonner`, `@tanstack/react-virtual`
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @withwiz/cms-kit
|
|
33
|
+
# or
|
|
34
|
+
pnpm add @withwiz/cms-kit
|
|
35
|
+
# or
|
|
36
|
+
yarn add @withwiz/cms-kit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`@withwiz/toolkit` is a peer dependency (`>=0.7.1`). Depending on your package manager and resolution strategy, you may need to install it explicitly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @withwiz/toolkit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For monorepo development the `file:` protocol is also supported:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@withwiz/cms-kit": "file:packages/cms-kit"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Entry Points
|
|
56
|
+
|
|
57
|
+
| Path | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `@withwiz/cms-kit` | Full barrel export |
|
|
60
|
+
| `@withwiz/cms-kit/components` | `AdminShell`, `AdminManagerBase`, `ImageDropUpload`, `ToggleSwitch`, … |
|
|
61
|
+
| `@withwiz/cms-kit/hooks` | `useAdminList`, `useAdminForm`, `useImageDropZone`, `useScrollReveal` |
|
|
62
|
+
| `@withwiz/cms-kit/infrastructure` | Prisma proxy, middleware wrappers |
|
|
63
|
+
| `@withwiz/cms-kit/infrastructure/middleware` | `withPublicApi` / `withAuthApi` / `withAdminApi` |
|
|
64
|
+
| `@withwiz/cms-kit/services` | `base-service`, pagination |
|
|
65
|
+
| `@withwiz/cms-kit/types` | `PaginatedResult`, `SortOrder` |
|
|
66
|
+
| `@withwiz/cms-kit/utils` | `adminFetch`, `r2-storage`, `image-variants`, `jwt`, `date`, `html-sanitizer` |
|
|
67
|
+
| `@withwiz/cms-kit/validators` | `slugSchema`, `optionalUrlSchema` |
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### 1. Inject the Prisma client
|
|
72
|
+
|
|
73
|
+
The package does not know your Prisma schema; inject the client during application bootstrap.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/lib/prisma.ts
|
|
77
|
+
import { PrismaClient } from '@prisma/client';
|
|
78
|
+
import { setPrismaClient } from '@withwiz/cms-kit/infrastructure';
|
|
79
|
+
|
|
80
|
+
const prisma = new PrismaClient();
|
|
81
|
+
setPrismaClient(prisma);
|
|
82
|
+
export { prisma };
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Calling the proxy before injection throws `Error('Prisma client not initialized')`.
|
|
86
|
+
|
|
87
|
+
### 2. Configure the package boundary
|
|
88
|
+
|
|
89
|
+
Consumer-specific values (brand, routes, JWT secret, trusted sanitizer origins, storage public URL, rate-limit identity) are injected — never hard-coded.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { setCmsConfig } from '@withwiz/cms-kit/utils';
|
|
93
|
+
|
|
94
|
+
setCmsConfig({
|
|
95
|
+
brand: { brandLabel: 'ACME', navItems: [{ label: 'Home', href: '/x', glyph: 'H' }] },
|
|
96
|
+
routes: { loginPath: '/signin', uploadEndpoint: '/files/upload' },
|
|
97
|
+
jwt: { secret: process.env.MY_JWT_SECRET },
|
|
98
|
+
sanitizer: { trustedIframeOrigins: ['https://www.loom.com/'] },
|
|
99
|
+
storage: { publicBaseUrl: 'https://cdn.example.com' },
|
|
100
|
+
rateLimit: { identityExtractor: (h) => myTrustedClientIp(h) },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Resolution order (uniform across all surfaces):
|
|
105
|
+
|
|
106
|
+
1. Explicit injection
|
|
107
|
+
2. Legacy environment variables (current names preserved)
|
|
108
|
+
3. Built-in defaults
|
|
109
|
+
|
|
110
|
+
Failures are lazy and surface at point of use, not on import. Missing JWT secret is a **fail-fast** error (no safe default for signing keys).
|
|
111
|
+
|
|
112
|
+
### 3. Build a manager page
|
|
113
|
+
|
|
114
|
+
Use `AdminManagerBase` plus the shared hooks to assemble a three-pane admin page with list, editor, and preview.
|
|
115
|
+
|
|
116
|
+
## Scripts
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run build # tsup build → dist/
|
|
120
|
+
npm test # vitest run
|
|
121
|
+
npm run test:watch # vitest in watch mode
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Dependency Rules
|
|
125
|
+
|
|
126
|
+
- `src/` (your app) → `@withwiz/cms-kit/*` ✓
|
|
127
|
+
- `@withwiz/cms-kit` → `@withwiz/toolkit/*` ✓
|
|
128
|
+
- `@withwiz/cms-kit` → `src/` ✗ (keep the package independent)
|
|
129
|
+
|
|
130
|
+
See [docs/architecture.md](./docs/architecture.md) for the full layering diagram.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- [docs/architecture.md](./docs/architecture.md) — Package structure and module boundaries
|
|
135
|
+
- [docs/components.md](./docs/components.md) — UI component reference
|
|
136
|
+
- [docs/hooks.md](./docs/hooks.md) — React hooks reference
|
|
137
|
+
- [docs/services.md](./docs/services.md) — Base service and pagination
|
|
138
|
+
- [docs/infrastructure.md](./docs/infrastructure.md) — Middleware wrappers and Prisma DI
|
|
139
|
+
- [docs/utils.md](./docs/utils.md) — Utility function reference
|
|
140
|
+
- [docs/validators.md](./docs/validators.md) — Shared Zod schemas
|
|
141
|
+
- [docs/testing.md](./docs/testing.md) — Test strategy and execution
|
|
142
|
+
- [docs/plans/](./docs/plans/) — Package design and refactoring plans
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
2
|
+
|
|
3
|
+
var _chunkHXVWBFCNjs = require('./chunk-HXVWBFCN.js');
|
|
4
|
+
|
|
5
|
+
// src/utils/image-variants.ts
|
|
6
|
+
var _sharp = require('sharp'); var _sharp2 = _interopRequireDefault(_sharp);
|
|
7
|
+
async function generateImageVariants(buffer, baseKey, contentType) {
|
|
8
|
+
if (contentType === "image/gif") return [];
|
|
9
|
+
const image = _sharp2.default.call(void 0, buffer);
|
|
10
|
+
const metadata = await image.metadata();
|
|
11
|
+
const originalWidth = metadata.width || 0;
|
|
12
|
+
const variants = [];
|
|
13
|
+
for (const [size, maxWidth] of Object.entries(_chunkHXVWBFCNjs.IMAGE_VARIANT_SIZES)) {
|
|
14
|
+
if (maxWidth >= originalWidth && size !== "thumb") continue;
|
|
15
|
+
const resized = _sharp2.default.call(void 0, buffer).resize(maxWidth, void 0, { fit: "inside", withoutEnlargement: true }).webp({ quality: 80 });
|
|
16
|
+
const variantBuffer = await resized.toBuffer();
|
|
17
|
+
const variantKey = `${baseKey}-${size}.webp`;
|
|
18
|
+
variants.push({
|
|
19
|
+
size,
|
|
20
|
+
width: Math.min(maxWidth, originalWidth),
|
|
21
|
+
buffer: variantBuffer,
|
|
22
|
+
key: variantKey,
|
|
23
|
+
contentType: "image/webp"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return variants;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
exports.generateImageVariants = generateImageVariants;
|
|
32
|
+
//# sourceMappingURL=chunk-2REYWBQ6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-2REYWBQ6.js","../src/utils/image-variants.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACA;ACJA,4EAAkB;AAclB,MAAA,SAAsB,qBAAA,CACpB,MAAA,EACA,OAAA,EACA,WAAA,EACyB;AACzB,EAAA,GAAA,CAAI,YAAA,IAAgB,WAAA,EAAa,OAAO,CAAC,CAAA;AAEzC,EAAA,MAAM,MAAA,EAAQ,6BAAA,MAAY,CAAA;AAC1B,EAAA,MAAM,SAAA,EAAW,MAAM,KAAA,CAAM,QAAA,CAAS,CAAA;AACtC,EAAA,MAAM,cAAA,EAAgB,QAAA,CAAS,MAAA,GAAS,CAAA;AAExC,EAAA,MAAM,SAAA,EAA2B,CAAC,CAAA;AAElC,EAAA,IAAA,CAAA,MAAW,CAAC,IAAA,EAAM,QAAQ,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,oCAAmB,CAAA,EAA8B;AAC7F,IAAA,GAAA,CAAI,SAAA,GAAY,cAAA,GAAiB,KAAA,IAAS,OAAA,EAAS,QAAA;AAEnD,IAAA,MAAM,QAAA,EAAU,6BAAA,MAAY,CAAA,CACzB,MAAA,CAAO,QAAA,EAAU,KAAA,CAAA,EAAW,EAAE,GAAA,EAAK,QAAA,EAAU,kBAAA,EAAoB,KAAK,CAAC,CAAA,CACvE,IAAA,CAAK,EAAE,OAAA,EAAS,GAAG,CAAC,CAAA;AAEvB,IAAA,MAAM,cAAA,EAAgB,MAAM,OAAA,CAAQ,QAAA,CAAS,CAAA;AAC7C,IAAA,MAAM,WAAA,EAAa,CAAA,EAAA;AAEL,IAAA;AACZ,MAAA;AACgB,MAAA;AACR,MAAA;AACH,MAAA;AACQ,MAAA;AACd,IAAA;AACH,EAAA;AAEO,EAAA;AACT;ADpBwB;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-2REYWBQ6.js","sourcesContent":[null,"import sharp from 'sharp';\n\nexport { IMAGE_VARIANT_SIZES } from './image-variant-utils';\nexport type { VariantSize } from './image-variant-utils';\nimport { IMAGE_VARIANT_SIZES, type VariantSize } from './image-variant-utils';\n\nexport interface ImageVariant {\n size: VariantSize;\n width: number;\n buffer: Buffer;\n key: string;\n contentType: string;\n}\n\nexport async function generateImageVariants(\n buffer: Buffer,\n baseKey: string,\n contentType: string,\n): Promise<ImageVariant[]> {\n if (contentType === 'image/gif') return [];\n\n const image = sharp(buffer);\n const metadata = await image.metadata();\n const originalWidth = metadata.width || 0;\n\n const variants: ImageVariant[] = [];\n\n for (const [size, maxWidth] of Object.entries(IMAGE_VARIANT_SIZES) as [VariantSize, number][]) {\n if (maxWidth >= originalWidth && size !== 'thumb') continue;\n\n const resized = sharp(buffer)\n .resize(maxWidth, undefined, { fit: 'inside', withoutEnlargement: true })\n .webp({ quality: 80 });\n\n const variantBuffer = await resized.toBuffer();\n const variantKey = `${baseKey}-${size}.webp`;\n\n variants.push({\n size,\n width: Math.min(maxWidth, originalWidth),\n buffer: variantBuffer,\n key: variantKey,\n contentType: 'image/webp',\n });\n }\n\n return variants;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/validators/shared.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var slugSchema = z.string().min(1).max(200).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slug must be URL-safe (lowercase, hyphens only)");
|
|
4
|
+
var safeUrl = z.url().refine(
|
|
5
|
+
(url) => {
|
|
6
|
+
const lower = url.toLowerCase().replace(/[\t\n\r]/g, "");
|
|
7
|
+
return !lower.startsWith("file:") && !lower.startsWith("javascript:") && !lower.startsWith("data:");
|
|
8
|
+
},
|
|
9
|
+
{ error: "Unsafe URL protocol (file:, javascript:, data: not allowed)" }
|
|
10
|
+
);
|
|
11
|
+
var optionalUrlSchema = safeUrl.optional().or(z.literal(""));
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
slugSchema,
|
|
15
|
+
optionalUrlSchema
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=chunk-2SYUEQ6J.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validators/shared.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const slugSchema = z\n .string()\n .min(1)\n .max(200)\n .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Slug must be URL-safe (lowercase, hyphens only)');\n\nconst safeUrl = z.url().refine(\n (url) => {\n const lower = url.toLowerCase().replace(/[\\t\\n\\r]/g, '');\n return !lower.startsWith('file:') && !lower.startsWith('javascript:') && !lower.startsWith('data:');\n },\n { error: 'Unsafe URL protocol (file:, javascript:, data: not allowed)' },\n);\nexport const optionalUrlSchema = safeUrl.optional().or(z.literal(''));\n"],"mappings":";AAAA,SAAS,SAAS;AAEX,IAAM,aAAa,EACvB,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP,MAAM,8BAA8B,iDAAiD;AAExF,IAAM,UAAU,EAAE,IAAI,EAAE;AAAA,EACtB,CAAC,QAAQ;AACP,UAAM,QAAQ,IAAI,YAAY,EAAE,QAAQ,aAAa,EAAE;AACvD,WAAO,CAAC,MAAM,WAAW,OAAO,KAAK,CAAC,MAAM,WAAW,aAAa,KAAK,CAAC,MAAM,WAAW,OAAO;AAAA,EACpG;AAAA,EACA,EAAE,OAAO,8DAA8D;AACzE;AACO,IAAM,oBAAoB,QAAQ,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMAGE_VARIANT_SIZES
|
|
3
|
+
} from "./chunk-MMKAMH4U.mjs";
|
|
4
|
+
|
|
5
|
+
// src/utils/image-variants.ts
|
|
6
|
+
import sharp from "sharp";
|
|
7
|
+
async function generateImageVariants(buffer, baseKey, contentType) {
|
|
8
|
+
if (contentType === "image/gif") return [];
|
|
9
|
+
const image = sharp(buffer);
|
|
10
|
+
const metadata = await image.metadata();
|
|
11
|
+
const originalWidth = metadata.width || 0;
|
|
12
|
+
const variants = [];
|
|
13
|
+
for (const [size, maxWidth] of Object.entries(IMAGE_VARIANT_SIZES)) {
|
|
14
|
+
if (maxWidth >= originalWidth && size !== "thumb") continue;
|
|
15
|
+
const resized = sharp(buffer).resize(maxWidth, void 0, { fit: "inside", withoutEnlargement: true }).webp({ quality: 80 });
|
|
16
|
+
const variantBuffer = await resized.toBuffer();
|
|
17
|
+
const variantKey = `${baseKey}-${size}.webp`;
|
|
18
|
+
variants.push({
|
|
19
|
+
size,
|
|
20
|
+
width: Math.min(maxWidth, originalWidth),
|
|
21
|
+
buffer: variantBuffer,
|
|
22
|
+
key: variantKey,
|
|
23
|
+
contentType: "image/webp"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return variants;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
generateImageVariants
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=chunk-3ILEMZAL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/image-variants.ts"],"sourcesContent":["import sharp from 'sharp';\n\nexport { IMAGE_VARIANT_SIZES } from './image-variant-utils';\nexport type { VariantSize } from './image-variant-utils';\nimport { IMAGE_VARIANT_SIZES, type VariantSize } from './image-variant-utils';\n\nexport interface ImageVariant {\n size: VariantSize;\n width: number;\n buffer: Buffer;\n key: string;\n contentType: string;\n}\n\nexport async function generateImageVariants(\n buffer: Buffer,\n baseKey: string,\n contentType: string,\n): Promise<ImageVariant[]> {\n if (contentType === 'image/gif') return [];\n\n const image = sharp(buffer);\n const metadata = await image.metadata();\n const originalWidth = metadata.width || 0;\n\n const variants: ImageVariant[] = [];\n\n for (const [size, maxWidth] of Object.entries(IMAGE_VARIANT_SIZES) as [VariantSize, number][]) {\n if (maxWidth >= originalWidth && size !== 'thumb') continue;\n\n const resized = sharp(buffer)\n .resize(maxWidth, undefined, { fit: 'inside', withoutEnlargement: true })\n .webp({ quality: 80 });\n\n const variantBuffer = await resized.toBuffer();\n const variantKey = `${baseKey}-${size}.webp`;\n\n variants.push({\n size,\n width: Math.min(maxWidth, originalWidth),\n buffer: variantBuffer,\n key: variantKey,\n contentType: 'image/webp',\n });\n }\n\n return variants;\n}\n"],"mappings":";;;;;AAAA,OAAO,WAAW;AAclB,eAAsB,sBACpB,QACA,SACA,aACyB;AACzB,MAAI,gBAAgB,YAAa,QAAO,CAAC;AAEzC,QAAM,QAAQ,MAAM,MAAM;AAC1B,QAAM,WAAW,MAAM,MAAM,SAAS;AACtC,QAAM,gBAAgB,SAAS,SAAS;AAExC,QAAM,WAA2B,CAAC;AAElC,aAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,mBAAmB,GAA8B;AAC7F,QAAI,YAAY,iBAAiB,SAAS,QAAS;AAEnD,UAAM,UAAU,MAAM,MAAM,EACzB,OAAO,UAAU,QAAW,EAAE,KAAK,UAAU,oBAAoB,KAAK,CAAC,EACvE,KAAK,EAAE,SAAS,GAAG,CAAC;AAEvB,UAAM,gBAAgB,MAAM,QAAQ,SAAS;AAC7C,UAAM,aAAa,GAAG,OAAO,IAAI,IAAI;AAErC,aAAS,KAAK;AAAA,MACZ;AAAA,MACA,OAAO,KAAK,IAAI,UAAU,aAAa;AAAA,MACvC,QAAQ;AAAA,MACR,KAAK;AAAA,MACL,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/components/JsonLd.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
var LS = String.fromCharCode(8232);
|
|
4
|
+
var PS = String.fromCharCode(8233);
|
|
5
|
+
var JSON_LD_UNSAFE = new RegExp(`[<>&${LS}${PS}]`, "g");
|
|
6
|
+
var JSON_LD_ESCAPES = {
|
|
7
|
+
"<": "\\u003c",
|
|
8
|
+
">": "\\u003e",
|
|
9
|
+
"&": "\\u0026",
|
|
10
|
+
[LS]: "\\u2028",
|
|
11
|
+
[PS]: "\\u2029"
|
|
12
|
+
};
|
|
13
|
+
function escapeJsonForScript(data) {
|
|
14
|
+
return JSON.stringify(data).replace(
|
|
15
|
+
JSON_LD_UNSAFE,
|
|
16
|
+
(ch) => JSON_LD_ESCAPES[ch]
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function JsonLd({ data }) {
|
|
20
|
+
return /* @__PURE__ */ jsx(
|
|
21
|
+
"script",
|
|
22
|
+
{
|
|
23
|
+
type: "application/ld+json",
|
|
24
|
+
dangerouslySetInnerHTML: { __html: escapeJsonForScript(data) }
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
escapeJsonForScript,
|
|
31
|
+
JsonLd
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=chunk-3Y2TQCHU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/JsonLd.tsx"],"sourcesContent":["/**\n * JSON-LD 구조화 데이터 스크립트 (spec.md §4.6 / Sprint 1 S2).\n *\n * `JSON.stringify(data)` 를 그대로 `<script>` 에 주입하면 `data` 값에 들어 있는\n * `</script>` / `<!--` / `<` / `&` / U+2028 / U+2029 가 스크립트 요소를\n * 탈출(breakout)시켜 저장/반사형 XSS 가 된다. 아래 escapeJsonForScript 가\n * 직렬화 결과를 안전하게 이스케이프한다 (codepoint 기반 — 소스에 비가시\n * 라인 종결자 문자를 두지 않는다). 이스케이프 대상:\n *\n * U+003C (escaped to backslash-u003c) -- blocks </script>, <!--, <\n * U+003E (escaped to backslash-u003e)\n * U+0026 (escaped to backslash-u0026)\n * U+2028 (escaped to backslash-u2028) -- JS string literal break guard\n * U+2029 (escaped to backslash-u2029)\n *\n * 이 치환들은 JSON 문자열 컨텍스트 안에서 유효한 \\uXXXX 이스케이프이므로,\n * 디코드된 스크립트 텍스트를 JSON.parse 하면 원본 data 와 정확히\n * deep-equal 한다 (round-trip 보존).\n */\n\n// 소스에 raw U+2028/U+2029 (JS line terminator) 를 두지 않도록 codepoint 으로\n// 문자 클래스를 구성한다.\nconst LS = String.fromCharCode(0x2028);\nconst PS = String.fromCharCode(0x2029);\nconst JSON_LD_UNSAFE = new RegExp(`[<>&${LS}${PS}]`, 'g');\n\nconst JSON_LD_ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n [LS]: '\\\\u2028',\n [PS]: '\\\\u2029',\n};\n\n/**\n * `<script>` 안에 안전하게 넣을 수 있도록 직렬화된 JSON 을 이스케이프한다.\n * 결과는 여전히 유효한 `application/ld+json` 이며 `JSON.parse` 시 원본으로\n * 복원된다.\n */\nexport function escapeJsonForScript(data: unknown): string {\n return JSON.stringify(data).replace(\n JSON_LD_UNSAFE,\n (ch) => JSON_LD_ESCAPES[ch],\n );\n}\n\nexport function JsonLd({ data }: { data: Record<string, unknown> }) {\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: escapeJsonForScript(data) }}\n />\n );\n}\n"],"mappings":";AAgDI;AA1BJ,IAAM,KAAK,OAAO,aAAa,IAAM;AACrC,IAAM,KAAK,OAAO,aAAa,IAAM;AACrC,IAAM,iBAAiB,IAAI,OAAO,OAAO,EAAE,GAAG,EAAE,KAAK,GAAG;AAExD,IAAM,kBAA0C;AAAA,EAC9C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,CAAC,EAAE,GAAG;AAAA,EACN,CAAC,EAAE,GAAG;AACR;AAOO,SAAS,oBAAoB,MAAuB;AACzD,SAAO,KAAK,UAAU,IAAI,EAAE;AAAA,IAC1B;AAAA,IACA,CAAC,OAAO,gBAAgB,EAAE;AAAA,EAC5B;AACF;AAEO,SAAS,OAAO,EAAE,KAAK,GAAsC;AAClE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,yBAAyB,EAAE,QAAQ,oBAAoB,IAAI,EAAE;AAAA;AAAA,EAC/D;AAEJ;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";//# sourceMappingURL=chunk-4LSFAAZW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-4LSFAAZW.js"],"names":[],"mappings":"AAAA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-4LSFAAZW.js"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
var _chunkHXVWBFCNjs = require('./chunk-HXVWBFCN.js');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkBE7JDL64js = require('./chunk-BE7JDL64.js');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
|
|
11
|
+
|
|
12
|
+
// src/utils/r2-helpers.ts
|
|
13
|
+
var _logger = require('@withwiz/toolkit/core/logger/logger');
|
|
14
|
+
var IMG_SRC_REGEX = /<img[^>]+src=["']([^"']+)["']/gi;
|
|
15
|
+
var VARIANT_SUFFIXES = Object.keys(_chunkHXVWBFCNjs.IMAGE_VARIANT_SIZES);
|
|
16
|
+
function toRelativePath(src) {
|
|
17
|
+
let path;
|
|
18
|
+
try {
|
|
19
|
+
const u = new URL(src);
|
|
20
|
+
path = u.pathname;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
path = src;
|
|
23
|
+
}
|
|
24
|
+
path = path.replace(/^\/+/, "").split(/[?#]/)[0];
|
|
25
|
+
return path.length > 0 ? path : null;
|
|
26
|
+
}
|
|
27
|
+
function srcToKey(src, rule) {
|
|
28
|
+
if (rule.publicBaseUrl) {
|
|
29
|
+
const base = rule.publicBaseUrl.replace(/\/+$/, "");
|
|
30
|
+
if (!src.startsWith(base + "/") && !src.startsWith(base)) return null;
|
|
31
|
+
const after = src.slice(base.length).replace(/^\/+/, "").split(/[?#]/)[0];
|
|
32
|
+
return after.length > 0 ? after : null;
|
|
33
|
+
}
|
|
34
|
+
const path = toRelativePath(src);
|
|
35
|
+
if (!path) return null;
|
|
36
|
+
if (rule.inlineKeyPrefixes) {
|
|
37
|
+
const top = path.split("/")[0] + "/";
|
|
38
|
+
const matches = rule.inlineKeyPrefixes.some((p) => {
|
|
39
|
+
const norm = p.endsWith("/") ? p : p + "/";
|
|
40
|
+
return top === norm || path.startsWith(norm);
|
|
41
|
+
});
|
|
42
|
+
return matches ? path : null;
|
|
43
|
+
}
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
function extractR2KeysFromHtml(...htmlContents) {
|
|
47
|
+
const rule = _chunkEG6NVPP2js.resolveStorageConfig.call(void 0, );
|
|
48
|
+
if (!rule.inlineKeyPrefixes && !rule.publicBaseUrl) {
|
|
49
|
+
_chunkEG6NVPP2js.warnOnceMissingConfig.call(void 0,
|
|
50
|
+
"storage.inlinePrefix",
|
|
51
|
+
"storage inline-image key prefix/publicBaseUrl is not configured. Collecting ALL inline <img> paths to avoid silently orphaning non-default-prefix objects. Inject `setCmsConfig({ storage: { inlineKeyPrefixes } })` or `{ storage: { publicBaseUrl } }` for precise R2 cleanup."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const keys = [];
|
|
55
|
+
for (const html of htmlContents) {
|
|
56
|
+
if (!html) continue;
|
|
57
|
+
const regex = new RegExp(IMG_SRC_REGEX.source, IMG_SRC_REGEX.flags);
|
|
58
|
+
let match;
|
|
59
|
+
while ((match = regex.exec(html)) !== null) {
|
|
60
|
+
const key = srcToKey(match[1], rule);
|
|
61
|
+
if (key) keys.push(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return keys;
|
|
65
|
+
}
|
|
66
|
+
function getVariantKeys(key) {
|
|
67
|
+
const baseKey = key.replace(/\.[^.]+$/, "");
|
|
68
|
+
return VARIANT_SUFFIXES.map((suffix) => `${baseKey}-${suffix}.webp`);
|
|
69
|
+
}
|
|
70
|
+
function collectR2Keys(primaryKey, ...htmlContents) {
|
|
71
|
+
const keys = [];
|
|
72
|
+
if (primaryKey) {
|
|
73
|
+
keys.push(primaryKey);
|
|
74
|
+
keys.push(...getVariantKeys(primaryKey));
|
|
75
|
+
}
|
|
76
|
+
const inlineKeys = extractR2KeysFromHtml(...htmlContents);
|
|
77
|
+
for (const ik of inlineKeys) {
|
|
78
|
+
keys.push(ik);
|
|
79
|
+
keys.push(...getVariantKeys(ik));
|
|
80
|
+
}
|
|
81
|
+
return [...new Set(keys)];
|
|
82
|
+
}
|
|
83
|
+
async function deleteR2Keys(keys) {
|
|
84
|
+
if (keys.length === 0) return;
|
|
85
|
+
await Promise.allSettled(
|
|
86
|
+
keys.map(
|
|
87
|
+
(key) => _chunkBE7JDL64js.deleteFromR2.call(void 0, key).catch(
|
|
88
|
+
(err) => _logger.logError.call(void 0, `Failed to delete ${key} from R2`, { error: err instanceof Error ? err.message : err })
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
exports.extractR2KeysFromHtml = extractR2KeysFromHtml; exports.getVariantKeys = getVariantKeys; exports.collectR2Keys = collectR2Keys; exports.deleteR2Keys = deleteR2Keys;
|
|
100
|
+
//# sourceMappingURL=chunk-4W3X7OKF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-4W3X7OKF.js","../src/utils/r2-helpers.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACE;AACA;AACF,sDAA4B;AAC5B;AACA;ACTA,6DAAyB;AAoBzB,IAAM,cAAA,EAAgB,iCAAA;AAEtB,IAAM,iBAAA,EAAmB,MAAA,CAAO,IAAA,CAAK,oCAAmB,CAAA;AAGxD,SAAS,cAAA,CAAe,GAAA,EAA4B;AAClD,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AAEF,IAAA,MAAM,EAAA,EAAI,IAAI,GAAA,CAAI,GAAG,CAAA;AACrB,IAAA,KAAA,EAAO,CAAA,CAAE,QAAA;AAAA,EACX,EAAA,UAAQ;AAEN,IAAA,KAAA,EAAO,GAAA;AAAA,EACT;AACA,EAAA,KAAA,EAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA,CAAE,KAAA,CAAM,MAAM,CAAA,CAAE,CAAC,CAAA;AAC/C,EAAA,OAAO,IAAA,CAAK,OAAA,EAAS,EAAA,EAAI,KAAA,EAAO,IAAA;AAClC;AAGA,SAAS,QAAA,CACP,GAAA,EACA,IAAA,EACe;AACf,EAAA,GAAA,CAAI,IAAA,CAAK,aAAA,EAAe;AACtB,IAAA,MAAM,KAAA,EAAO,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAClD,IAAA,GAAA,CAAI,CAAC,GAAA,CAAI,UAAA,CAAW,KAAA,EAAO,GAAG,EAAA,GAAK,CAAC,GAAA,CAAI,UAAA,CAAW,IAAI,CAAA,EAAG,OAAO,IAAA;AACjE,IAAA,MAAM,MAAA,EAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA,CAAE,KAAA,CAAM,MAAM,CAAA,CAAE,CAAC,CAAA;AACxE,IAAA,OAAO,KAAA,CAAM,OAAA,EAAS,EAAA,EAAI,MAAA,EAAQ,IAAA;AAAA,EACpC;AAEA,EAAA,MAAM,KAAA,EAAO,cAAA,CAAe,GAAG,CAAA;AAC/B,EAAA,GAAA,CAAI,CAAC,IAAA,EAAM,OAAO,IAAA;AAElB,EAAA,GAAA,CAAI,IAAA,CAAK,iBAAA,EAAmB;AAC1B,IAAA,MAAM,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,EAAA,EAAI,GAAA;AACjC,IAAA,MAAM,QAAA,EAAU,IAAA,CAAK,iBAAA,CAAkB,IAAA,CAAK,CAAC,CAAA,EAAA,GAAM;AACjD,MAAA,MAAM,KAAA,EAAO,CAAA,CAAE,QAAA,CAAS,GAAG,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,GAAA;AACvC,MAAA,OAAO,IAAA,IAAQ,KAAA,GAAQ,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA;AAAA,IAC7C,CAAC,CAAA;AACD,IAAA,OAAO,QAAA,EAAU,KAAA,EAAO,IAAA;AAAA,EAC1B;AAIA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,qBAAA,CAAA,GAAyB,YAAA,EAA2C;AAClF,EAAA,MAAM,KAAA,EAAO,mDAAA,CAAqB;AAClC,EAAA,GAAA,CAAI,CAAC,IAAA,CAAK,kBAAA,GAAqB,CAAC,IAAA,CAAK,aAAA,EAAe;AAClD,IAAA,oDAAA;AAAA,MACE,sBAAA;AAAA,MACA;AAAA,IAKF,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,EAAiB,CAAC,CAAA;AACxB,EAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,YAAA,EAAc;AAC/B,IAAA,GAAA,CAAI,CAAC,IAAA,EAAM,QAAA;AACX,IAAA,MAAM,MAAA,EAAQ,IAAI,MAAA,CAAO,aAAA,CAAc,MAAA,EAAQ,aAAA,CAAc,KAAK,CAAA;AAClE,IAAA,IAAI,KAAA;AACJ,IAAA,MAAA,CAAA,CAAQ,MAAA,EAAQ,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,EAAA,IAAO,IAAA,EAAM;AAC1C,MAAA,MAAM,IAAA,EAAM,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,IAAI,CAAA;AACnC,MAAA,GAAA,CAAI,GAAA,EAAK,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,cAAA,CAAe,GAAA,EAAuB;AACpD,EAAA,MAAM,QAAA,EAAU,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA;AAC1C,EAAA,OAAO,gBAAA,CAAiB,GAAA,CAAI,CAAC,MAAA,EAAA,GAAW,CAAA,EAAA;AAC1C;AAE4D;AAClC,EAAA;AACR,EAAA;AACM,IAAA;AACmB,IAAA;AACzC,EAAA;AACyC,EAAA;AACZ,EAAA;AACf,IAAA;AACmB,IAAA;AACjC,EAAA;AACwB,EAAA;AAC1B;AAIkE;AACzC,EAAA;AACT,EAAA;AACP,IAAA;AACe,MAAA;AACP,QAAA;AACX,MAAA;AACF,IAAA;AACF,EAAA;AACF;ADlC2C;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-4W3X7OKF.js","sourcesContent":[null,"import { deleteFromR2 } from './r2-storage';\nimport { IMAGE_VARIANT_SIZES, type VariantSize } from './image-variant-utils';\nimport { logError } from '@withwiz/toolkit/core/logger/logger';\nimport { resolveStorageConfig, warnOnceMissingConfig } from '../config';\n\n/**\n * inline `<img>` 에서 R2/storage key 를 추출한다 (spec.md §4.1 C3 /\n * orphan-bug fix).\n *\n * 더 이상 `news/` prefix 만 매칭하지 않는다. prefix 규칙은 §5 config\n * boundary 를 통해 주입된다:\n *\n * - `storage.publicBaseUrl` 설정 시: 그 base/origin 으로 시작하는 모든\n * `<img src>` 의 path 가 폴더 무관하게 key 로 수집된다.\n * - `storage.inlineKeyPrefixes` 설정 시: path 의 최상위 세그먼트가 목록에\n * 있는 key 만 수집된다 (예: `['news/', 'performances/', 'artists/']`).\n * - 미설정(unconfigured) 기본값: 어떤 prefix 도 *silently drop 하지 않는다*.\n * 하드코딩 `news/`-only regex 와 달리 모든 inline 이미지의 host 이후\n * path 를 수집한다 (orphan 방지). 정밀 cleanup 을 위해 prefix/base 설정을\n * 권장하는 `@withwiz/cms-kit:` warn 을 1회 발행한다.\n */\n\nconst IMG_SRC_REGEX = /<img[^>]+src=[\"']([^\"']+)[\"']/gi;\n\nconst VARIANT_SUFFIXES = Object.keys(IMAGE_VARIANT_SIZES) as VariantSize[];\n\n/** URL/문자열에서 scheme+host 를 제거한 path(leading-slash 없음)를 얻는다. */\nfunction toRelativePath(src: string): string | null {\n let path: string;\n try {\n // 절대 URL: origin 제거\n const u = new URL(src);\n path = u.pathname;\n } catch {\n // 상대 경로: 그대로\n path = src;\n }\n path = path.replace(/^\\/+/, '').split(/[?#]/)[0];\n return path.length > 0 ? path : null;\n}\n\n/** 한 src 가 설정된 규칙에 따라 수집 대상 key 인지 판별하고 key 를 반환. */\nfunction srcToKey(\n src: string,\n rule: { inlineKeyPrefixes: readonly string[] | null; publicBaseUrl: string | null },\n): string | null {\n if (rule.publicBaseUrl) {\n const base = rule.publicBaseUrl.replace(/\\/+$/, '');\n if (!src.startsWith(base + '/') && !src.startsWith(base)) return null;\n const after = src.slice(base.length).replace(/^\\/+/, '').split(/[?#]/)[0];\n return after.length > 0 ? after : null;\n }\n\n const path = toRelativePath(src);\n if (!path) return null;\n\n if (rule.inlineKeyPrefixes) {\n const top = path.split('/')[0] + '/';\n const matches = rule.inlineKeyPrefixes.some((p) => {\n const norm = p.endsWith('/') ? p : p + '/';\n return top === norm || path.startsWith(norm);\n });\n return matches ? path : null;\n }\n\n // unconfigured default: collect every plausible inline storage key (no\n // silent orphaning). A path with at least one segment qualifies.\n return path;\n}\n\nexport function extractR2KeysFromHtml(...htmlContents: (string | null)[]): string[] {\n const rule = resolveStorageConfig();\n if (!rule.inlineKeyPrefixes && !rule.publicBaseUrl) {\n warnOnceMissingConfig(\n 'storage.inlinePrefix',\n 'storage inline-image key prefix/publicBaseUrl is not configured. ' +\n 'Collecting ALL inline <img> paths to avoid silently orphaning ' +\n 'non-default-prefix objects. Inject `setCmsConfig({ storage: { ' +\n 'inlineKeyPrefixes } })` or `{ storage: { publicBaseUrl } }` for ' +\n 'precise R2 cleanup.',\n );\n }\n\n const keys: string[] = [];\n for (const html of htmlContents) {\n if (!html) continue;\n const regex = new RegExp(IMG_SRC_REGEX.source, IMG_SRC_REGEX.flags);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(html)) !== null) {\n const key = srcToKey(match[1], rule);\n if (key) keys.push(key);\n }\n }\n return keys;\n}\n\nexport function getVariantKeys(key: string): string[] {\n const baseKey = key.replace(/\\.[^.]+$/, '');\n return VARIANT_SUFFIXES.map((suffix) => `${baseKey}-${suffix}.webp`);\n}\n\nexport function collectR2Keys(primaryKey: string | null, ...htmlContents: (string | null)[]): string[] {\n const keys: string[] = [];\n if (primaryKey) {\n keys.push(primaryKey);\n keys.push(...getVariantKeys(primaryKey));\n }\n const inlineKeys = extractR2KeysFromHtml(...htmlContents);\n for (const ik of inlineKeys) {\n keys.push(ik);\n keys.push(...getVariantKeys(ik));\n }\n return [...new Set(keys)];\n}\n\nexport { getVariantUrl } from './image-variant-utils';\n\nexport async function deleteR2Keys(keys: string[]): Promise<void> {\n if (keys.length === 0) return;\n await Promise.allSettled(\n keys.map((key) =>\n deleteFromR2(key).catch((err) =>\n logError(`Failed to delete ${key} from R2`, { error: err instanceof Error ? err.message : err }),\n ),\n ),\n );\n}\n"]}
|