@withwiz/cms-kit 0.1.0 → 0.2.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 +4 -2
- package/README.md +4 -2
- package/dist/{chunk-JMWXVXK5.js → chunk-25GMHPCW.js} +12 -4
- package/dist/chunk-25GMHPCW.js.map +1 -0
- package/dist/{chunk-5JUIMXNF.js → chunk-2VHNZWH2.js} +3 -3
- package/dist/{chunk-5JUIMXNF.js.map → chunk-2VHNZWH2.js.map} +1 -1
- package/dist/{chunk-BWKKHCDG.mjs → chunk-3UXX27N5.mjs} +44 -3
- package/dist/{chunk-BWKKHCDG.mjs.map → chunk-3UXX27N5.mjs.map} +1 -1
- package/dist/{chunk-T7EVFD4G.mjs → chunk-47DPXAO6.mjs} +2 -2
- package/dist/{chunk-4W3X7OKF.js → chunk-5Z5XUR2K.js} +48 -27
- package/dist/chunk-5Z5XUR2K.js.map +1 -0
- package/dist/{chunk-LDFE33ML.mjs → chunk-7F5YFG2W.mjs} +10 -2
- package/dist/chunk-7F5YFG2W.mjs.map +1 -0
- package/dist/{chunk-CGVFB23E.mjs → chunk-A557FA2K.mjs} +24 -19
- package/dist/{chunk-CGVFB23E.mjs.map → chunk-A557FA2K.mjs.map} +1 -1
- package/dist/{chunk-OBNALAMG.mjs → chunk-BG4C5KZO.mjs} +2 -2
- package/dist/{chunk-DCIF4XJL.js → chunk-EPVV6655.js} +10 -10
- package/dist/{chunk-DCIF4XJL.js.map → chunk-EPVV6655.js.map} +1 -1
- package/dist/{chunk-LF5LOI7K.mjs → chunk-FCWUNCGA.mjs} +3 -3
- package/dist/{chunk-YEQFKOWD.mjs → chunk-GQJNLE34.mjs} +2 -2
- package/dist/{chunk-W4FWIZ2Y.js → chunk-HVB72GT3.js} +3 -3
- package/dist/{chunk-W4FWIZ2Y.js.map → chunk-HVB72GT3.js.map} +1 -1
- package/dist/{chunk-KWKV2GAH.js → chunk-J32BTYJZ.js} +4 -4
- package/dist/{chunk-KWKV2GAH.js.map → chunk-J32BTYJZ.js.map} +1 -1
- package/dist/{chunk-C5WFZJM4.js → chunk-JPIQU2C4.js} +6 -6
- package/dist/{chunk-C5WFZJM4.js.map → chunk-JPIQU2C4.js.map} +1 -1
- package/dist/{chunk-FEPGJ7WN.mjs → chunk-MN27HJKW.mjs} +3 -3
- package/dist/{chunk-ZULSFSDG.mjs → chunk-MWX5B4DP.mjs} +3 -3
- package/dist/{chunk-FCCCG5Z7.mjs → chunk-MXXC4U3D.mjs} +2 -2
- package/dist/{chunk-5RDMODGV.mjs → chunk-N7LMNP4W.mjs} +2 -2
- package/dist/{chunk-YY7K5CXX.js → chunk-OHJISVGC.js} +5 -5
- package/dist/{chunk-YY7K5CXX.js.map → chunk-OHJISVGC.js.map} +1 -1
- package/dist/{chunk-EM4MQEWV.mjs → chunk-SN3XOMF6.mjs} +45 -24
- package/dist/chunk-SN3XOMF6.mjs.map +1 -0
- package/dist/{chunk-BE7JDL64.js → chunk-TKOFGRU4.js} +11 -11
- package/dist/{chunk-BE7JDL64.js.map → chunk-TKOFGRU4.js.map} +1 -1
- package/dist/{chunk-EG6NVPP2.js → chunk-UVZD7GHG.js} +44 -3
- package/dist/chunk-UVZD7GHG.js.map +1 -0
- package/dist/{chunk-6EZ3QE5N.js → chunk-Y3YYIJYG.js} +3 -3
- package/dist/{chunk-6EZ3QE5N.js.map → chunk-Y3YYIJYG.js.map} +1 -1
- package/dist/{chunk-OYJRR3G2.js → chunk-ZMXNPYFS.js} +25 -20
- package/dist/chunk-ZMXNPYFS.js.map +1 -0
- package/dist/components/AdminManagerBase.js +5 -5
- package/dist/components/AdminManagerBase.mjs +4 -4
- package/dist/components/AdminShell.d.mts +1 -1
- package/dist/components/AdminShell.d.ts +1 -1
- package/dist/components/AdminShell.js +4 -4
- package/dist/components/AdminShell.mjs +3 -3
- package/dist/components/index.d.mts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +8 -8
- package/dist/components/index.mjs +7 -7
- package/dist/hooks/index.js +5 -5
- package/dist/hooks/index.mjs +4 -4
- package/dist/hooks/useImageDropZone.js +4 -4
- package/dist/hooks/useImageDropZone.mjs +3 -3
- package/dist/{index-00a40-FQ.d.ts → index-Cce8FHtI.d.mts} +38 -1
- package/dist/{index-00a40-FQ.d.mts → index-Cce8FHtI.d.ts} +38 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -12
- package/dist/infrastructure/index.js +3 -3
- package/dist/infrastructure/index.mjs +2 -2
- package/dist/infrastructure/middleware/index.js +3 -3
- package/dist/infrastructure/middleware/index.mjs +2 -2
- package/dist/infrastructure/middleware/wrappers.js +3 -3
- package/dist/infrastructure/middleware/wrappers.mjs +2 -2
- package/dist/services/index.js +5 -5
- package/dist/services/index.mjs +4 -4
- package/dist/utils/admin-fetch.js +3 -3
- package/dist/utils/admin-fetch.mjs +2 -2
- package/dist/utils/html-sanitizer.js +3 -3
- package/dist/utils/html-sanitizer.mjs +2 -2
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +13 -7
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +12 -6
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/jwt.mjs +2 -2
- package/dist/utils/r2-helpers.js +4 -4
- package/dist/utils/r2-helpers.mjs +3 -3
- package/dist/utils/r2-storage.js +3 -3
- package/dist/utils/r2-storage.mjs +2 -2
- package/package.json +4 -4
- package/dist/chunk-4W3X7OKF.js.map +0 -1
- package/dist/chunk-EG6NVPP2.js.map +0 -1
- package/dist/chunk-EM4MQEWV.mjs.map +0 -1
- package/dist/chunk-JMWXVXK5.js.map +0 -1
- package/dist/chunk-LDFE33ML.mjs.map +0 -1
- package/dist/chunk-OYJRR3G2.js.map +0 -1
- /package/dist/{chunk-T7EVFD4G.mjs.map → chunk-47DPXAO6.mjs.map} +0 -0
- /package/dist/{chunk-OBNALAMG.mjs.map → chunk-BG4C5KZO.mjs.map} +0 -0
- /package/dist/{chunk-LF5LOI7K.mjs.map → chunk-FCWUNCGA.mjs.map} +0 -0
- /package/dist/{chunk-YEQFKOWD.mjs.map → chunk-GQJNLE34.mjs.map} +0 -0
- /package/dist/{chunk-FEPGJ7WN.mjs.map → chunk-MN27HJKW.mjs.map} +0 -0
- /package/dist/{chunk-ZULSFSDG.mjs.map → chunk-MWX5B4DP.mjs.map} +0 -0
- /package/dist/{chunk-FCCCG5Z7.mjs.map → chunk-MXXC4U3D.mjs.map} +0 -0
- /package/dist/{chunk-5RDMODGV.mjs.map → chunk-N7LMNP4W.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/html-sanitizer.ts"],"sourcesContent":["/**\n * 서버사이드 HTML 새니타이저 (spec.md §4.6 / Sprint 1 S1).\n *\n * 위험한 요소(script, iframe, object 등)와 이벤트 핸들러 속성을 제거하면서\n * 블록 에디터가 사용하는 안전한 HTML 구조를 보존한다.\n *\n * - 1차(기본): `isomorphic-dompurify` (실제 jsdom+DOMPurify DOM/parser\n * allowlist 새니타이저 — 손수 만든 regex 가 아님). 동적으로 로드한다.\n * - fallback: regex 기반 새니타이저는 *방어 심층(defense-in-depth)* 으로만\n * 남겨둔다 (optional dep 미설치 시). 더 이상 1차 경로가 아니다.\n * - `createSanitizer(config)` 로 신뢰 iframe origin 을 주입할 수 있다.\n *\n * `sanitizeHtmlContent` 는 하위 호환을 위해 기본 안전 설정으로 동작한다\n * (I1 — 동일 이름, 동일 `string|null|undefined → string|null` 시그니처).\n * 기본 신뢰 iframe origin 은 §5 config boundary 를 통해 consumer 가\n * override 할 수 있고, 미설정 시 현재 YouTube/Vimeo 집합이 그대로 쓰인다\n * (unconfigured 동작 불변 — I3 보호).\n *\n * `../../withwiz-blog-core/src/utils/html-sanitizer.ts` 와 *메커니즘 동일*\n * (`createSanitizer` factory + `tryLoadDomPurify()` 동적 require +\n * `uponSanitizeElement` iframe-origin hook + regex defense-in-depth fallback).\n */\n\nimport { resolveTrustedIframeOrigins } from '../config';\n\n// ── 정규식 패턴 (defense-in-depth fallback 전용) ──\n\n/** 항상 제거할 태그 (내용 포함) */\nconst STRIP_TAGS_WITH_CONTENT =\n /(<\\s*\\/?\\s*(script|object|embed|applet|form|input|textarea|select|button)\\b[^>]*>)/gi;\n\n/** 신뢰되지 않는 iframe 매칭 */\nconst UNTRUSTED_IFRAME = /<\\s*iframe\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*iframe\\s*>/gi;\n\n/** script/style 태그 사이 콘텐츠 */\nconst STRIP_TAG_CONTENT = /<\\s*(script|style)\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*\\1\\s*>/gi;\n\n/** 이벤트 핸들러 속성 (onclick, onerror, onload 등) */\nconst EVENT_HANDLER_ATTRS = /\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)/gi;\n\n/** 위험한 URL 프로토콜 (href/src 속성 내) */\nconst DANGEROUS_PROTOCOL =\n /(href|src|action)\\s*=\\s*[\"']\\s*(javascript|vbscript|data\\s*:(?!image\\/))[^\"']*[\"']/gi;\n\n// ── 설정 인터페이스 ──\n\n/** 새니타이저 설정 */\nexport interface SanitizerConfig {\n /**\n * 신뢰할 수 있는 iframe origin 목록 (prefix 매칭).\n * 미지정 시 §5 config boundary 의 기본값(YouTube/Vimeo) 사용.\n */\n trustedIframeOrigins?: readonly string[];\n /** DOMPurify 사용 시 허용할 태그 화이트리스트. */\n allowedTags?: string[];\n /** DOMPurify 사용 시 허용할 속성 화이트리스트. */\n allowedAttributes?: Record<string, string[]>;\n}\n\n// ── DOMPurify 동적 로딩 (선택적 / 실제 사용되는 1차 경로) ──\n\ntype DOMPurifyLike = {\n sanitize: (dirty: string, options?: Record<string, unknown>) => string;\n};\n\nlet cachedDomPurify: DOMPurifyLike | null | undefined;\n\n/**\n * 선택적 의존성 `isomorphic-dompurify` (실제 DOM allowlist 새니타이저) 를\n * 동기적으로 로드한다. 설치되지 않았거나 로드 실패 시에만 null 을 반환하고,\n * 호출자는 regex defense-in-depth fallback 을 쓴다.\n */\nfunction tryLoadDomPurify(): DOMPurifyLike | null {\n if (cachedDomPurify !== undefined) return cachedDomPurify;\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('isomorphic-dompurify') as unknown;\n const candidate =\n (mod as { default?: DOMPurifyLike }).default ?? (mod as DOMPurifyLike);\n if (candidate && typeof candidate.sanitize === 'function') {\n cachedDomPurify = candidate;\n return candidate;\n }\n } catch {\n // 모듈 미설치 — regex fallback 사용\n }\n cachedDomPurify = null;\n return null;\n}\n\n// ── 정규식 기반 새니타이저 (defense-in-depth fallback) ──\n\nfunction regexSanitize(html: string, trustedOrigins: readonly string[]): string {\n let result = html;\n\n // 1. script/style 태그 사이 콘텐츠 제거\n result = result.replace(STRIP_TAG_CONTENT, '');\n\n // 2. 위험한 태그 제거\n result = result.replace(STRIP_TAGS_WITH_CONTENT, '');\n\n // 2b. 신뢰되지 않는 iframe 제거 (신뢰 origin 유지)\n result = result.replace(UNTRUSTED_IFRAME, (match) => {\n const srcMatch = match.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i);\n if (srcMatch) {\n const src = srcMatch[1].trim();\n if (trustedOrigins.some((origin) => src.startsWith(origin))) {\n return match;\n }\n }\n return '';\n });\n\n // 3. 이벤트 핸들러 속성 제거\n result = result.replace(EVENT_HANDLER_ATTRS, '');\n\n // 4. 위험한 URL 프로토콜 무력화\n result = result.replace(DANGEROUS_PROTOCOL, '$1=\"\"');\n\n return result;\n}\n\n// ── DOMPurify 기반 새니타이저 (1차 경로) ──\n\nfunction dompurifySanitize(\n html: string,\n purify: DOMPurifyLike,\n trustedOrigins: readonly string[],\n config: SanitizerConfig,\n): string {\n const options: Record<string, unknown> = {\n // iframe 은 hook 에서 origin 검증 후 허용\n ADD_TAGS: ['iframe'],\n ADD_ATTR: ['allowfullscreen', 'frameborder', 'allow'],\n FORBID_TAGS: [\n 'script',\n 'object',\n 'embed',\n 'applet',\n 'form',\n 'input',\n 'textarea',\n 'select',\n 'button',\n 'style',\n ],\n FORBID_ATTR: [\n 'onerror',\n 'onload',\n 'onclick',\n 'onmouseover',\n 'onfocus',\n 'onblur',\n ],\n };\n\n if (config.allowedTags) options.ALLOWED_TAGS = config.allowedTags;\n if (config.allowedAttributes) {\n const flat = new Set<string>();\n for (const attrs of Object.values(config.allowedAttributes)) {\n for (const a of attrs) flat.add(a);\n }\n options.ALLOWED_ATTR = Array.from(flat);\n }\n\n const purifyWithHook = purify as DOMPurifyLike & {\n addHook?: (hook: string, cb: (node: Element) => void) => void;\n removeHook?: (hook: string) => void;\n };\n if (typeof purifyWithHook.addHook === 'function') {\n purifyWithHook.addHook('uponSanitizeElement', (node) => {\n if (node.nodeName && node.nodeName.toLowerCase() === 'iframe') {\n const src = (node as Element).getAttribute?.('src') ?? '';\n const trusted = trustedOrigins.some((origin) =>\n src.startsWith(origin),\n );\n if (!trusted) {\n (node as Element).remove?.();\n }\n }\n });\n }\n try {\n return purify.sanitize(html, options);\n } finally {\n if (typeof purifyWithHook.removeHook === 'function') {\n purifyWithHook.removeHook('uponSanitizeElement');\n }\n }\n}\n\n// ── Public API ──\n\n/**\n * 새니타이저 팩토리.\n *\n * DOMPurify(isomorphic-dompurify) 가 설치되어 있으면 그것을 *1차* 로 사용하고,\n * 없을 때만 regex defense-in-depth fallback 으로 동작한다.\n *\n * @example\n * const sanitize = createSanitizer({\n * trustedIframeOrigins: ['https://www.youtube.com/', 'https://www.loom.com/'],\n * });\n * const safe = sanitize(userHtml);\n */\nexport function createSanitizer(\n config: SanitizerConfig = {},\n): (html: string | null | undefined) => string | null {\n return function sanitize(html: string | null | undefined): string | null {\n if (!html) return html as string | null;\n // 신뢰 origin 은 point-of-use 에서 §5 boundary 를 통해 해석한다\n // (lazy — import 시점에 config 가 없어도 throw 하지 않음).\n const trustedOrigins =\n config.trustedIframeOrigins ?? resolveTrustedIframeOrigins();\n const purify = tryLoadDomPurify();\n if (purify) {\n return dompurifySanitize(html, purify, trustedOrigins, config);\n }\n return regexSanitize(html, trustedOrigins);\n };\n}\n\n/**\n * 리치 HTML 콘텐츠에서 위험한 요소와 속성을 제거한다.\n * 기본 안전 설정으로 동작한다 (하위 호환, I1). 호스트별 신뢰 origin 확장은\n * `createSanitizer({ trustedIframeOrigins: [...] })` 또는 §5\n * `setCmsConfig({ sanitizer: { trustedIframeOrigins } })` 로 한다.\n */\nexport function sanitizeHtmlContent(\n html: string | null | undefined,\n): string | null {\n return defaultSanitize(html);\n}\n\n/** 모듈 로드 시점에 1회 만들어지는 기본 새니타이저 (실행은 lazy). */\nconst defaultSanitize = createSanitizer();\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/html-sanitizer.ts"],"sourcesContent":["/**\n * 서버사이드 HTML 새니타이저 (spec.md §4.6 / Sprint 1 S1).\n *\n * 위험한 요소(script, iframe, object 등)와 이벤트 핸들러 속성을 제거하면서\n * 블록 에디터가 사용하는 안전한 HTML 구조를 보존한다.\n *\n * - 1차(기본): `isomorphic-dompurify` (실제 jsdom+DOMPurify DOM/parser\n * allowlist 새니타이저 — 손수 만든 regex 가 아님). 동적으로 로드한다.\n * - fallback: regex 기반 새니타이저는 *방어 심층(defense-in-depth)* 으로만\n * 남겨둔다 (optional dep 미설치 시). 더 이상 1차 경로가 아니다.\n * - `createSanitizer(config)` 로 신뢰 iframe origin 을 주입할 수 있다.\n *\n * `sanitizeHtmlContent` 는 하위 호환을 위해 기본 안전 설정으로 동작한다\n * (I1 — 동일 이름, 동일 `string|null|undefined → string|null` 시그니처).\n * 기본 신뢰 iframe origin 은 §5 config boundary 를 통해 consumer 가\n * override 할 수 있고, 미설정 시 현재 YouTube/Vimeo 집합이 그대로 쓰인다\n * (unconfigured 동작 불변 — I3 보호).\n *\n * `../../withwiz-blog-core/src/utils/html-sanitizer.ts` 와 *메커니즘 동일*\n * (`createSanitizer` factory + `tryLoadDomPurify()` 동적 require +\n * `uponSanitizeElement` iframe-origin hook + regex defense-in-depth fallback).\n */\n\nimport { resolveTrustedIframeOrigins } from '../config';\n\n// ── 정규식 패턴 (defense-in-depth fallback 전용) ──\n\n/** 항상 제거할 태그 (내용 포함) */\nconst STRIP_TAGS_WITH_CONTENT =\n /(<\\s*\\/?\\s*(script|object|embed|applet|form|input|textarea|select|button)\\b[^>]*>)/gi;\n\n/** 신뢰되지 않는 iframe 매칭 */\nconst UNTRUSTED_IFRAME = /<\\s*iframe\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*iframe\\s*>/gi;\n\n/** script/style 태그 사이 콘텐츠 */\nconst STRIP_TAG_CONTENT = /<\\s*(script|style)\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*\\1\\s*>/gi;\n\n/** 이벤트 핸들러 속성 (onclick, onerror, onload 등) */\nconst EVENT_HANDLER_ATTRS = /\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)/gi;\n\n/** 위험한 URL 프로토콜 (href/src 속성 내) */\nconst DANGEROUS_PROTOCOL =\n /(href|src|action)\\s*=\\s*[\"']\\s*(javascript|vbscript|data\\s*:(?!image\\/))[^\"']*[\"']/gi;\n\n// ── 설정 인터페이스 ──\n\n/** 새니타이저 설정 */\nexport interface SanitizerConfig {\n /**\n * 신뢰할 수 있는 iframe origin 목록 (prefix 매칭).\n * 미지정 시 §5 config boundary 의 기본값(YouTube/Vimeo) 사용.\n */\n trustedIframeOrigins?: readonly string[];\n /** DOMPurify 사용 시 허용할 태그 화이트리스트. */\n allowedTags?: string[];\n /** DOMPurify 사용 시 허용할 속성 화이트리스트. */\n allowedAttributes?: Record<string, string[]>;\n}\n\n// ── DOMPurify 동적 로딩 (선택적 / 실제 사용되는 1차 경로) ──\n\ntype DOMPurifyLike = {\n sanitize: (dirty: string, options?: Record<string, unknown>) => string;\n};\n\nlet cachedDomPurify: DOMPurifyLike | null | undefined;\n\n/**\n * 선택적 의존성 `isomorphic-dompurify` (실제 DOM allowlist 새니타이저) 를\n * 동기적으로 로드한다. 설치되지 않았거나 로드 실패 시에만 null 을 반환하고,\n * 호출자는 regex defense-in-depth fallback 을 쓴다.\n */\nfunction tryLoadDomPurify(): DOMPurifyLike | null {\n if (cachedDomPurify !== undefined) return cachedDomPurify;\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('isomorphic-dompurify') as unknown;\n const candidate =\n (mod as { default?: DOMPurifyLike }).default ?? (mod as DOMPurifyLike);\n if (candidate && typeof candidate.sanitize === 'function') {\n cachedDomPurify = candidate;\n return candidate;\n }\n } catch {\n // 모듈 미설치 — regex fallback 사용\n }\n cachedDomPurify = null;\n return null;\n}\n\n// ── 정규식 기반 새니타이저 (defense-in-depth fallback) ──\n\nfunction regexSanitize(html: string, trustedOrigins: readonly string[]): string {\n let result = html;\n\n // 1. script/style 태그 사이 콘텐츠 제거\n result = result.replace(STRIP_TAG_CONTENT, '');\n\n // 2. 위험한 태그 제거\n result = result.replace(STRIP_TAGS_WITH_CONTENT, '');\n\n // 2b. 신뢰되지 않는 iframe 제거 (신뢰 origin 유지)\n result = result.replace(UNTRUSTED_IFRAME, (match) => {\n const srcMatch = match.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i);\n if (srcMatch) {\n const src = srcMatch[1].trim();\n if (trustedOrigins.some((origin) => src.startsWith(origin))) {\n return match;\n }\n }\n return '';\n });\n\n // 3. 이벤트 핸들러 속성 제거\n result = result.replace(EVENT_HANDLER_ATTRS, '');\n\n // 4. 위험한 URL 프로토콜 무력화\n result = result.replace(DANGEROUS_PROTOCOL, '$1=\"\"');\n\n return result;\n}\n\n// ── DOMPurify 기반 새니타이저 (1차 경로) ──\n\nfunction dompurifySanitize(\n html: string,\n purify: DOMPurifyLike,\n trustedOrigins: readonly string[],\n config: SanitizerConfig,\n): string {\n const options: Record<string, unknown> = {\n // iframe 은 hook 에서 origin 검증 후 허용\n ADD_TAGS: ['iframe'],\n ADD_ATTR: ['allowfullscreen', 'frameborder', 'allow'],\n FORBID_TAGS: [\n 'script',\n 'object',\n 'embed',\n 'applet',\n 'form',\n 'input',\n 'textarea',\n 'select',\n 'button',\n 'style',\n ],\n FORBID_ATTR: [\n 'onerror',\n 'onload',\n 'onclick',\n 'onmouseover',\n 'onfocus',\n 'onblur',\n ],\n };\n\n if (config.allowedTags) options.ALLOWED_TAGS = config.allowedTags;\n if (config.allowedAttributes) {\n const flat = new Set<string>();\n for (const attrs of Object.values(config.allowedAttributes)) {\n for (const a of attrs) flat.add(a);\n }\n options.ALLOWED_ATTR = Array.from(flat);\n }\n\n ensureIframeHook(purify);\n // sanitize 는 동기이므로 호출 동안만 활성 origin 을 노출한다.\n activeTrustedOrigins = trustedOrigins;\n try {\n return purify.sanitize(html, options);\n } finally {\n activeTrustedOrigins = null;\n }\n}\n\ntype DOMPurifyWithHooks = DOMPurifyLike & {\n addHook?: (hook: string, cb: (node: Element) => void) => void;\n};\n\n/**\n * 현재 sanitize 호출이 신뢰하는 iframe origin. 호출 밖(null)에서는 훅이\n * 아무것도 하지 않는다 — consumer 가 같은 DOMPurify 인스턴스를 직접 쓸 때\n * 우리 정책이 끼어들지 않도록.\n */\nlet activeTrustedOrigins: readonly string[] | null = null;\n\n/** 인스턴스별로 훅을 1회만 등록했는지 추적. */\nconst hookedInstances = new WeakSet<object>();\n\n/**\n * iframe origin 검증 훅을 인스턴스당 한 번만 등록한다.\n *\n * 이전 구현은 호출마다 addHook/removeHook 을 반복했는데, DOMPurify 의\n * `removeHook('uponSanitizeElement')` 는 *같은 종류의 훅을 모두* 제거하므로\n * consumer 가 공유 인스턴스에 등록한 훅까지 지워 버렸다. 이제 우리 훅은\n * 한 번 등록된 뒤 그대로 남고, consumer 훅도 건드리지 않는다.\n */\nfunction ensureIframeHook(purify: DOMPurifyLike): void {\n const p = purify as DOMPurifyWithHooks;\n if (typeof p.addHook !== 'function') return;\n if (hookedInstances.has(p)) return;\n hookedInstances.add(p);\n p.addHook('uponSanitizeElement', (node) => {\n const origins = activeTrustedOrigins;\n if (!origins) return;\n if (node.nodeName && node.nodeName.toLowerCase() === 'iframe') {\n const src = (node as Element).getAttribute?.('src') ?? '';\n const trusted = origins.some((origin) => src.startsWith(origin));\n if (!trusted) {\n (node as Element).remove?.();\n }\n }\n });\n}\n\n// ── Public API ──\n\n/**\n * 새니타이저 팩토리.\n *\n * DOMPurify(isomorphic-dompurify) 가 설치되어 있으면 그것을 *1차* 로 사용하고,\n * 없을 때만 regex defense-in-depth fallback 으로 동작한다.\n *\n * @example\n * const sanitize = createSanitizer({\n * trustedIframeOrigins: ['https://www.youtube.com/', 'https://www.loom.com/'],\n * });\n * const safe = sanitize(userHtml);\n */\nexport function createSanitizer(\n config: SanitizerConfig = {},\n): (html: string | null | undefined) => string | null {\n return function sanitize(html: string | null | undefined): string | null {\n if (!html) return html as string | null;\n // 신뢰 origin 은 point-of-use 에서 §5 boundary 를 통해 해석한다\n // (lazy — import 시점에 config 가 없어도 throw 하지 않음).\n const trustedOrigins =\n config.trustedIframeOrigins ?? resolveTrustedIframeOrigins();\n const purify = tryLoadDomPurify();\n if (purify) {\n return dompurifySanitize(html, purify, trustedOrigins, config);\n }\n return regexSanitize(html, trustedOrigins);\n };\n}\n\n/**\n * 리치 HTML 콘텐츠에서 위험한 요소와 속성을 제거한다.\n * 기본 안전 설정으로 동작한다 (하위 호환, I1). 호스트별 신뢰 origin 확장은\n * `createSanitizer({ trustedIframeOrigins: [...] })` 또는 §5\n * `setCmsConfig({ sanitizer: { trustedIframeOrigins } })` 로 한다.\n */\nexport function sanitizeHtmlContent(\n html: string | null | undefined,\n): string | null {\n return defaultSanitize(html);\n}\n\n/** 모듈 로드 시점에 1회 만들어지는 기본 새니타이저 (실행은 lazy). */\nconst defaultSanitize = createSanitizer();\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useImageDropZone
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FCWUNCGA.mjs";
|
|
4
4
|
|
|
5
5
|
// src/components/ImageDropUpload.tsx
|
|
6
6
|
import { useRef } from "react";
|
|
@@ -68,4 +68,4 @@ function ImageDropUpload({
|
|
|
68
68
|
export {
|
|
69
69
|
ImageDropUpload
|
|
70
70
|
};
|
|
71
|
-
//# sourceMappingURL=chunk-
|
|
71
|
+
//# sourceMappingURL=chunk-BG4C5KZO.mjs.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunk2VHNZWH2js = require('./chunk-2VHNZWH2.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunkJ32BTYJZjs = require('./chunk-J32BTYJZ.js');
|
|
9
9
|
|
|
10
10
|
// src/components/AdminManagerBase.tsx
|
|
11
11
|
var _react = require('react');
|
|
@@ -20,7 +20,7 @@ function AdminManagerBaseInner({
|
|
|
20
20
|
defaultFilterValue,
|
|
21
21
|
innerRef
|
|
22
22
|
}) {
|
|
23
|
-
const list =
|
|
23
|
+
const list = _chunk2VHNZWH2js.useAdminList.call(void 0, {
|
|
24
24
|
initialItems,
|
|
25
25
|
apiPath: config.apiPath,
|
|
26
26
|
defaultSortKey: config.defaultSortKey,
|
|
@@ -37,7 +37,7 @@ function AdminManagerBaseInner({
|
|
|
37
37
|
filterValue,
|
|
38
38
|
setFilterValue
|
|
39
39
|
} = list;
|
|
40
|
-
const adminForm =
|
|
40
|
+
const adminForm = _chunk2VHNZWH2js.useAdminForm.call(void 0, config.emptyForm);
|
|
41
41
|
const {
|
|
42
42
|
tab,
|
|
43
43
|
setTab,
|
|
@@ -77,7 +77,7 @@ function AdminManagerBaseInner({
|
|
|
77
77
|
const selectItem = _react.useCallback.call(void 0, async (id) => {
|
|
78
78
|
startEdit(id, config.emptyForm);
|
|
79
79
|
try {
|
|
80
|
-
const res = await
|
|
80
|
+
const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, `${config.apiPath}/${id}`);
|
|
81
81
|
const json = await res.json();
|
|
82
82
|
if (json.success) {
|
|
83
83
|
setForm(config.loadItem(json.data));
|
|
@@ -105,14 +105,14 @@ function AdminManagerBaseInner({
|
|
|
105
105
|
const method = isNew ? "POST" : "PUT";
|
|
106
106
|
setSaving(true);
|
|
107
107
|
try {
|
|
108
|
-
const res = await
|
|
108
|
+
const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, url, {
|
|
109
109
|
method,
|
|
110
|
-
headers: { ...
|
|
110
|
+
headers: { ..._chunkJ32BTYJZjs.getAuthHeaders.call(void 0, ), "Content-Type": "application/json" },
|
|
111
111
|
body: JSON.stringify(payload)
|
|
112
112
|
});
|
|
113
113
|
const json = await res.json();
|
|
114
114
|
if (json.success) {
|
|
115
|
-
const listRes = await
|
|
115
|
+
const listRes = await _chunkJ32BTYJZjs.adminFetch.call(void 0, `${config.apiPath}?limit=100&sortBy=${sortKey}`);
|
|
116
116
|
const listJson = await listRes.json();
|
|
117
117
|
if (listJson.success) {
|
|
118
118
|
setItems(listJson.data.items.map(config.normalizeListItem));
|
|
@@ -136,7 +136,7 @@ function AdminManagerBaseInner({
|
|
|
136
136
|
const label = item ? config.meta.getItemLabel(item) : id;
|
|
137
137
|
if (!confirm(`"${label}"\uC744(\uB97C) \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) return;
|
|
138
138
|
try {
|
|
139
|
-
const res = await
|
|
139
|
+
const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, `${config.apiPath}/${id}`, {
|
|
140
140
|
method: "DELETE"
|
|
141
141
|
});
|
|
142
142
|
if (res.ok) {
|
|
@@ -306,4 +306,4 @@ function AdminManagerBase(props) {
|
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
exports.AdminManagerBase = AdminManagerBase;
|
|
309
|
-
//# sourceMappingURL=chunk-
|
|
309
|
+
//# sourceMappingURL=chunk-EPVV6655.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-DCIF4XJL.js","../src/components/AdminManagerBase.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-DCIF4XJL.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useEffect, useImperativeHandle, useMemo, useRef, forwardRef } from \"react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { toast } from \"sonner\";\nimport { adminFetch, getAuthHeaders } from \"../utils/admin-fetch\";\nimport { useAdminList } from \"../hooks/useAdminList\";\nimport { useAdminForm } from \"../hooks/useAdminForm\";\nimport type { AdminManagerConfig } from \"./AdminManagerConfig\";\n\ninterface Props<TItem extends { id: string }, TForm extends object> {\n initialItems: TItem[];\n config: AdminManagerConfig<TItem, TForm>;\n /** 초기에 선택할 항목 ID (예: detail 라우트의 [id] 세그먼트) */\n initialSelectedId?: string | null;\n /** 최초 진입 시 새 항목 편집 모드로 시작할지 여부 (예: 새 항목 라우트) */\n startWithNew?: boolean;\n /** 초기 필터 값 (예: 카테고리 필터링) */\n defaultFilterValue?: string;\n}\n\nexport interface AdminManagerBaseHandle {\n selectItem: (id: string) => void;\n}\n\nfunction AdminManagerBaseInner<\n TItem extends { id: string },\n TForm extends object\n>({\n initialItems,\n config,\n initialSelectedId,\n startWithNew,\n defaultFilterValue,\n innerRef,\n}: Props<TItem, TForm> & { innerRef?: React.Ref<AdminManagerBaseHandle> }) {\n const list = useAdminList<TItem>({\n initialItems,\n apiPath: config.apiPath,\n defaultSortKey: config.defaultSortKey,\n normalizeItem: config.normalizeListItem,\n defaultFilterValue,\n });\n const {\n items,\n setItems,\n searchQuery,\n setSearchQuery,\n sortKey,\n setSortKey,\n filterValue,\n setFilterValue,\n } = list;\n\n const adminForm = useAdminForm<TForm>(config.emptyForm);\n const {\n tab,\n setTab,\n form,\n setForm,\n updateField,\n selectedId,\n setSelectedId,\n isNew,\n setIsNew,\n saving,\n setSaving,\n loading,\n setLoading,\n mobilePv,\n setMobilePv,\n addNew,\n startEdit,\n } = adminForm;\n\n useEffect(() => {\n if (startWithNew) {\n handleAdd();\n return;\n }\n if (initialSelectedId) {\n selectItem(initialSelectedId);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const filteredItems = useMemo(\n () => config.filterItems(items, filterValue, searchQuery),\n [items, filterValue, searchQuery, config]\n );\n\n const publishedItems = useMemo(\n () => items.filter(config.meta.getItemPublished),\n [items, config]\n );\n\n const selectItem = useCallback(async (id: string) => {\n startEdit(id, config.emptyForm);\n try {\n const res = await adminFetch(`${config.apiPath}/${id}`);\n const json = await res.json();\n if (json.success) {\n setForm(config.loadItem(json.data));\n } else {\n toast.error(json.error?.message || \"데이터 로드 실패\");\n }\n } catch {\n toast.error(\"데이터 로드 실패\");\n } finally {\n setLoading(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiPath, config.loadItem, config.emptyForm]);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useImperativeHandle(innerRef, () => ({ selectItem }), [selectItem]);\n\n const handleAdd = useCallback(() => {\n addNew(config.emptyForm);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.emptyForm]);\n\n async function handleSave() {\n const err = config.validate(form);\n if (err) {\n toast.error(err);\n return;\n }\n const payload = config.buildSavePayload(form, { isNew, selectedId, items });\n const url = isNew ? config.apiPath : `${config.apiPath}/${selectedId}`;\n const method = isNew ? \"POST\" : \"PUT\";\n\n setSaving(true);\n try {\n const res = await adminFetch(url, {\n method,\n headers: { ...getAuthHeaders(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n const json = await res.json();\n if (json.success) {\n const listRes = await adminFetch(`${config.apiPath}?limit=100&sortBy=${sortKey}`);\n const listJson = await listRes.json();\n if (listJson.success) {\n setItems(listJson.data.items.map(config.normalizeListItem));\n }\n setTab(\"list\");\n setSelectedId(null);\n toast.success(\"저장 완료\");\n config.onAfterSave?.();\n config.onNavigateToList?.();\n } else {\n toast.error(json.error?.message || \"저장 실패\");\n }\n } catch {\n toast.error(\"저장 중 오류 발생\");\n } finally {\n setSaving(false);\n }\n }\n\n const handleDelete = useCallback(async (id: string) => {\n const item = items.find((i) => i.id === id);\n const label = item ? config.meta.getItemLabel(item) : id;\n if (!confirm(`\"${label}\"을(를) 삭제하시겠습니까?`)) return;\n try {\n const res = await adminFetch(`${config.apiPath}/${id}`, {\n method: \"DELETE\",\n });\n if (res.ok) {\n setItems((prev) => prev.filter((i) => i.id !== id));\n if (selectedId === id) {\n setSelectedId(null);\n setTab(\"list\");\n }\n } else {\n toast.error(\"삭제에 실패했습니다.\");\n }\n } catch {\n toast.error(\"삭제 중 오류가 발생했습니다.\");\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, selectedId, config.apiPath, config.meta]);\n\n const listScrollRef = useRef<HTMLDivElement>(null);\n\n const virtualizer = useVirtualizer({\n count: filteredItems.length,\n getScrollElement: () => listScrollRef.current,\n estimateSize: () => 58,\n overscan: 5,\n });\n\n return (\n <div className={`pm${mobilePv ? \" mobile-pv-on\" : \"\"}`}>\n {/* Topbar */}\n <div className=\"pm-topbar\">\n <div className=\"pm-tb-l\">\n <span className=\"pm-tb-logo\">{config.meta.appTitle}</span>\n <span className=\"pm-tb-sep\" />\n <span className=\"pm-tb-pg\">{config.meta.pageTitle}</span>\n </div>\n </div>\n\n {/* Tabs */}\n <div className=\"pm-tabs\">\n <div\n className={`pm-tab ${tab === \"list\" ? \"on\" : \"\"}`}\n onClick={() => { setTab(\"list\"); config.onNavigateToList?.(); }}\n >\n {config.meta.listTabLabel}\n </div>\n <div\n className={`pm-tab ${tab === \"edit\" ? \"on\" : \"\"}`}\n onClick={() => setTab(\"edit\")}\n >\n 편집 + 미리보기\n </div>\n </div>\n\n {/* Mobile Preview Toggle */}\n <div className=\"mobile-pv-bar\">\n <button\n type=\"button\"\n className={`mobile-pv-btn${!mobilePv ? \" on\" : \"\"}`}\n onClick={() => setMobilePv(false)}\n >\n 편집\n </button>\n <button\n type=\"button\"\n className={`mobile-pv-btn${mobilePv ? \" on\" : \"\"}`}\n onClick={() => setMobilePv(true)}\n >\n 미리보기\n </button>\n </div>\n\n {/* Body */}\n <div className=\"pm-body\">\n {/* List Panel */}\n <div className={`pm-panel pm-panel-list ${tab === \"list\" ? \"on\" : \"\"}`}>\n <div className=\"pm-list-left\">\n <div className=\"pm-list-header\">\n {config.renderFilterControls({\n filterValue,\n setFilterValue,\n sortKey,\n setSortKey,\n sortOptions: config.sortOptions,\n searchQuery,\n setSearchQuery,\n filteredCount: filteredItems.length,\n filteredItems,\n onAdd: handleAdd,\n addButtonLabel: config.meta.addButtonLabel,\n allItems: items,\n })}\n </div>\n <div className=\"pm-perf-list\" ref={listScrollRef}>\n <div\n style={{\n height: `${virtualizer.getTotalSize()}px`,\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const item = filteredItems[virtualRow.index];\n return (\n <div\n key={item.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n transform: `translateY(${virtualRow.start}px)`,\n }}\n >\n {config.renderListItem(item, {\n isSelected: selectedId === item.id,\n onSelect: selectItem,\n onDelete: handleDelete,\n })}\n </div>\n );\n })}\n </div>\n </div>\n </div>\n <div className=\"pm-list-right\">\n <div className=\"pm-pv-label\">홈페이지 미리보기</div>\n {config.renderListPreview({\n publishedItems,\n onSelectItem: selectItem,\n })}\n </div>\n </div>\n\n {/* Edit Panel */}\n <div\n className={`pm-panel pm-panel-edit ${tab === \"edit\" ? \"on\" : \"\"}`}\n >\n <div className=\"pm-edit-left\">\n {config.renderEditForm({\n form,\n setForm,\n updateField,\n isNew,\n selectedId,\n saving,\n loading,\n onSave: handleSave,\n onCancel: () => { setTab(\"list\"); config.onNavigateToList?.(); },\n onDelete: () => { if (selectedId) handleDelete(selectedId); },\n })}\n </div>\n <div className=\"pm-edit-right\">\n <div className=\"pm-pv-label\">실시간 미리보기</div>\n {config.renderDetailPreview({ form, isNew, selectedId })}\n </div>\n </div>\n </div>\n\n {/* Domain Modal */}\n {config.renderModal?.()}\n </div>\n );\n}\n\nexport default function AdminManagerBase<\n TItem extends { id: string },\n TForm extends object\n>(props: Props<TItem, TForm> & { ref?: React.Ref<AdminManagerBaseHandle> }) {\n const { ref, ...rest } = props;\n return <AdminManagerBaseInner {...rest} innerRef={ref} />;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-EPVV6655.js","../src/components/AdminManagerBase.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-EPVV6655.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useEffect, useImperativeHandle, useMemo, useRef, forwardRef } from \"react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { toast } from \"sonner\";\nimport { adminFetch, getAuthHeaders } from \"../utils/admin-fetch\";\nimport { useAdminList } from \"../hooks/useAdminList\";\nimport { useAdminForm } from \"../hooks/useAdminForm\";\nimport type { AdminManagerConfig } from \"./AdminManagerConfig\";\n\ninterface Props<TItem extends { id: string }, TForm extends object> {\n initialItems: TItem[];\n config: AdminManagerConfig<TItem, TForm>;\n /** 초기에 선택할 항목 ID (예: detail 라우트의 [id] 세그먼트) */\n initialSelectedId?: string | null;\n /** 최초 진입 시 새 항목 편집 모드로 시작할지 여부 (예: 새 항목 라우트) */\n startWithNew?: boolean;\n /** 초기 필터 값 (예: 카테고리 필터링) */\n defaultFilterValue?: string;\n}\n\nexport interface AdminManagerBaseHandle {\n selectItem: (id: string) => void;\n}\n\nfunction AdminManagerBaseInner<\n TItem extends { id: string },\n TForm extends object\n>({\n initialItems,\n config,\n initialSelectedId,\n startWithNew,\n defaultFilterValue,\n innerRef,\n}: Props<TItem, TForm> & { innerRef?: React.Ref<AdminManagerBaseHandle> }) {\n const list = useAdminList<TItem>({\n initialItems,\n apiPath: config.apiPath,\n defaultSortKey: config.defaultSortKey,\n normalizeItem: config.normalizeListItem,\n defaultFilterValue,\n });\n const {\n items,\n setItems,\n searchQuery,\n setSearchQuery,\n sortKey,\n setSortKey,\n filterValue,\n setFilterValue,\n } = list;\n\n const adminForm = useAdminForm<TForm>(config.emptyForm);\n const {\n tab,\n setTab,\n form,\n setForm,\n updateField,\n selectedId,\n setSelectedId,\n isNew,\n setIsNew,\n saving,\n setSaving,\n loading,\n setLoading,\n mobilePv,\n setMobilePv,\n addNew,\n startEdit,\n } = adminForm;\n\n useEffect(() => {\n if (startWithNew) {\n handleAdd();\n return;\n }\n if (initialSelectedId) {\n selectItem(initialSelectedId);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const filteredItems = useMemo(\n () => config.filterItems(items, filterValue, searchQuery),\n [items, filterValue, searchQuery, config]\n );\n\n const publishedItems = useMemo(\n () => items.filter(config.meta.getItemPublished),\n [items, config]\n );\n\n const selectItem = useCallback(async (id: string) => {\n startEdit(id, config.emptyForm);\n try {\n const res = await adminFetch(`${config.apiPath}/${id}`);\n const json = await res.json();\n if (json.success) {\n setForm(config.loadItem(json.data));\n } else {\n toast.error(json.error?.message || \"데이터 로드 실패\");\n }\n } catch {\n toast.error(\"데이터 로드 실패\");\n } finally {\n setLoading(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiPath, config.loadItem, config.emptyForm]);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useImperativeHandle(innerRef, () => ({ selectItem }), [selectItem]);\n\n const handleAdd = useCallback(() => {\n addNew(config.emptyForm);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.emptyForm]);\n\n async function handleSave() {\n const err = config.validate(form);\n if (err) {\n toast.error(err);\n return;\n }\n const payload = config.buildSavePayload(form, { isNew, selectedId, items });\n const url = isNew ? config.apiPath : `${config.apiPath}/${selectedId}`;\n const method = isNew ? \"POST\" : \"PUT\";\n\n setSaving(true);\n try {\n const res = await adminFetch(url, {\n method,\n headers: { ...getAuthHeaders(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n const json = await res.json();\n if (json.success) {\n const listRes = await adminFetch(`${config.apiPath}?limit=100&sortBy=${sortKey}`);\n const listJson = await listRes.json();\n if (listJson.success) {\n setItems(listJson.data.items.map(config.normalizeListItem));\n }\n setTab(\"list\");\n setSelectedId(null);\n toast.success(\"저장 완료\");\n config.onAfterSave?.();\n config.onNavigateToList?.();\n } else {\n toast.error(json.error?.message || \"저장 실패\");\n }\n } catch {\n toast.error(\"저장 중 오류 발생\");\n } finally {\n setSaving(false);\n }\n }\n\n const handleDelete = useCallback(async (id: string) => {\n const item = items.find((i) => i.id === id);\n const label = item ? config.meta.getItemLabel(item) : id;\n if (!confirm(`\"${label}\"을(를) 삭제하시겠습니까?`)) return;\n try {\n const res = await adminFetch(`${config.apiPath}/${id}`, {\n method: \"DELETE\",\n });\n if (res.ok) {\n setItems((prev) => prev.filter((i) => i.id !== id));\n if (selectedId === id) {\n setSelectedId(null);\n setTab(\"list\");\n }\n } else {\n toast.error(\"삭제에 실패했습니다.\");\n }\n } catch {\n toast.error(\"삭제 중 오류가 발생했습니다.\");\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, selectedId, config.apiPath, config.meta]);\n\n const listScrollRef = useRef<HTMLDivElement>(null);\n\n const virtualizer = useVirtualizer({\n count: filteredItems.length,\n getScrollElement: () => listScrollRef.current,\n estimateSize: () => 58,\n overscan: 5,\n });\n\n return (\n <div className={`pm${mobilePv ? \" mobile-pv-on\" : \"\"}`}>\n {/* Topbar */}\n <div className=\"pm-topbar\">\n <div className=\"pm-tb-l\">\n <span className=\"pm-tb-logo\">{config.meta.appTitle}</span>\n <span className=\"pm-tb-sep\" />\n <span className=\"pm-tb-pg\">{config.meta.pageTitle}</span>\n </div>\n </div>\n\n {/* Tabs */}\n <div className=\"pm-tabs\">\n <div\n className={`pm-tab ${tab === \"list\" ? \"on\" : \"\"}`}\n onClick={() => { setTab(\"list\"); config.onNavigateToList?.(); }}\n >\n {config.meta.listTabLabel}\n </div>\n <div\n className={`pm-tab ${tab === \"edit\" ? \"on\" : \"\"}`}\n onClick={() => setTab(\"edit\")}\n >\n 편집 + 미리보기\n </div>\n </div>\n\n {/* Mobile Preview Toggle */}\n <div className=\"mobile-pv-bar\">\n <button\n type=\"button\"\n className={`mobile-pv-btn${!mobilePv ? \" on\" : \"\"}`}\n onClick={() => setMobilePv(false)}\n >\n 편집\n </button>\n <button\n type=\"button\"\n className={`mobile-pv-btn${mobilePv ? \" on\" : \"\"}`}\n onClick={() => setMobilePv(true)}\n >\n 미리보기\n </button>\n </div>\n\n {/* Body */}\n <div className=\"pm-body\">\n {/* List Panel */}\n <div className={`pm-panel pm-panel-list ${tab === \"list\" ? \"on\" : \"\"}`}>\n <div className=\"pm-list-left\">\n <div className=\"pm-list-header\">\n {config.renderFilterControls({\n filterValue,\n setFilterValue,\n sortKey,\n setSortKey,\n sortOptions: config.sortOptions,\n searchQuery,\n setSearchQuery,\n filteredCount: filteredItems.length,\n filteredItems,\n onAdd: handleAdd,\n addButtonLabel: config.meta.addButtonLabel,\n allItems: items,\n })}\n </div>\n <div className=\"pm-perf-list\" ref={listScrollRef}>\n <div\n style={{\n height: `${virtualizer.getTotalSize()}px`,\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const item = filteredItems[virtualRow.index];\n return (\n <div\n key={item.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n transform: `translateY(${virtualRow.start}px)`,\n }}\n >\n {config.renderListItem(item, {\n isSelected: selectedId === item.id,\n onSelect: selectItem,\n onDelete: handleDelete,\n })}\n </div>\n );\n })}\n </div>\n </div>\n </div>\n <div className=\"pm-list-right\">\n <div className=\"pm-pv-label\">홈페이지 미리보기</div>\n {config.renderListPreview({\n publishedItems,\n onSelectItem: selectItem,\n })}\n </div>\n </div>\n\n {/* Edit Panel */}\n <div\n className={`pm-panel pm-panel-edit ${tab === \"edit\" ? \"on\" : \"\"}`}\n >\n <div className=\"pm-edit-left\">\n {config.renderEditForm({\n form,\n setForm,\n updateField,\n isNew,\n selectedId,\n saving,\n loading,\n onSave: handleSave,\n onCancel: () => { setTab(\"list\"); config.onNavigateToList?.(); },\n onDelete: () => { if (selectedId) handleDelete(selectedId); },\n })}\n </div>\n <div className=\"pm-edit-right\">\n <div className=\"pm-pv-label\">실시간 미리보기</div>\n {config.renderDetailPreview({ form, isNew, selectedId })}\n </div>\n </div>\n </div>\n\n {/* Domain Modal */}\n {config.renderModal?.()}\n </div>\n );\n}\n\nexport default function AdminManagerBase<\n TItem extends { id: string },\n TForm extends object\n>(props: Props<TItem, TForm> & { ref?: React.Ref<AdminManagerBaseHandle> }) {\n const { ref, ...rest } = props;\n return <AdminManagerBaseInner {...rest} innerRef={ref} />;\n}\n"]}
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
} from "./chunk-J5N76ASB.mjs";
|
|
5
5
|
import {
|
|
6
6
|
adminFetch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-47DPXAO6.mjs";
|
|
8
8
|
import {
|
|
9
9
|
resolveRouteConfig
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3UXX27N5.mjs";
|
|
11
11
|
|
|
12
12
|
// src/hooks/useImageDropZone.ts
|
|
13
13
|
import { useState, useCallback, useRef } from "react";
|
|
@@ -171,4 +171,4 @@ function useImageDropZone(options) {
|
|
|
171
171
|
export {
|
|
172
172
|
useImageDropZone
|
|
173
173
|
};
|
|
174
|
-
//# sourceMappingURL=chunk-
|
|
174
|
+
//# sourceMappingURL=chunk-FCWUNCGA.mjs.map
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
namespacedError,
|
|
3
3
|
resolveR2CredentialsConfig,
|
|
4
4
|
resolveR2PublicUrl
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-3UXX27N5.mjs";
|
|
6
6
|
|
|
7
7
|
// src/utils/r2-storage.ts
|
|
8
8
|
import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
@@ -151,4 +151,4 @@ export {
|
|
|
151
151
|
uploadImageWithVariants,
|
|
152
152
|
deleteFromR2
|
|
153
153
|
};
|
|
154
|
-
//# sourceMappingURL=chunk-
|
|
154
|
+
//# sourceMappingURL=chunk-GQJNLE34.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
|
|
4
4
|
|
|
5
5
|
// src/utils/jwt.ts
|
|
6
6
|
var _jwt = require('@withwiz/toolkit/core/auth/jwt');
|
|
@@ -8,7 +8,7 @@ var _logger = require('@withwiz/toolkit/core/logger/logger');
|
|
|
8
8
|
var _jwtManager = null;
|
|
9
9
|
function getJWTManager() {
|
|
10
10
|
if (!_jwtManager) {
|
|
11
|
-
const { secret, accessTokenExpiry, refreshTokenExpiry, algorithm } =
|
|
11
|
+
const { secret, accessTokenExpiry, refreshTokenExpiry, algorithm } = _chunkUVZD7GHGjs.resolveJwtConfig.call(void 0, );
|
|
12
12
|
_jwtManager = new (0, _jwt.JWTManager)(
|
|
13
13
|
{
|
|
14
14
|
secret,
|
|
@@ -31,4 +31,4 @@ function getJWTManager() {
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
exports.getJWTManager = getJWTManager;
|
|
34
|
-
//# sourceMappingURL=chunk-
|
|
34
|
+
//# sourceMappingURL=chunk-HVB72GT3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-HVB72GT3.js","../src/utils/jwt.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACA;ACJA,qDAA2B;AAC3B,6DAAkC;AAGlC,IAAI,YAAA,EAAiC,IAAA;AAgB9B,SAAS,aAAA,CAAA,EAA4B;AAC1C,EAAA,GAAA,CAAI,CAAC,WAAA,EAAa;AAChB,IAAA,MAAM,EAAE,MAAA,EAAQ,iBAAA,EAAmB,kBAAA,EAAoB,UAAU,EAAA,EAC/D,+CAAA,CAAiB;AACnB,IAAA,YAAA,EAAc,IAAI,oBAAA;AAAA,MAChB;AAAA,QACE,MAAA;AAAA,QACA,iBAAA;AAAA,QACA,kBAAA;AAAA,QACA;AAAA,MACF,CAAA;AAAA,MACA;AAAA,QACE,KAAA,EAAO,CAAA,EAAA,GAAM;AAAA,QAAC,CAAA;AAAA,QACd,IAAA,EAAM,CAAC,GAAA,EAAa,IAAA,EAAA,GAAe,6BAAA,CAAQ,MAAA,EAAS,GAAG,CAAA,CAAA;AACA,QAAA;AACD,QAAA;AACxD,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;ADXkD;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-HVB72GT3.js","sourcesContent":[null,"import { JWTManager } from '@withwiz/toolkit/core/auth/jwt';\nimport { logInfo, logError } from '@withwiz/toolkit/core/logger/logger';\nimport { resolveJwtConfig } from '../config';\n\nlet _jwtManager: JWTManager | null = null;\n\n/**\n * JWT 매니저 싱글턴 (spec.md §4.6 / §4.3 / Sprint 1 S3).\n *\n * secret/expiry/algorithm 은 §5 config boundary (`resolveJwtConfig`) 를 통해\n * 해석된다: 명시적 주입 > `process.env.JWT_SECRET` 레거시 fallback > (서명\n * 비밀에는 안전한 기본값이 없음 → point-of-use fail-fast).\n *\n * - 비밀 누락: `@withwiz/cms-kit:` 네임스페이스 에러로 즉시 throw (undefined 로\n * 조용히 JWTManager 를 만들지 않는다).\n * - 비밀 취약(<32자, 문서화 정책): `@withwiz/cms-kit:` 네임스페이스 에러로 거부\n * (forgeable 서명 키는 unsafe-no-safe-default).\n * - import 만으로는 절대 throw 하지 않는다 (lazy / point-of-use).\n * 기존의 JWT_SECRET non-null 단정(bang) 직접 접근은 제거되었다.\n */\nexport function getJWTManager(): JWTManager {\n if (!_jwtManager) {\n const { secret, accessTokenExpiry, refreshTokenExpiry, algorithm } =\n resolveJwtConfig();\n _jwtManager = new JWTManager(\n {\n secret,\n accessTokenExpiry,\n refreshTokenExpiry,\n algorithm,\n },\n {\n debug: () => {},\n info: (msg: string, meta?: any) => logInfo(`[JWT] ${msg}`, meta),\n warn: (msg: string, meta?: any) => logInfo(`[JWT] ${msg}`, meta),\n error: (msg: string, meta?: any) => logError(`[JWT] ${msg}`, meta),\n },\n );\n }\n return _jwtManager;\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
|
|
4
4
|
|
|
5
5
|
// src/utils/admin-fetch.ts
|
|
6
6
|
var isRefreshing = false;
|
|
@@ -13,7 +13,7 @@ async function refreshAccessToken() {
|
|
|
13
13
|
}
|
|
14
14
|
async function tryRefreshCookie() {
|
|
15
15
|
try {
|
|
16
|
-
const { refreshEndpoint } =
|
|
16
|
+
const { refreshEndpoint } = _chunkUVZD7GHGjs.resolveRouteConfig.call(void 0, );
|
|
17
17
|
const res = await fetch(refreshEndpoint, {
|
|
18
18
|
method: "POST",
|
|
19
19
|
credentials: "same-origin"
|
|
@@ -46,7 +46,7 @@ async function adminFetch(url, options = {}) {
|
|
|
46
46
|
if (refreshed) {
|
|
47
47
|
return fetch(url, { ...options, credentials: "same-origin" });
|
|
48
48
|
}
|
|
49
|
-
const { loginPath } =
|
|
49
|
+
const { loginPath } = _chunkUVZD7GHGjs.resolveRouteConfig.call(void 0, );
|
|
50
50
|
window.location.href = loginPath;
|
|
51
51
|
}
|
|
52
52
|
return res;
|
|
@@ -57,4 +57,4 @@ async function adminFetch(url, options = {}) {
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
exports.getAuthHeaders = getAuthHeaders; exports.refreshAccessToken = refreshAccessToken; exports.adminFetch = adminFetch;
|
|
60
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
//# sourceMappingURL=chunk-J32BTYJZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-J32BTYJZ.js","../src/utils/admin-fetch.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACA;ACFA,IAAI,aAAA,EAAe,KAAA;AACnB,IAAI,eAAA,EAA0C,IAAA;AAKvC,SAAS,cAAA,CAAA,EAAyC;AACvD,EAAA,OAAO,CAAC,CAAA;AACV;AAKA,MAAA,SAAsB,kBAAA,CAAA,EAAuC;AAC3D,EAAA,OAAO,gBAAA,CAAiB,CAAA;AAC1B;AAEA,MAAA,SAAe,gBAAA,CAAA,EAAqC;AAClD,EAAA,IAAI;AAGF,IAAA,MAAM,EAAE,gBAAgB,EAAA,EAAI,iDAAA,CAAmB;AAC/C,IAAA,MAAM,IAAA,EAAM,MAAM,KAAA,CAAM,eAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,MAAA;AAAA,MACR,WAAA,EAAa;AAAA,IACf,CAAC,CAAA;AAED,IAAA,GAAA,CAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,KAAA;AAEpB,IAAA,MAAM,KAAA,EAAO,MAAM,GAAA,CAAI,IAAA,CAAK,CAAA;AAC5B,IAAA,OAAO,IAAA,CAAK,QAAA,IAAY,IAAA;AAAA,EAC1B,EAAA,UAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,MAAA,SAAe,UAAA,CAAA,EAA+B;AAC5C,EAAA,GAAA,CAAI,aAAA,GAAgB,cAAA,EAAgB;AAClC,IAAA,OAAO,cAAA;AAAA,EACT;AAEA,EAAA,aAAA,EAAe,IAAA;AACf,EAAA,eAAA,EAAiB,gBAAA,CAAiB,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAA,GAAM;AAChD,IAAA,aAAA,EAAe,KAAA;AACf,IAAA,eAAA,EAAiB,IAAA;AAAA,EACnB,CAAC,CAAA;AAED,EAAA,OAAO,cAAA;AACT;AAEA,MAAA,SAAsB,UAAA,CACpB,GAAA,EACA,QAAA,EAAuB,CAAC,CAAA,EACL;AACnB,EAAA,MAAM,IAAA,EAAM,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAC3B,GAAG,OAAA;AAAA,IACH,WAAA,EAAa;AAAA,EACf,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAO,OAAA,IAAW,WAAA,EAAa;AACvD,IAAA,MAAM,UAAA,EAAY,MAAM,UAAA,CAAW,CAAA;AAEnC,IAAA,GAAA,CAAI,SAAA,EAAW;AACb,MAAA,OAAO,KAAA,CAAM,GAAA,EAAK,EAAE,GAAG,OAAA,EAAS,WAAA,EAAa,cAAc,CAAC,CAAA;AAAA,IAC9D;AAGA,IAAA,MAAM,EAAE,UAAU,EAAA,EAAI,iDAAA,CAAmB;AACzC,IAAA,MAAA,CAAO,QAAA,CAAS,KAAA,EAAO,SAAA;AAAA,EACzB;AAEA,EAAA,OAAO,GAAA;AACT;ADrBA;AACA;AACE;AACA;AACA;AACF,0HAAC","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-J32BTYJZ.js","sourcesContent":[null,"import { resolveRouteConfig } from '../config';\n\nlet isRefreshing = false;\nlet refreshPromise: Promise<boolean> | null = null;\n\n/**\n * @deprecated Auth is now handled via httpOnly cookies. Returns empty headers.\n */\nexport function getAuthHeaders(): Record<string, string> {\n return {};\n}\n\n/**\n * @deprecated Auth is now handled via httpOnly cookies.\n */\nexport async function refreshAccessToken(): Promise<boolean> {\n return tryRefreshCookie();\n}\n\nasync function tryRefreshCookie(): Promise<boolean> {\n try {\n // refresh 엔드포인트는 §5 config boundary 에서 해석된다 (consumer\n // override 가능; 미설정 시 레거시 기본 경로 — spec.md §4.1 C2 / B5).\n const { refreshEndpoint } = resolveRouteConfig();\n const res = await fetch(refreshEndpoint, {\n method: 'POST',\n credentials: 'same-origin',\n });\n\n if (!res.ok) return false;\n\n const data = await res.json();\n return data.success === true;\n } catch {\n return false;\n }\n}\n\nasync function tryRefresh(): Promise<boolean> {\n if (isRefreshing && refreshPromise) {\n return refreshPromise;\n }\n\n isRefreshing = true;\n refreshPromise = tryRefreshCookie().finally(() => {\n isRefreshing = false;\n refreshPromise = null;\n });\n\n return refreshPromise;\n}\n\nexport async function adminFetch(\n url: string,\n options: RequestInit = {},\n): Promise<Response> {\n const res = await fetch(url, {\n ...options,\n credentials: 'same-origin',\n });\n\n if (res.status === 401 && typeof window !== 'undefined') {\n const refreshed = await tryRefresh();\n\n if (refreshed) {\n return fetch(url, { ...options, credentials: 'same-origin' });\n }\n\n // login 페이지 경로도 §5 config boundary 에서 해석된다.\n const { loginPath } = resolveRouteConfig();\n window.location.href = loginPath;\n }\n\n return res;\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkJ32BTYJZjs = require('./chunk-J32BTYJZ.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
|
|
8
8
|
|
|
9
9
|
// src/components/AdminShell.tsx
|
|
10
10
|
var _react = require('react');
|
|
@@ -31,8 +31,8 @@ function AdminShell({
|
|
|
31
31
|
const routerRef = _react.useRef.call(void 0, router);
|
|
32
32
|
routerRef.current = router;
|
|
33
33
|
const brandSuppliedViaProps = brandLabel !== void 0 || navItems !== void 0;
|
|
34
|
-
const brandCfg =
|
|
35
|
-
const routeCfg =
|
|
34
|
+
const brandCfg = _chunkUVZD7GHGjs.resolveBrandConfig.call(void 0, brandSuppliedViaProps);
|
|
35
|
+
const routeCfg = _chunkUVZD7GHGjs.resolveRouteConfig.call(void 0, );
|
|
36
36
|
const resolvedBrandLabel = _nullishCoalesce(_nullishCoalesce(brandLabel, () => ( brandCfg.brandLabel)), () => ( null));
|
|
37
37
|
const resolvedBrandHref = _nullishCoalesce(brandHref, () => ( brandCfg.brandHref));
|
|
38
38
|
const resolvedAdminHref = _nullishCoalesce(adminHref, () => ( brandCfg.adminHref));
|
|
@@ -71,7 +71,7 @@ function AdminShell({
|
|
|
71
71
|
let cancelled = false;
|
|
72
72
|
async function checkAuth() {
|
|
73
73
|
try {
|
|
74
|
-
const res = await
|
|
74
|
+
const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, resolvedMeEndpoint);
|
|
75
75
|
if (cancelled) return;
|
|
76
76
|
if (!res.ok) {
|
|
77
77
|
routerRef.current.replace(resolvedLoginPath);
|
|
@@ -215,4 +215,4 @@ function AdminShell({
|
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
exports.AdminShell = AdminShell;
|
|
218
|
-
//# sourceMappingURL=chunk-
|
|
218
|
+
//# sourceMappingURL=chunk-JPIQU2C4.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-C5WFZJM4.js","../src/components/AdminShell.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-C5WFZJM4.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\n\nconst Toaster = dynamic(\n () => import(\"sonner\").then((m) => m.Toaster),\n { ssr: false }\n);\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resolveBrandConfig,\n resolveRouteConfig,\n type CmsNavItem,\n} from \"../config\";\n\ninterface AdminUser {\n id: string;\n email: string;\n name: string | null;\n role: string;\n}\n\n/**\n * AdminShell props (spec.md §4.1 / Sprint 1 C1/C2).\n *\n * brand/nav/route 는 더 이상 하드코딩되지 않는다. 우선순위:\n * props > §5 config boundary (`setCmsConfig`) > 안전 중립 기본값\n * (빈 nav + 1회 `@withwiz/cms-kit:` warn, route 는 레거시 기본 경로).\n */\nexport interface AdminShellProps {\n children: React.ReactNode;\n /** 브랜드 라벨 (사이드바 홈 링크 텍스트). 미지정 시 §5 config. */\n brandLabel?: string;\n /** 브랜드 홈 링크 href. */\n brandHref?: string;\n /** admin 링크 href. */\n adminHref?: string;\n /** 순서 있는 nav 항목 목록 (label/href/glyph). */\n navItems?: CmsNavItem[];\n /** 로그인 페이지 경로 (라우트 가드/리다이렉트). */\n loginPath?: string;\n /** \"현재 사용자\" 엔드포인트. */\n meEndpoint?: string;\n /** 로그아웃 엔드포인트. */\n logoutEndpoint?: string;\n}\n\nexport default function AdminShell({\n children,\n brandLabel,\n brandHref,\n adminHref,\n navItems,\n loginPath,\n meEndpoint,\n logoutEndpoint,\n}: AdminShellProps) {\n const pathname = usePathname();\n const router = useRouter();\n const routerRef = useRef(router);\n routerRef.current = router;\n\n // props > §5 config boundary > safe default. The single\n // @withwiz/cms-kit-namespaced warn-once fires ONLY when neither props NOR §5\n // config supply brand/nav (props are a valid injection — suppress warn).\n const brandSuppliedViaProps =\n brandLabel !== undefined || navItems !== undefined;\n const brandCfg = resolveBrandConfig(brandSuppliedViaProps);\n const routeCfg = resolveRouteConfig();\n const resolvedBrandLabel =\n brandLabel ?? brandCfg.brandLabel ?? null;\n const resolvedBrandHref = brandHref ?? brandCfg.brandHref;\n const resolvedAdminHref = adminHref ?? brandCfg.adminHref;\n const resolvedNav: CmsNavItem[] = navItems ?? brandCfg.navItems;\n const resolvedLoginPath = loginPath ?? routeCfg.loginPath;\n const resolvedMeEndpoint = meEndpoint ?? routeCfg.meEndpoint;\n const resolvedLogoutEndpoint = logoutEndpoint ?? routeCfg.logoutEndpoint;\n\n const [checking, setChecking] = useState(true);\n const [user, setUser] = useState<AdminUser | null>(null);\n const [mobileOpen, setMobileOpen] = useState(false);\n const [collapsed, setCollapsed] = useState(() => {\n if (typeof window !== \"undefined\") {\n return localStorage.getItem(\"admin_sidebar_collapsed\") === \"true\";\n }\n return false;\n });\n\n const DEFAULT_WIDTH = 200;\n const MIN_WIDTH = 200;\n const MAX_WIDTH = 400;\n\n const [sidebarWidth, setSidebarWidth] = useState(() => {\n if (typeof window !== \"undefined\") {\n const saved = localStorage.getItem(\"admin_sidebar_width\");\n return saved ? Number(saved) : DEFAULT_WIDTH;\n }\n return DEFAULT_WIDTH;\n });\n const isResizing = useRef(false);\n const widthRef = useRef(sidebarWidth);\n const [dragging, setDragging] = useState(false);\n\n const isLoginPage = pathname === resolvedLoginPath;\n\n useEffect(() => {\n if (isLoginPage) {\n setChecking(false);\n return;\n }\n\n let cancelled = false;\n\n async function checkAuth() {\n try {\n const res = await adminFetch(resolvedMeEndpoint);\n if (cancelled) return;\n\n if (!res.ok) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n\n const data = await res.json();\n if (data.success && data.data?.user) {\n const u = data.data.user as AdminUser;\n setUser((prev) =>\n prev?.email === u.email && prev?.id === u.id ? prev : u\n );\n }\n } catch {\n if (!cancelled) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n }\n\n setChecking(false);\n }\n\n checkAuth();\n return () => { cancelled = true; };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoginPage]);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n if (!isResizing.current) return;\n const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));\n widthRef.current = newWidth;\n setSidebarWidth(newWidth);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n if (!isResizing.current) return;\n isResizing.current = false;\n setDragging(false);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n localStorage.setItem(\"admin_sidebar_width\", String(widthRef.current));\n document.removeEventListener(\"mousemove\", handleMouseMove);\n document.removeEventListener(\"mouseup\", handleMouseUp);\n }, [handleMouseMove]);\n\n if (isLoginPage) {\n return <>{children}</>;\n }\n\n if (checking) {\n return <div className=\"admin-auth-loading\">인증 확인 중...</div>;\n }\n\n async function handleLogout() {\n await fetch(resolvedLogoutEndpoint, { method: \"POST\", credentials: \"same-origin\" });\n router.replace(resolvedLoginPath);\n }\n\n function toggleSidebar() {\n setCollapsed((prev) => {\n const next = !prev;\n localStorage.setItem(\"admin_sidebar_collapsed\", String(next));\n return next;\n });\n }\n\n function startResize(e: React.MouseEvent) {\n e.preventDefault();\n isResizing.current = true;\n setDragging(true);\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n document.addEventListener(\"mousemove\", handleMouseMove);\n document.addEventListener(\"mouseup\", handleMouseUp);\n }\n\n return (\n <div className={`admin-layout${collapsed ? \" admin-sidebar-collapsed\" : \"\"}${dragging ? \" admin-resizing\" : \"\"}${mobileOpen ? \" admin-sidebar-mobile-open\" : \"\"}`}>\n <button\n className=\"admin-mobile-toggle\"\n onClick={() => setMobileOpen(true)}\n aria-label=\"메뉴 열기\"\n >\n ☰\n </button>\n {mobileOpen && (\n <div className=\"admin-sidebar-overlay\" onClick={() => setMobileOpen(false)} />\n )}\n <aside\n className=\"admin-sidebar\"\n style={!collapsed ? { width: sidebarWidth } : undefined}\n >\n <div className=\"admin-sidebar-header\">\n {!collapsed && (\n <div className=\"admin-sidebar-logo\">\n {resolvedBrandLabel && (\n <a href={resolvedBrandHref} className=\"admin-logo-home\" title=\"사이트 보기\" target=\"_blank\" rel=\"noopener noreferrer\">{resolvedBrandLabel}</a>\n )}\n <Link href={resolvedAdminHref} className=\"admin-logo-admin\">Admin</Link>\n </div>\n )}\n <button\n className=\"admin-sidebar-toggle\"\n onClick={() => {\n if (mobileOpen) {\n setMobileOpen(false);\n } else {\n toggleSidebar();\n }\n }}\n title={collapsed ? \"메뉴 펼치기\" : \"메뉴 접기\"}\n >\n {mobileOpen ? \"✕\" : collapsed ? \"›\" : \"‹\"}\n </button>\n </div>\n {!collapsed && user && (\n <div className=\"admin-sidebar-user\">{user.email}</div>\n )}\n <nav className=\"admin-sidebar-nav\">\n {resolvedNav.map((item) => (\n <Link\n key={item.href}\n href={item.href}\n className=\"admin-sidebar-link\"\n title={item.label}\n onClick={() => setMobileOpen(false)}\n >\n {collapsed ? item.glyph : item.label}\n </Link>\n ))}\n </nav>\n <div className=\"admin-sidebar-footer\">\n <button className=\"admin-sidebar-logout\" onClick={handleLogout} title=\"로그아웃\">\n {collapsed ? \"✕\" : \"로그아웃\"}\n </button>\n </div>\n {!collapsed && (\n <div\n className=\"admin-sidebar-resize\"\n onMouseDown={startResize}\n />\n )}\n </aside>\n <main\n className=\"admin-main\"\n style={!collapsed ? { marginLeft: sidebarWidth, width: `calc(100vw - ${sidebarWidth}px)` } : undefined}\n >{children}</main>\n <Toaster position=\"top-center\" richColors closeButton duration={3000} />\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-JPIQU2C4.js","../src/components/AdminShell.tsx"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACA;AACF,sDAA4B;AAC5B;AACA;ACNA,8BAAyD;AACzD,6EAAiB;AACjB,6CAAuC;AACvC,yFAAoB;AAkKT,+CAAA;AAhKX,IAAM,QAAA,EAAU,+BAAA;AAAA,EACd,CAAA,EAAA,GAAM,4DAAA,CAAO,QAAQ,GAAA,CAAE,IAAA,CAAK,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,OAAO,CAAA;AAAA,EAC5C,EAAE,GAAA,EAAK,MAAM;AACf,CAAA;AAwCe,SAAR,UAAA,CAA4B;AAAA,EACjC,QAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAAoB;AAClB,EAAA,MAAM,SAAA,EAAW,qCAAA,CAAY;AAC7B,EAAA,MAAM,OAAA,EAAS,mCAAA,CAAU;AACzB,EAAA,MAAM,UAAA,EAAY,2BAAA,MAAa,CAAA;AAC/B,EAAA,SAAA,CAAU,QAAA,EAAU,MAAA;AAKpB,EAAA,MAAM,sBAAA,EACJ,WAAA,IAAe,KAAA,EAAA,GAAa,SAAA,IAAa,KAAA,CAAA;AAC3C,EAAA,MAAM,SAAA,EAAW,iDAAA,qBAAwC,CAAA;AACzD,EAAA,MAAM,SAAA,EAAW,iDAAA,CAAmB;AACpC,EAAA,MAAM,mBAAA,oCACJ,UAAA,UAAc,QAAA,CAAS,YAAA,UAAc,MAAA;AACvC,EAAA,MAAM,kBAAA,mBAAoB,SAAA,UAAa,QAAA,CAAS,WAAA;AAChD,EAAA,MAAM,kBAAA,mBAAoB,SAAA,UAAa,QAAA,CAAS,WAAA;AAChD,EAAA,MAAM,YAAA,mBAA4B,QAAA,UAAY,QAAA,CAAS,UAAA;AACvD,EAAA,MAAM,kBAAA,mBAAoB,SAAA,UAAa,QAAA,CAAS,WAAA;AAChD,EAAA,MAAM,mBAAA,mBAAqB,UAAA,UAAc,QAAA,CAAS,YAAA;AAClD,EAAA,MAAM,uBAAA,mBAAyB,cAAA,UAAkB,QAAA,CAAS,gBAAA;AAE1D,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,EAAA,EAAI,6BAAA,IAAa,CAAA;AAC7C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,EAAA,EAAI,6BAAA,IAA+B,CAAA;AACvD,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,EAAA,EAAI,6BAAA,KAAc,CAAA;AAClD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,EAAA,EAAI,6BAAA,CAAS,EAAA,GAAM;AAC/C,IAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa;AACjC,MAAA,OAAO,YAAA,CAAa,OAAA,CAAQ,yBAAyB,EAAA,IAAM,MAAA;AAAA,IAC7D;AACA,IAAA,OAAO,KAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,MAAM,cAAA,EAAgB,GAAA;AACtB,EAAA,MAAM,UAAA,EAAY,GAAA;AAClB,EAAA,MAAM,UAAA,EAAY,GAAA;AAElB,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,EAAA,EAAI,6BAAA,CAAS,EAAA,GAAM;AACrD,IAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa;AACjC,MAAA,MAAM,MAAA,EAAQ,YAAA,CAAa,OAAA,CAAQ,qBAAqB,CAAA;AACxD,MAAA,OAAO,MAAA,EAAQ,MAAA,CAAO,KAAK,EAAA,EAAI,aAAA;AAAA,IACjC;AACA,IAAA,OAAO,aAAA;AAAA,EACT,CAAC,CAAA;AACD,EAAA,MAAM,WAAA,EAAa,2BAAA,KAAY,CAAA;AAC/B,EAAA,MAAM,SAAA,EAAW,2BAAA,YAAmB,CAAA;AACpC,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,EAAA,EAAI,6BAAA,KAAc,CAAA;AAE9C,EAAA,MAAM,YAAA,EAAc,SAAA,IAAa,iBAAA;AAEjC,EAAA,8BAAA,CAAU,EAAA,GAAM;AACd,IAAA,GAAA,CAAI,WAAA,EAAa;AACf,MAAA,WAAA,CAAY,KAAK,CAAA;AACjB,MAAA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,UAAA,EAAY,KAAA;AAEhB,IAAA,MAAA,SAAe,SAAA,CAAA,EAAY;AACzB,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,EAAM,MAAM,yCAAA,kBAA6B,CAAA;AAC/C,QAAA,GAAA,CAAI,SAAA,EAAW,MAAA;AAEf,QAAA,GAAA,CAAI,CAAC,GAAA,CAAI,EAAA,EAAI;AACX,UAAA,SAAA,CAAU,OAAA,CAAQ,OAAA,CAAQ,iBAAiB,CAAA;AAC3C,UAAA,MAAA;AAAA,QACF;AAEA,QAAA,MAAM,KAAA,EAAO,MAAM,GAAA,CAAI,IAAA,CAAK,CAAA;AAC5B,QAAA,GAAA,CAAI,IAAA,CAAK,QAAA,mBAAW,IAAA,mBAAK,IAAA,6BAAM,MAAA,EAAM;AACnC,UAAA,MAAM,EAAA,EAAI,IAAA,CAAK,IAAA,CAAK,IAAA;AACpB,UAAA,OAAA;AAAA,YAAQ,CAAC,IAAA,EAAA,mBACP,IAAA,6BAAM,QAAA,IAAU,CAAA,CAAE,MAAA,mBAAS,IAAA,6BAAM,KAAA,IAAO,CAAA,CAAE,GAAA,EAAK,KAAA,EAAO;AAAA,UACxD,CAAA;AAAA,QACF;AAAA,MACF,EAAA,WAAQ;AACN,QAAA,GAAA,CAAI,CAAC,SAAA,EAAW;AACd,UAAA,SAAA,CAAU,OAAA,CAAQ,OAAA,CAAQ,iBAAiB,CAAA;AAC3C,UAAA,MAAA;AAAA,QACF;AAAA,MACF;AAEA,MAAA,WAAA,CAAY,KAAK,CAAA;AAAA,IACnB;AAEA,IAAA,SAAA,CAAU,CAAA;AACV,IAAA,OAAO,CAAA,EAAA,GAAM;AAAE,MAAA,UAAA,EAAY,IAAA;AAAA,IAAM,CAAA;AAAA,EAEnC,CAAA,EAAG,CAAC,WAAW,CAAC,CAAA;AAEhB,EAAA,MAAM,gBAAA,EAAkB,gCAAA,CAAa,CAAA,EAAA,GAAkB;AACrD,IAAA,GAAA,CAAI,CAAC,UAAA,CAAW,OAAA,EAAS,MAAA;AACzB,IAAA,MAAM,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,CAAA,CAAE,OAAO,CAAC,CAAA;AACnE,IAAA,QAAA,CAAS,QAAA,EAAU,QAAA;AACnB,IAAA,eAAA,CAAgB,QAAQ,CAAA;AAAA,EAC1B,CAAA,EAAG,CAAC,CAAC,CAAA;AAEL,EAAA,MAAM,cAAA,EAAgB,gCAAA,CAAY,EAAA,GAAM;AACtC,IAAA,GAAA,CAAI,CAAC,UAAA,CAAW,OAAA,EAAS,MAAA;AACzB,IAAA,UAAA,CAAW,QAAA,EAAU,KAAA;AACrB,IAAA,WAAA,CAAY,KAAK,CAAA;AACjB,IAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,OAAA,EAAS,EAAA;AAC7B,IAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,EAAA;AACjC,IAAA,YAAA,CAAa,OAAA,CAAQ,qBAAA,EAAuB,MAAA,CAAO,QAAA,CAAS,OAAO,CAAC,CAAA;AACpE,IAAA,QAAA,CAAS,mBAAA,CAAoB,WAAA,EAAa,eAAe,CAAA;AACzD,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,aAAa,CAAA;AAAA,EACvD,CAAA,EAAG,CAAC,eAAe,CAAC,CAAA;AAEpB,EAAA,GAAA,CAAI,WAAA,EAAa;AACf,IAAA,uBAAO,6BAAA,oBAAA,EAAA,EAAG,SAAA,CAAS,CAAA;AAAA,EACrB;AAEA,EAAA,GAAA,CAAI,QAAA,EAAU;AACZ,IAAA,uBAAO,6BAAA,KAAC,EAAA,EAAI,SAAA,EAAU,oBAAA,EAAqB,QAAA,EAAA,sCAAA,CAAU,CAAA;AAAA,EACvD;AAEA,EAAA,MAAA,SAAe,YAAA,CAAA,EAAe;AAC5B,IAAA,MAAM,KAAA,CAAM,sBAAA,EAAwB,EAAE,MAAA,EAAQ,MAAA,EAAQ,WAAA,EAAa,cAAc,CAAC,CAAA;AAClF,IAAA,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA;AAAA,EAClC;AAEA,EAAA,SAAS,aAAA,CAAA,EAAgB;AACvB,IAAA,YAAA,CAAa,CAAC,IAAA,EAAA,GAAS;AACrB,MAAA,MAAM,KAAA,EAAO,CAAC,IAAA;AACd,MAAA,YAAA,CAAa,OAAA,CAAQ,yBAAA,EAA2B,MAAA,CAAO,IAAI,CAAC,CAAA;AAC5D,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,SAAS,WAAA,CAAY,CAAA,EAAqB;AACxC,IAAA,CAAA,CAAE,cAAA,CAAe,CAAA;AACjB,IAAA,UAAA,CAAW,QAAA,EAAU,IAAA;AACrB,IAAA,WAAA,CAAY,IAAI,CAAA;AAChB,IAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,OAAA,EAAS,YAAA;AAC7B,IAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,MAAA;AACjC,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAA,EAAa,eAAe,CAAA;AACtD,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,aAAa,CAAA;AAAA,EACpD;AAEA,EAAA,uBACE,8BAAA,KAAC,EAAA,EAAI,SAAA,EAAW,CAAA,YAAA,EAAe,UAAA,EAAY,2BAAA,EAA6B,EAAE,CAAA,EAAA;AACxE,oBAAA;AAAC,MAAA;AAAA,MAAA;AACW,QAAA;AACuB,QAAA;AACtB,QAAA;AACZ,QAAA;AAAA,MAAA;AAED,IAAA;AAEwD,IAAA;AAExD,oBAAA;AAAC,MAAA;AAAA,MAAA;AACW,QAAA;AACoC,QAAA;AAE9C,QAAA;AACG,0BAAA;AAEI,YAAA;AACuC,cAAA;AAEoB,8BAAA;AAC9D,YAAA;AAEF,4BAAA;AAAC,cAAA;AAAA,cAAA;AACW,gBAAA;AACK,gBAAA;AACG,kBAAA;AACK,oBAAA;AACd,kBAAA;AACS,oBAAA;AAChB,kBAAA;AACF,gBAAA;AAC8B,gBAAA;AAEQ,gBAAA;AAAA,cAAA;AACxC,YAAA;AACF,UAAA;AAE4C,UAAA;AAIxC,0BAAA;AAAC,YAAA;AAAA,YAAA;AAEY,cAAA;AACD,cAAA;AACE,cAAA;AACsB,cAAA;AAEH,cAAA;AAAA,YAAA;AANrB,YAAA;AAShB,UAAA;AAEG,0BAAA;AAKD,UAAA;AAAC,YAAA;AAAA,YAAA;AACW,cAAA;AACG,cAAA;AAAA,YAAA;AACf,UAAA;AAAA,QAAA;AAAA,MAAA;AAEJ,IAAA;AACA,oBAAA;AAAC,MAAA;AAAA,MAAA;AACW,QAAA;AACmF,QAAA;AAC7F,QAAA;AAAA,MAAA;AAAS,IAAA;AACqD,oBAAA;AAClE,EAAA;AAEJ;AD1D6G;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-JPIQU2C4.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\n\nconst Toaster = dynamic(\n () => import(\"sonner\").then((m) => m.Toaster),\n { ssr: false }\n);\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resolveBrandConfig,\n resolveRouteConfig,\n type CmsNavItem,\n} from \"../config\";\n\ninterface AdminUser {\n id: string;\n email: string;\n name: string | null;\n role: string;\n}\n\n/**\n * AdminShell props (spec.md §4.1 / Sprint 1 C1/C2).\n *\n * brand/nav/route 는 더 이상 하드코딩되지 않는다. 우선순위:\n * props > §5 config boundary (`setCmsConfig`) > 안전 중립 기본값\n * (빈 nav + 1회 `@withwiz/cms-kit:` warn, route 는 레거시 기본 경로).\n */\nexport interface AdminShellProps {\n children: React.ReactNode;\n /** 브랜드 라벨 (사이드바 홈 링크 텍스트). 미지정 시 §5 config. */\n brandLabel?: string;\n /** 브랜드 홈 링크 href. */\n brandHref?: string;\n /** admin 링크 href. */\n adminHref?: string;\n /** 순서 있는 nav 항목 목록 (label/href/glyph). */\n navItems?: CmsNavItem[];\n /** 로그인 페이지 경로 (라우트 가드/리다이렉트). */\n loginPath?: string;\n /** \"현재 사용자\" 엔드포인트. */\n meEndpoint?: string;\n /** 로그아웃 엔드포인트. */\n logoutEndpoint?: string;\n}\n\nexport default function AdminShell({\n children,\n brandLabel,\n brandHref,\n adminHref,\n navItems,\n loginPath,\n meEndpoint,\n logoutEndpoint,\n}: AdminShellProps) {\n const pathname = usePathname();\n const router = useRouter();\n const routerRef = useRef(router);\n routerRef.current = router;\n\n // props > §5 config boundary > safe default. The single\n // @withwiz/cms-kit-namespaced warn-once fires ONLY when neither props NOR §5\n // config supply brand/nav (props are a valid injection — suppress warn).\n const brandSuppliedViaProps =\n brandLabel !== undefined || navItems !== undefined;\n const brandCfg = resolveBrandConfig(brandSuppliedViaProps);\n const routeCfg = resolveRouteConfig();\n const resolvedBrandLabel =\n brandLabel ?? brandCfg.brandLabel ?? null;\n const resolvedBrandHref = brandHref ?? brandCfg.brandHref;\n const resolvedAdminHref = adminHref ?? brandCfg.adminHref;\n const resolvedNav: CmsNavItem[] = navItems ?? brandCfg.navItems;\n const resolvedLoginPath = loginPath ?? routeCfg.loginPath;\n const resolvedMeEndpoint = meEndpoint ?? routeCfg.meEndpoint;\n const resolvedLogoutEndpoint = logoutEndpoint ?? routeCfg.logoutEndpoint;\n\n const [checking, setChecking] = useState(true);\n const [user, setUser] = useState<AdminUser | null>(null);\n const [mobileOpen, setMobileOpen] = useState(false);\n const [collapsed, setCollapsed] = useState(() => {\n if (typeof window !== \"undefined\") {\n return localStorage.getItem(\"admin_sidebar_collapsed\") === \"true\";\n }\n return false;\n });\n\n const DEFAULT_WIDTH = 200;\n const MIN_WIDTH = 200;\n const MAX_WIDTH = 400;\n\n const [sidebarWidth, setSidebarWidth] = useState(() => {\n if (typeof window !== \"undefined\") {\n const saved = localStorage.getItem(\"admin_sidebar_width\");\n return saved ? Number(saved) : DEFAULT_WIDTH;\n }\n return DEFAULT_WIDTH;\n });\n const isResizing = useRef(false);\n const widthRef = useRef(sidebarWidth);\n const [dragging, setDragging] = useState(false);\n\n const isLoginPage = pathname === resolvedLoginPath;\n\n useEffect(() => {\n if (isLoginPage) {\n setChecking(false);\n return;\n }\n\n let cancelled = false;\n\n async function checkAuth() {\n try {\n const res = await adminFetch(resolvedMeEndpoint);\n if (cancelled) return;\n\n if (!res.ok) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n\n const data = await res.json();\n if (data.success && data.data?.user) {\n const u = data.data.user as AdminUser;\n setUser((prev) =>\n prev?.email === u.email && prev?.id === u.id ? prev : u\n );\n }\n } catch {\n if (!cancelled) {\n routerRef.current.replace(resolvedLoginPath);\n return;\n }\n }\n\n setChecking(false);\n }\n\n checkAuth();\n return () => { cancelled = true; };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoginPage]);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n if (!isResizing.current) return;\n const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));\n widthRef.current = newWidth;\n setSidebarWidth(newWidth);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n if (!isResizing.current) return;\n isResizing.current = false;\n setDragging(false);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n localStorage.setItem(\"admin_sidebar_width\", String(widthRef.current));\n document.removeEventListener(\"mousemove\", handleMouseMove);\n document.removeEventListener(\"mouseup\", handleMouseUp);\n }, [handleMouseMove]);\n\n if (isLoginPage) {\n return <>{children}</>;\n }\n\n if (checking) {\n return <div className=\"admin-auth-loading\">인증 확인 중...</div>;\n }\n\n async function handleLogout() {\n await fetch(resolvedLogoutEndpoint, { method: \"POST\", credentials: \"same-origin\" });\n router.replace(resolvedLoginPath);\n }\n\n function toggleSidebar() {\n setCollapsed((prev) => {\n const next = !prev;\n localStorage.setItem(\"admin_sidebar_collapsed\", String(next));\n return next;\n });\n }\n\n function startResize(e: React.MouseEvent) {\n e.preventDefault();\n isResizing.current = true;\n setDragging(true);\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n document.addEventListener(\"mousemove\", handleMouseMove);\n document.addEventListener(\"mouseup\", handleMouseUp);\n }\n\n return (\n <div className={`admin-layout${collapsed ? \" admin-sidebar-collapsed\" : \"\"}${dragging ? \" admin-resizing\" : \"\"}${mobileOpen ? \" admin-sidebar-mobile-open\" : \"\"}`}>\n <button\n className=\"admin-mobile-toggle\"\n onClick={() => setMobileOpen(true)}\n aria-label=\"메뉴 열기\"\n >\n ☰\n </button>\n {mobileOpen && (\n <div className=\"admin-sidebar-overlay\" onClick={() => setMobileOpen(false)} />\n )}\n <aside\n className=\"admin-sidebar\"\n style={!collapsed ? { width: sidebarWidth } : undefined}\n >\n <div className=\"admin-sidebar-header\">\n {!collapsed && (\n <div className=\"admin-sidebar-logo\">\n {resolvedBrandLabel && (\n <a href={resolvedBrandHref} className=\"admin-logo-home\" title=\"사이트 보기\" target=\"_blank\" rel=\"noopener noreferrer\">{resolvedBrandLabel}</a>\n )}\n <Link href={resolvedAdminHref} className=\"admin-logo-admin\">Admin</Link>\n </div>\n )}\n <button\n className=\"admin-sidebar-toggle\"\n onClick={() => {\n if (mobileOpen) {\n setMobileOpen(false);\n } else {\n toggleSidebar();\n }\n }}\n title={collapsed ? \"메뉴 펼치기\" : \"메뉴 접기\"}\n >\n {mobileOpen ? \"✕\" : collapsed ? \"›\" : \"‹\"}\n </button>\n </div>\n {!collapsed && user && (\n <div className=\"admin-sidebar-user\">{user.email}</div>\n )}\n <nav className=\"admin-sidebar-nav\">\n {resolvedNav.map((item) => (\n <Link\n key={item.href}\n href={item.href}\n className=\"admin-sidebar-link\"\n title={item.label}\n onClick={() => setMobileOpen(false)}\n >\n {collapsed ? item.glyph : item.label}\n </Link>\n ))}\n </nav>\n <div className=\"admin-sidebar-footer\">\n <button className=\"admin-sidebar-logout\" onClick={handleLogout} title=\"로그아웃\">\n {collapsed ? \"✕\" : \"로그아웃\"}\n </button>\n </div>\n {!collapsed && (\n <div\n className=\"admin-sidebar-resize\"\n onMouseDown={startResize}\n />\n )}\n </aside>\n <main\n className=\"admin-main\"\n style={!collapsed ? { marginLeft: sidebarWidth, width: `calc(100vw - ${sidebarWidth}px)` } : undefined}\n >{children}</main>\n <Toaster position=\"top-center\" richColors closeButton duration={3000} />\n </div>\n );\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adminFetch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-47DPXAO6.mjs";
|
|
4
4
|
import {
|
|
5
5
|
resolveBrandConfig,
|
|
6
6
|
resolveRouteConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3UXX27N5.mjs";
|
|
8
8
|
|
|
9
9
|
// src/components/AdminShell.tsx
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -215,4 +215,4 @@ function AdminShell({
|
|
|
215
215
|
export {
|
|
216
216
|
AdminShell
|
|
217
217
|
};
|
|
218
|
-
//# sourceMappingURL=chunk-
|
|
218
|
+
//# sourceMappingURL=chunk-MN27HJKW.mjs.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useAdminForm,
|
|
3
3
|
useAdminList
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MXXC4U3D.mjs";
|
|
5
5
|
import {
|
|
6
6
|
adminFetch,
|
|
7
7
|
getAuthHeaders
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-47DPXAO6.mjs";
|
|
9
9
|
|
|
10
10
|
// src/components/AdminManagerBase.tsx
|
|
11
11
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
|
@@ -306,4 +306,4 @@ function AdminManagerBase(props) {
|
|
|
306
306
|
export {
|
|
307
307
|
AdminManagerBase
|
|
308
308
|
};
|
|
309
|
-
//# sourceMappingURL=chunk-
|
|
309
|
+
//# sourceMappingURL=chunk-MWX5B4DP.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adminFetch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-47DPXAO6.mjs";
|
|
4
4
|
|
|
5
5
|
// src/hooks/useAdminList.ts
|
|
6
6
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
@@ -105,4 +105,4 @@ export {
|
|
|
105
105
|
useAdminList,
|
|
106
106
|
useAdminForm
|
|
107
107
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-MXXC4U3D.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveJwtConfig
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3UXX27N5.mjs";
|
|
4
4
|
|
|
5
5
|
// src/utils/jwt.ts
|
|
6
6
|
import { JWTManager } from "@withwiz/toolkit/core/auth/jwt";
|
|
@@ -31,4 +31,4 @@ function getJWTManager() {
|
|
|
31
31
|
export {
|
|
32
32
|
getJWTManager
|
|
33
33
|
};
|
|
34
|
-
//# sourceMappingURL=chunk-
|
|
34
|
+
//# sourceMappingURL=chunk-N7LMNP4W.mjs.map
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
var _chunkS7GOXCRMjs = require('./chunk-S7GOXCRM.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkJ32BTYJZjs = require('./chunk-J32BTYJZ.js');
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunkUVZD7GHGjs = require('./chunk-UVZD7GHG.js');
|
|
11
11
|
|
|
12
12
|
// src/hooks/useImageDropZone.ts
|
|
13
13
|
var _react = require('react');
|
|
@@ -25,8 +25,8 @@ function validateFile(file) {
|
|
|
25
25
|
async function uploadSingleFile(file) {
|
|
26
26
|
const fd = new FormData();
|
|
27
27
|
fd.append("file", file);
|
|
28
|
-
const { uploadEndpoint } =
|
|
29
|
-
const res = await
|
|
28
|
+
const { uploadEndpoint } = _chunkUVZD7GHGjs.resolveRouteConfig.call(void 0, );
|
|
29
|
+
const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, uploadEndpoint, {
|
|
30
30
|
method: "POST",
|
|
31
31
|
body: fd
|
|
32
32
|
});
|
|
@@ -171,4 +171,4 @@ function useImageDropZone(options) {
|
|
|
171
171
|
|
|
172
172
|
|
|
173
173
|
exports.useImageDropZone = useImageDropZone;
|
|
174
|
-
//# sourceMappingURL=chunk-
|
|
174
|
+
//# sourceMappingURL=chunk-OHJISVGC.js.map
|