@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.
Files changed (101) hide show
  1. package/README.ko.md +4 -2
  2. package/README.md +4 -2
  3. package/dist/{chunk-JMWXVXK5.js → chunk-25GMHPCW.js} +12 -4
  4. package/dist/chunk-25GMHPCW.js.map +1 -0
  5. package/dist/{chunk-5JUIMXNF.js → chunk-2VHNZWH2.js} +3 -3
  6. package/dist/{chunk-5JUIMXNF.js.map → chunk-2VHNZWH2.js.map} +1 -1
  7. package/dist/{chunk-BWKKHCDG.mjs → chunk-3UXX27N5.mjs} +44 -3
  8. package/dist/{chunk-BWKKHCDG.mjs.map → chunk-3UXX27N5.mjs.map} +1 -1
  9. package/dist/{chunk-T7EVFD4G.mjs → chunk-47DPXAO6.mjs} +2 -2
  10. package/dist/{chunk-4W3X7OKF.js → chunk-5Z5XUR2K.js} +48 -27
  11. package/dist/chunk-5Z5XUR2K.js.map +1 -0
  12. package/dist/{chunk-LDFE33ML.mjs → chunk-7F5YFG2W.mjs} +10 -2
  13. package/dist/chunk-7F5YFG2W.mjs.map +1 -0
  14. package/dist/{chunk-CGVFB23E.mjs → chunk-A557FA2K.mjs} +24 -19
  15. package/dist/{chunk-CGVFB23E.mjs.map → chunk-A557FA2K.mjs.map} +1 -1
  16. package/dist/{chunk-OBNALAMG.mjs → chunk-BG4C5KZO.mjs} +2 -2
  17. package/dist/{chunk-DCIF4XJL.js → chunk-EPVV6655.js} +10 -10
  18. package/dist/{chunk-DCIF4XJL.js.map → chunk-EPVV6655.js.map} +1 -1
  19. package/dist/{chunk-LF5LOI7K.mjs → chunk-FCWUNCGA.mjs} +3 -3
  20. package/dist/{chunk-YEQFKOWD.mjs → chunk-GQJNLE34.mjs} +2 -2
  21. package/dist/{chunk-W4FWIZ2Y.js → chunk-HVB72GT3.js} +3 -3
  22. package/dist/{chunk-W4FWIZ2Y.js.map → chunk-HVB72GT3.js.map} +1 -1
  23. package/dist/{chunk-KWKV2GAH.js → chunk-J32BTYJZ.js} +4 -4
  24. package/dist/{chunk-KWKV2GAH.js.map → chunk-J32BTYJZ.js.map} +1 -1
  25. package/dist/{chunk-C5WFZJM4.js → chunk-JPIQU2C4.js} +6 -6
  26. package/dist/{chunk-C5WFZJM4.js.map → chunk-JPIQU2C4.js.map} +1 -1
  27. package/dist/{chunk-FEPGJ7WN.mjs → chunk-MN27HJKW.mjs} +3 -3
  28. package/dist/{chunk-ZULSFSDG.mjs → chunk-MWX5B4DP.mjs} +3 -3
  29. package/dist/{chunk-FCCCG5Z7.mjs → chunk-MXXC4U3D.mjs} +2 -2
  30. package/dist/{chunk-5RDMODGV.mjs → chunk-N7LMNP4W.mjs} +2 -2
  31. package/dist/{chunk-YY7K5CXX.js → chunk-OHJISVGC.js} +5 -5
  32. package/dist/{chunk-YY7K5CXX.js.map → chunk-OHJISVGC.js.map} +1 -1
  33. package/dist/{chunk-EM4MQEWV.mjs → chunk-SN3XOMF6.mjs} +45 -24
  34. package/dist/chunk-SN3XOMF6.mjs.map +1 -0
  35. package/dist/{chunk-BE7JDL64.js → chunk-TKOFGRU4.js} +11 -11
  36. package/dist/{chunk-BE7JDL64.js.map → chunk-TKOFGRU4.js.map} +1 -1
  37. package/dist/{chunk-EG6NVPP2.js → chunk-UVZD7GHG.js} +44 -3
  38. package/dist/chunk-UVZD7GHG.js.map +1 -0
  39. package/dist/{chunk-6EZ3QE5N.js → chunk-Y3YYIJYG.js} +3 -3
  40. package/dist/{chunk-6EZ3QE5N.js.map → chunk-Y3YYIJYG.js.map} +1 -1
  41. package/dist/{chunk-OYJRR3G2.js → chunk-ZMXNPYFS.js} +25 -20
  42. package/dist/chunk-ZMXNPYFS.js.map +1 -0
  43. package/dist/components/AdminManagerBase.js +5 -5
  44. package/dist/components/AdminManagerBase.mjs +4 -4
  45. package/dist/components/AdminShell.d.mts +1 -1
  46. package/dist/components/AdminShell.d.ts +1 -1
  47. package/dist/components/AdminShell.js +4 -4
  48. package/dist/components/AdminShell.mjs +3 -3
  49. package/dist/components/index.d.mts +1 -1
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.js +8 -8
  52. package/dist/components/index.mjs +7 -7
  53. package/dist/hooks/index.js +5 -5
  54. package/dist/hooks/index.mjs +4 -4
  55. package/dist/hooks/useImageDropZone.js +4 -4
  56. package/dist/hooks/useImageDropZone.mjs +3 -3
  57. package/dist/{index-00a40-FQ.d.ts → index-Cce8FHtI.d.mts} +38 -1
  58. package/dist/{index-00a40-FQ.d.mts → index-Cce8FHtI.d.ts} +38 -1
  59. package/dist/index.d.mts +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +19 -13
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +18 -12
  64. package/dist/infrastructure/index.js +3 -3
  65. package/dist/infrastructure/index.mjs +2 -2
  66. package/dist/infrastructure/middleware/index.js +3 -3
  67. package/dist/infrastructure/middleware/index.mjs +2 -2
  68. package/dist/infrastructure/middleware/wrappers.js +3 -3
  69. package/dist/infrastructure/middleware/wrappers.mjs +2 -2
  70. package/dist/services/index.js +5 -5
  71. package/dist/services/index.mjs +4 -4
  72. package/dist/utils/admin-fetch.js +3 -3
  73. package/dist/utils/admin-fetch.mjs +2 -2
  74. package/dist/utils/html-sanitizer.js +3 -3
  75. package/dist/utils/html-sanitizer.mjs +2 -2
  76. package/dist/utils/index.d.mts +1 -1
  77. package/dist/utils/index.d.ts +1 -1
  78. package/dist/utils/index.js +13 -7
  79. package/dist/utils/index.js.map +1 -1
  80. package/dist/utils/index.mjs +12 -6
  81. package/dist/utils/jwt.js +3 -3
  82. package/dist/utils/jwt.mjs +2 -2
  83. package/dist/utils/r2-helpers.js +4 -4
  84. package/dist/utils/r2-helpers.mjs +3 -3
  85. package/dist/utils/r2-storage.js +3 -3
  86. package/dist/utils/r2-storage.mjs +2 -2
  87. package/package.json +4 -4
  88. package/dist/chunk-4W3X7OKF.js.map +0 -1
  89. package/dist/chunk-EG6NVPP2.js.map +0 -1
  90. package/dist/chunk-EM4MQEWV.mjs.map +0 -1
  91. package/dist/chunk-JMWXVXK5.js.map +0 -1
  92. package/dist/chunk-LDFE33ML.mjs.map +0 -1
  93. package/dist/chunk-OYJRR3G2.js.map +0 -1
  94. /package/dist/{chunk-T7EVFD4G.mjs.map → chunk-47DPXAO6.mjs.map} +0 -0
  95. /package/dist/{chunk-OBNALAMG.mjs.map → chunk-BG4C5KZO.mjs.map} +0 -0
  96. /package/dist/{chunk-LF5LOI7K.mjs.map → chunk-FCWUNCGA.mjs.map} +0 -0
  97. /package/dist/{chunk-YEQFKOWD.mjs.map → chunk-GQJNLE34.mjs.map} +0 -0
  98. /package/dist/{chunk-FEPGJ7WN.mjs.map → chunk-MN27HJKW.mjs.map} +0 -0
  99. /package/dist/{chunk-ZULSFSDG.mjs.map → chunk-MWX5B4DP.mjs.map} +0 -0
  100. /package/dist/{chunk-FCCCG5Z7.mjs.map → chunk-MXXC4U3D.mjs.map} +0 -0
  101. /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-LF5LOI7K.mjs";
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-OBNALAMG.mjs.map
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 _chunk5JUIMXNFjs = require('./chunk-5JUIMXNF.js');
4
+ var _chunk2VHNZWH2js = require('./chunk-2VHNZWH2.js');
5
5
 
6
6
 
7
7
 
8
- var _chunkKWKV2GAHjs = require('./chunk-KWKV2GAH.js');
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 = _chunk5JUIMXNFjs.useAdminList.call(void 0, {
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 = _chunk5JUIMXNFjs.useAdminForm.call(void 0, config.emptyForm);
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 _chunkKWKV2GAHjs.adminFetch.call(void 0, `${config.apiPath}/${id}`);
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 _chunkKWKV2GAHjs.adminFetch.call(void 0, url, {
108
+ const res = await _chunkJ32BTYJZjs.adminFetch.call(void 0, url, {
109
109
  method,
110
- headers: { ..._chunkKWKV2GAHjs.getAuthHeaders.call(void 0, ), "Content-Type": "application/json" },
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 _chunkKWKV2GAHjs.adminFetch.call(void 0, `${config.apiPath}?limit=100&sortBy=${sortKey}`);
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 _chunkKWKV2GAHjs.adminFetch.call(void 0, `${config.apiPath}/${id}`, {
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-DCIF4XJL.js.map
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-T7EVFD4G.mjs";
7
+ } from "./chunk-47DPXAO6.mjs";
8
8
  import {
9
9
  resolveRouteConfig
10
- } from "./chunk-BWKKHCDG.mjs";
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-LF5LOI7K.mjs.map
174
+ //# sourceMappingURL=chunk-FCWUNCGA.mjs.map
@@ -2,7 +2,7 @@ import {
2
2
  namespacedError,
3
3
  resolveR2CredentialsConfig,
4
4
  resolveR2PublicUrl
5
- } from "./chunk-BWKKHCDG.mjs";
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-YEQFKOWD.mjs.map
154
+ //# sourceMappingURL=chunk-GQJNLE34.mjs.map
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
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 } = _chunkEG6NVPP2js.resolveJwtConfig.call(void 0, );
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-W4FWIZ2Y.js.map
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-W4FWIZ2Y.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-W4FWIZ2Y.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
+ {"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 _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
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 } = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
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 } = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
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-KWKV2GAH.js.map
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-KWKV2GAH.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-KWKV2GAH.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
+ {"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 _chunkKWKV2GAHjs = require('./chunk-KWKV2GAH.js');
3
+ var _chunkJ32BTYJZjs = require('./chunk-J32BTYJZ.js');
4
4
 
5
5
 
6
6
 
7
- var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
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 = _chunkEG6NVPP2js.resolveBrandConfig.call(void 0, brandSuppliedViaProps);
35
- const routeCfg = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
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 _chunkKWKV2GAHjs.adminFetch.call(void 0, resolvedMeEndpoint);
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-C5WFZJM4.js.map
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":"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","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-T7EVFD4G.mjs";
3
+ } from "./chunk-47DPXAO6.mjs";
4
4
  import {
5
5
  resolveBrandConfig,
6
6
  resolveRouteConfig
7
- } from "./chunk-BWKKHCDG.mjs";
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-FEPGJ7WN.mjs.map
218
+ //# sourceMappingURL=chunk-MN27HJKW.mjs.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  useAdminForm,
3
3
  useAdminList
4
- } from "./chunk-FCCCG5Z7.mjs";
4
+ } from "./chunk-MXXC4U3D.mjs";
5
5
  import {
6
6
  adminFetch,
7
7
  getAuthHeaders
8
- } from "./chunk-T7EVFD4G.mjs";
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-ZULSFSDG.mjs.map
309
+ //# sourceMappingURL=chunk-MWX5B4DP.mjs.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  adminFetch
3
- } from "./chunk-T7EVFD4G.mjs";
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-FCCCG5Z7.mjs.map
108
+ //# sourceMappingURL=chunk-MXXC4U3D.mjs.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveJwtConfig
3
- } from "./chunk-BWKKHCDG.mjs";
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-5RDMODGV.mjs.map
34
+ //# sourceMappingURL=chunk-N7LMNP4W.mjs.map
@@ -4,10 +4,10 @@
4
4
  var _chunkS7GOXCRMjs = require('./chunk-S7GOXCRM.js');
5
5
 
6
6
 
7
- var _chunkKWKV2GAHjs = require('./chunk-KWKV2GAH.js');
7
+ var _chunkJ32BTYJZjs = require('./chunk-J32BTYJZ.js');
8
8
 
9
9
 
10
- var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
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 } = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
29
- const res = await _chunkKWKV2GAHjs.adminFetch.call(void 0, uploadEndpoint, {
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-YY7K5CXX.js.map
174
+ //# sourceMappingURL=chunk-OHJISVGC.js.map