@withwiz/cms-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +146 -0
- package/README.md +146 -0
- package/dist/chunk-2REYWBQ6.js +32 -0
- package/dist/chunk-2REYWBQ6.js.map +1 -0
- package/dist/chunk-2SYUEQ6J.mjs +17 -0
- package/dist/chunk-2SYUEQ6J.mjs.map +1 -0
- package/dist/chunk-3ILEMZAL.mjs +32 -0
- package/dist/chunk-3ILEMZAL.mjs.map +1 -0
- package/dist/chunk-3Y2TQCHU.mjs +33 -0
- package/dist/chunk-3Y2TQCHU.mjs.map +1 -0
- package/dist/chunk-4LSFAAZW.js +1 -0
- package/dist/chunk-4LSFAAZW.js.map +1 -0
- package/dist/chunk-4W3X7OKF.js +100 -0
- package/dist/chunk-4W3X7OKF.js.map +1 -0
- package/dist/chunk-5JUIMXNF.js +108 -0
- package/dist/chunk-5JUIMXNF.js.map +1 -0
- package/dist/chunk-5RDMODGV.mjs +34 -0
- package/dist/chunk-5RDMODGV.mjs.map +1 -0
- package/dist/chunk-6ES4W2DL.mjs +1 -0
- package/dist/chunk-6ES4W2DL.mjs.map +1 -0
- package/dist/chunk-6EZ3QE5N.js +71 -0
- package/dist/chunk-6EZ3QE5N.js.map +1 -0
- package/dist/chunk-6JFERXNE.mjs +16 -0
- package/dist/chunk-6JFERXNE.mjs.map +1 -0
- package/dist/chunk-7SWW3ZIN.js +17 -0
- package/dist/chunk-7SWW3ZIN.js.map +1 -0
- package/dist/chunk-AXR56AUD.mjs +1 -0
- package/dist/chunk-AXR56AUD.mjs.map +1 -0
- package/dist/chunk-BE7JDL64.js +154 -0
- package/dist/chunk-BE7JDL64.js.map +1 -0
- package/dist/chunk-BHN2SRZC.js +51 -0
- package/dist/chunk-BHN2SRZC.js.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +11 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/chunk-BWKKHCDG.mjs +164 -0
- package/dist/chunk-BWKKHCDG.mjs.map +1 -0
- package/dist/chunk-C5WFZJM4.js +218 -0
- package/dist/chunk-C5WFZJM4.js.map +1 -0
- package/dist/chunk-CGVFB23E.mjs +123 -0
- package/dist/chunk-CGVFB23E.mjs.map +1 -0
- package/dist/chunk-D2VDTRXO.js +1 -0
- package/dist/chunk-D2VDTRXO.js.map +1 -0
- package/dist/chunk-D75ADHVM.mjs +181 -0
- package/dist/chunk-D75ADHVM.mjs.map +1 -0
- package/dist/chunk-DCIF4XJL.js +309 -0
- package/dist/chunk-DCIF4XJL.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DSORCGJK.js +44 -0
- package/dist/chunk-DSORCGJK.js.map +1 -0
- package/dist/chunk-E5YUAOJI.js +27 -0
- package/dist/chunk-E5YUAOJI.js.map +1 -0
- package/dist/chunk-EG6NVPP2.js +164 -0
- package/dist/chunk-EG6NVPP2.js.map +1 -0
- package/dist/chunk-EL7YGOTY.js +1 -0
- package/dist/chunk-EL7YGOTY.js.map +1 -0
- package/dist/chunk-EM4MQEWV.mjs +100 -0
- package/dist/chunk-EM4MQEWV.mjs.map +1 -0
- package/dist/chunk-FCCCG5Z7.mjs +108 -0
- package/dist/chunk-FCCCG5Z7.mjs.map +1 -0
- package/dist/chunk-FEPGJ7WN.mjs +218 -0
- package/dist/chunk-FEPGJ7WN.mjs.map +1 -0
- package/dist/chunk-HXVWBFCN.js +16 -0
- package/dist/chunk-HXVWBFCN.js.map +1 -0
- package/dist/chunk-I5YMTJV6.mjs +51 -0
- package/dist/chunk-I5YMTJV6.mjs.map +1 -0
- package/dist/chunk-IMNWW7Q5.mjs +13 -0
- package/dist/chunk-IMNWW7Q5.mjs.map +1 -0
- package/dist/chunk-J5N76ASB.mjs +114 -0
- package/dist/chunk-J5N76ASB.mjs.map +1 -0
- package/dist/chunk-JBV26CUG.mjs +51 -0
- package/dist/chunk-JBV26CUG.mjs.map +1 -0
- package/dist/chunk-JMWXVXK5.js +53 -0
- package/dist/chunk-JMWXVXK5.js.map +1 -0
- package/dist/chunk-KWKV2GAH.js +60 -0
- package/dist/chunk-KWKV2GAH.js.map +1 -0
- package/dist/chunk-LDFE33ML.mjs +53 -0
- package/dist/chunk-LDFE33ML.mjs.map +1 -0
- package/dist/chunk-LF5LOI7K.mjs +174 -0
- package/dist/chunk-LF5LOI7K.mjs.map +1 -0
- package/dist/chunk-MMKAMH4U.mjs +16 -0
- package/dist/chunk-MMKAMH4U.mjs.map +1 -0
- package/dist/chunk-MUHFEEYW.js +33 -0
- package/dist/chunk-MUHFEEYW.js.map +1 -0
- package/dist/chunk-MUQYK5EO.js +1 -0
- package/dist/chunk-MUQYK5EO.js.map +1 -0
- package/dist/chunk-OBNALAMG.mjs +71 -0
- package/dist/chunk-OBNALAMG.mjs.map +1 -0
- package/dist/chunk-OLEYNPW6.js +25 -0
- package/dist/chunk-OLEYNPW6.js.map +1 -0
- package/dist/chunk-OYJRR3G2.js +123 -0
- package/dist/chunk-OYJRR3G2.js.map +1 -0
- package/dist/chunk-PLVG7AW3.js +16 -0
- package/dist/chunk-PLVG7AW3.js.map +1 -0
- package/dist/chunk-PYD4CES4.js +1 -0
- package/dist/chunk-PYD4CES4.js.map +1 -0
- package/dist/chunk-QACD2U6P.mjs +1 -0
- package/dist/chunk-QACD2U6P.mjs.map +1 -0
- package/dist/chunk-R5GGIVD4.js +10 -0
- package/dist/chunk-R5GGIVD4.js.map +1 -0
- package/dist/chunk-RCK5WCU5.mjs +25 -0
- package/dist/chunk-RCK5WCU5.mjs.map +1 -0
- package/dist/chunk-S6UUSM7S.js +13 -0
- package/dist/chunk-S6UUSM7S.js.map +1 -0
- package/dist/chunk-S7GOXCRM.js +114 -0
- package/dist/chunk-S7GOXCRM.js.map +1 -0
- package/dist/chunk-SMV5D5LO.mjs +28 -0
- package/dist/chunk-SMV5D5LO.mjs.map +1 -0
- package/dist/chunk-T7EVFD4G.mjs +60 -0
- package/dist/chunk-T7EVFD4G.mjs.map +1 -0
- package/dist/chunk-TLH3V4SY.mjs +1 -0
- package/dist/chunk-TLH3V4SY.mjs.map +1 -0
- package/dist/chunk-TUQ2CVAK.mjs +10 -0
- package/dist/chunk-TUQ2CVAK.mjs.map +1 -0
- package/dist/chunk-U6PVJKTB.js +51 -0
- package/dist/chunk-U6PVJKTB.js.map +1 -0
- package/dist/chunk-UXMMEEX4.mjs +27 -0
- package/dist/chunk-UXMMEEX4.mjs.map +1 -0
- package/dist/chunk-VVXNFFKI.mjs +44 -0
- package/dist/chunk-VVXNFFKI.mjs.map +1 -0
- package/dist/chunk-VZZ3RU3Q.js +28 -0
- package/dist/chunk-VZZ3RU3Q.js.map +1 -0
- package/dist/chunk-W4FWIZ2Y.js +34 -0
- package/dist/chunk-W4FWIZ2Y.js.map +1 -0
- package/dist/chunk-YEQFKOWD.mjs +154 -0
- package/dist/chunk-YEQFKOWD.mjs.map +1 -0
- package/dist/chunk-YPBKY4KY.mjs +1 -0
- package/dist/chunk-YPBKY4KY.mjs.map +1 -0
- package/dist/chunk-YY7K5CXX.js +174 -0
- package/dist/chunk-YY7K5CXX.js.map +1 -0
- package/dist/chunk-ZFORODOX.js +181 -0
- package/dist/chunk-ZFORODOX.js.map +1 -0
- package/dist/chunk-ZULSFSDG.mjs +309 -0
- package/dist/chunk-ZULSFSDG.mjs.map +1 -0
- package/dist/components/AdminManagerBase.d.mts +26 -0
- package/dist/components/AdminManagerBase.d.ts +26 -0
- package/dist/components/AdminManagerBase.js +13 -0
- package/dist/components/AdminManagerBase.js.map +1 -0
- package/dist/components/AdminManagerBase.mjs +12 -0
- package/dist/components/AdminManagerBase.mjs.map +1 -0
- package/dist/components/AdminManagerConfig.d.mts +83 -0
- package/dist/components/AdminManagerConfig.d.ts +83 -0
- package/dist/components/AdminManagerConfig.js +2 -0
- package/dist/components/AdminManagerConfig.js.map +1 -0
- package/dist/components/AdminManagerConfig.mjs +2 -0
- package/dist/components/AdminManagerConfig.mjs.map +1 -0
- package/dist/components/AdminShell.d.mts +30 -0
- package/dist/components/AdminShell.d.ts +30 -0
- package/dist/components/AdminShell.js +12 -0
- package/dist/components/AdminShell.js.map +1 -0
- package/dist/components/AdminShell.mjs +11 -0
- package/dist/components/AdminShell.mjs.map +1 -0
- package/dist/components/JsonLd.d.mts +32 -0
- package/dist/components/JsonLd.d.ts +32 -0
- package/dist/components/JsonLd.js +11 -0
- package/dist/components/JsonLd.js.map +1 -0
- package/dist/components/JsonLd.mjs +11 -0
- package/dist/components/JsonLd.mjs.map +1 -0
- package/dist/components/ResizableImage.d.mts +18 -0
- package/dist/components/ResizableImage.d.ts +18 -0
- package/dist/components/ResizableImage.js +10 -0
- package/dist/components/ResizableImage.js.map +1 -0
- package/dist/components/ResizableImage.mjs +9 -0
- package/dist/components/ResizableImage.mjs.map +1 -0
- package/dist/components/ToggleSwitch.d.mts +11 -0
- package/dist/components/ToggleSwitch.d.ts +11 -0
- package/dist/components/ToggleSwitch.js +10 -0
- package/dist/components/ToggleSwitch.js.map +1 -0
- package/dist/components/ToggleSwitch.mjs +9 -0
- package/dist/components/ToggleSwitch.mjs.map +1 -0
- package/dist/components/image-drop-zone.css +115 -0
- package/dist/components/index.d.mts +26 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.js +35 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +35 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/components/toggle-switch.css +55 -0
- package/dist/hooks/index.d.mts +45 -0
- package/dist/hooks/index.d.ts +45 -0
- package/dist/hooks/index.js +25 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +25 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/hooks/useImageDropZone.d.mts +37 -0
- package/dist/hooks/useImageDropZone.d.ts +37 -0
- package/dist/hooks/useImageDropZone.js +13 -0
- package/dist/hooks/useImageDropZone.js.map +1 -0
- package/dist/hooks/useImageDropZone.mjs +12 -0
- package/dist/hooks/useImageDropZone.mjs.map +1 -0
- package/dist/hooks/useScrollReveal.d.mts +6 -0
- package/dist/hooks/useScrollReveal.d.ts +6 -0
- package/dist/hooks/useScrollReveal.js +12 -0
- package/dist/hooks/useScrollReveal.js.map +1 -0
- package/dist/hooks/useScrollReveal.mjs +11 -0
- package/dist/hooks/useScrollReveal.mjs.map +1 -0
- package/dist/image-drop-zone.css +115 -0
- package/dist/index-00a40-FQ.d.mts +136 -0
- package/dist/index-00a40-FQ.d.ts +136 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +169 -0
- package/dist/index.mjs.map +1 -0
- package/dist/infrastructure/index.d.mts +4 -0
- package/dist/infrastructure/index.d.ts +4 -0
- package/dist/infrastructure/index.js +20 -0
- package/dist/infrastructure/index.js.map +1 -0
- package/dist/infrastructure/index.mjs +20 -0
- package/dist/infrastructure/index.mjs.map +1 -0
- package/dist/infrastructure/middleware/index.d.mts +3 -0
- package/dist/infrastructure/middleware/index.d.ts +3 -0
- package/dist/infrastructure/middleware/index.js +15 -0
- package/dist/infrastructure/middleware/index.js.map +1 -0
- package/dist/infrastructure/middleware/index.mjs +15 -0
- package/dist/infrastructure/middleware/index.mjs.map +1 -0
- package/dist/infrastructure/middleware/wrappers.d.mts +10 -0
- package/dist/infrastructure/middleware/wrappers.d.ts +10 -0
- package/dist/infrastructure/middleware/wrappers.js +15 -0
- package/dist/infrastructure/middleware/wrappers.js.map +1 -0
- package/dist/infrastructure/middleware/wrappers.mjs +15 -0
- package/dist/infrastructure/middleware/wrappers.mjs.map +1 -0
- package/dist/infrastructure/prisma.d.mts +5 -0
- package/dist/infrastructure/prisma.d.ts +5 -0
- package/dist/infrastructure/prisma.js +12 -0
- package/dist/infrastructure/prisma.js.map +1 -0
- package/dist/infrastructure/prisma.mjs +12 -0
- package/dist/infrastructure/prisma.mjs.map +1 -0
- package/dist/services/index.d.mts +22 -0
- package/dist/services/index.d.ts +22 -0
- package/dist/services/index.js +36 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/index.mjs +36 -0
- package/dist/services/index.mjs.map +1 -0
- package/dist/toggle-switch.css +55 -0
- package/dist/types/common.d.mts +14 -0
- package/dist/types/common.d.ts +14 -0
- package/dist/types/common.js +8 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/common.mjs +8 -0
- package/dist/types/common.mjs.map +1 -0
- package/dist/types/index.d.mts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +9 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/utils/admin-fetch.d.mts +11 -0
- package/dist/utils/admin-fetch.d.ts +11 -0
- package/dist/utils/admin-fetch.js +13 -0
- package/dist/utils/admin-fetch.js.map +1 -0
- package/dist/utils/admin-fetch.mjs +13 -0
- package/dist/utils/admin-fetch.mjs.map +1 -0
- package/dist/utils/api-helpers.d.mts +20 -0
- package/dist/utils/api-helpers.d.ts +20 -0
- package/dist/utils/api-helpers.js +12 -0
- package/dist/utils/api-helpers.js.map +1 -0
- package/dist/utils/api-helpers.mjs +12 -0
- package/dist/utils/api-helpers.mjs.map +1 -0
- package/dist/utils/date.d.mts +5 -0
- package/dist/utils/date.d.ts +5 -0
- package/dist/utils/date.js +12 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.mjs +12 -0
- package/dist/utils/date.mjs.map +1 -0
- package/dist/utils/html-sanitizer.d.mts +56 -0
- package/dist/utils/html-sanitizer.d.ts +56 -0
- package/dist/utils/html-sanitizer.js +11 -0
- package/dist/utils/html-sanitizer.js.map +1 -0
- package/dist/utils/html-sanitizer.mjs +11 -0
- package/dist/utils/html-sanitizer.mjs.map +1 -0
- package/dist/utils/image-variant-utils.d.mts +14 -0
- package/dist/utils/image-variant-utils.d.ts +14 -0
- package/dist/utils/image-variant-utils.js +10 -0
- package/dist/utils/image-variant-utils.js.map +1 -0
- package/dist/utils/image-variant-utils.mjs +10 -0
- package/dist/utils/image-variant-utils.mjs.map +1 -0
- package/dist/utils/image-variants.d.mts +13 -0
- package/dist/utils/image-variants.d.ts +13 -0
- package/dist/utils/image-variants.js +12 -0
- package/dist/utils/image-variants.js.map +1 -0
- package/dist/utils/image-variants.mjs +12 -0
- package/dist/utils/image-variants.mjs.map +1 -0
- package/dist/utils/index.d.mts +88 -0
- package/dist/utils/index.d.ts +88 -0
- package/dist/utils/index.js +92 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +92 -0
- package/dist/utils/index.mjs.map +1 -0
- package/dist/utils/jwt.d.mts +19 -0
- package/dist/utils/jwt.d.ts +19 -0
- package/dist/utils/jwt.js +9 -0
- package/dist/utils/jwt.js.map +1 -0
- package/dist/utils/jwt.mjs +9 -0
- package/dist/utils/jwt.mjs.map +1 -0
- package/dist/utils/r2-helpers.d.mts +9 -0
- package/dist/utils/r2-helpers.d.ts +9 -0
- package/dist/utils/r2-helpers.js +20 -0
- package/dist/utils/r2-helpers.js.map +1 -0
- package/dist/utils/r2-helpers.mjs +20 -0
- package/dist/utils/r2-helpers.mjs.map +1 -0
- package/dist/utils/r2-storage.d.mts +38 -0
- package/dist/utils/r2-storage.d.ts +38 -0
- package/dist/utils/r2-storage.js +17 -0
- package/dist/utils/r2-storage.js.map +1 -0
- package/dist/utils/r2-storage.mjs +17 -0
- package/dist/utils/r2-storage.mjs.map +1 -0
- package/dist/utils/route-params.d.mts +3 -0
- package/dist/utils/route-params.d.ts +3 -0
- package/dist/utils/route-params.js +8 -0
- package/dist/utils/route-params.js.map +1 -0
- package/dist/utils/route-params.mjs +8 -0
- package/dist/utils/route-params.mjs.map +1 -0
- package/dist/validators/index.d.mts +2 -0
- package/dist/validators/index.d.ts +2 -0
- package/dist/validators/index.js +11 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/index.mjs +11 -0
- package/dist/validators/index.mjs.map +1 -0
- package/dist/validators/shared.d.mts +6 -0
- package/dist/validators/shared.d.ts +6 -0
- package/dist/validators/shared.js +10 -0
- package/dist/validators/shared.js.map +1 -0
- package/dist/validators/shared.mjs +10 -0
- package/dist/validators/shared.mjs.map +1 -0
- package/package.json +257 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
namespacedError,
|
|
3
|
+
resolveR2CredentialsConfig,
|
|
4
|
+
resolveR2PublicUrl
|
|
5
|
+
} from "./chunk-BWKKHCDG.mjs";
|
|
6
|
+
|
|
7
|
+
// src/utils/r2-storage.ts
|
|
8
|
+
import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
9
|
+
import { logError } from "@withwiz/toolkit/core/logger/logger";
|
|
10
|
+
function sanitizeStorageKey(key) {
|
|
11
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
12
|
+
throw namespacedError(
|
|
13
|
+
`storage key is empty or not a string; refusing to send to storage.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (key.startsWith("/")) {
|
|
17
|
+
throw namespacedError(
|
|
18
|
+
`storage key "${key}" is absolute / leading-slash; it could escape the intended namespace. Use a relative key (e.g. "news/x.jpg").`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (key.includes("\\")) {
|
|
22
|
+
throw namespacedError(
|
|
23
|
+
`storage key "${key}" contains a backslash; refusing (namespace-escape risk).`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < key.length; i++) {
|
|
27
|
+
if (key.charCodeAt(i) < 32) {
|
|
28
|
+
throw namespacedError(
|
|
29
|
+
`storage key contains control characters; refusing (namespace-escape risk).`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const segments = key.split("/");
|
|
34
|
+
if (segments.some((seg) => seg === ".." || seg === ".")) {
|
|
35
|
+
throw namespacedError(
|
|
36
|
+
`storage key "${key}" contains a path-traversal segment ("." / ".."); it could escape the intended namespace.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
var client = null;
|
|
42
|
+
var clientSnapshot = null;
|
|
43
|
+
function requireR2Credentials() {
|
|
44
|
+
const c = resolveR2CredentialsConfig();
|
|
45
|
+
const missing = [];
|
|
46
|
+
if (!c.accountId) missing.push("accountId");
|
|
47
|
+
if (!c.accessKeyId) missing.push("accessKeyId");
|
|
48
|
+
if (!c.secretAccessKey) missing.push("secretAccessKey");
|
|
49
|
+
if (!c.bucketName) missing.push("bucketName");
|
|
50
|
+
if (missing.length > 0) {
|
|
51
|
+
throw namespacedError(
|
|
52
|
+
`R2 credentials are missing: ${missing.join(", ")}. Inject \`setCmsConfig({ storage: { r2: { accountId, accessKeyId, secretAccessKey, bucketName } } })\` or set the legacy R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME environment variables. There is no safe default for storage credentials.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return c;
|
|
56
|
+
}
|
|
57
|
+
function getClient() {
|
|
58
|
+
const c = requireR2Credentials();
|
|
59
|
+
const snapshot = `${c.accountId}|${c.accessKeyId}`;
|
|
60
|
+
if (!client || clientSnapshot !== snapshot) {
|
|
61
|
+
client = new S3Client({
|
|
62
|
+
region: "auto",
|
|
63
|
+
endpoint: `https://${c.accountId}.r2.cloudflarestorage.com`,
|
|
64
|
+
credentials: {
|
|
65
|
+
accessKeyId: c.accessKeyId,
|
|
66
|
+
secretAccessKey: c.secretAccessKey
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
clientSnapshot = snapshot;
|
|
70
|
+
}
|
|
71
|
+
return client;
|
|
72
|
+
}
|
|
73
|
+
function isR2Enabled() {
|
|
74
|
+
const c = resolveR2CredentialsConfig();
|
|
75
|
+
return !!(c.accountId && c.accessKeyId && c.secretAccessKey && c.bucketName);
|
|
76
|
+
}
|
|
77
|
+
function buildPublicUrl(key, bucket) {
|
|
78
|
+
const base = resolveR2PublicUrl();
|
|
79
|
+
return base ? `${base}/${key}` : `https://${bucket}.r2.dev/${key}`;
|
|
80
|
+
}
|
|
81
|
+
async function uploadToR2(key, buffer, contentType) {
|
|
82
|
+
const safeKey = sanitizeStorageKey(key);
|
|
83
|
+
const s3 = getClient();
|
|
84
|
+
const { bucketName: bucket } = requireR2Credentials();
|
|
85
|
+
await s3.send(
|
|
86
|
+
new PutObjectCommand({
|
|
87
|
+
Bucket: bucket,
|
|
88
|
+
Key: safeKey,
|
|
89
|
+
Body: buffer,
|
|
90
|
+
ContentType: contentType
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
return { url: buildPublicUrl(safeKey, bucket), key: safeKey, size: buffer.length };
|
|
94
|
+
}
|
|
95
|
+
async function uploadImageWithVariants(originalKey, originalBuffer, originalContentType) {
|
|
96
|
+
const { generateImageVariants } = await import("./utils/image-variants.mjs");
|
|
97
|
+
const original = await uploadToR2(originalKey, originalBuffer, originalContentType);
|
|
98
|
+
const baseKey = originalKey.replace(/\.[^.]+$/, "");
|
|
99
|
+
const variants = {};
|
|
100
|
+
const variantKeys = [];
|
|
101
|
+
try {
|
|
102
|
+
const imageVariants = await generateImageVariants(originalBuffer, baseKey, originalContentType);
|
|
103
|
+
const { bucketName: bucket } = requireR2Credentials();
|
|
104
|
+
await Promise.all(
|
|
105
|
+
imageVariants.map(async (v) => {
|
|
106
|
+
try {
|
|
107
|
+
await uploadToR2(v.key, v.buffer, v.contentType);
|
|
108
|
+
variants[v.size] = buildPublicUrl(v.key, bucket);
|
|
109
|
+
variantKeys.push(v.key);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logError(`[image-variant] Failed to upload variant ${v.key}`, {
|
|
112
|
+
error: err instanceof Error ? err.message : err,
|
|
113
|
+
originalKey,
|
|
114
|
+
size: v.size
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
if (variantKeys.length === 0) {
|
|
120
|
+
logError(`[image-variant] No variants generated for ${originalKey}`);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logError(`[image-variant] Failed to generate variants for ${originalKey}`, {
|
|
124
|
+
error: err instanceof Error ? err.message : err
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
url: original.url,
|
|
129
|
+
key: original.key,
|
|
130
|
+
size: original.size,
|
|
131
|
+
variants,
|
|
132
|
+
variantKeys
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function deleteFromR2(key) {
|
|
136
|
+
const safeKey = sanitizeStorageKey(key);
|
|
137
|
+
const s3 = getClient();
|
|
138
|
+
const { bucketName: bucket } = requireR2Credentials();
|
|
139
|
+
await s3.send(
|
|
140
|
+
new DeleteObjectCommand({
|
|
141
|
+
Bucket: bucket,
|
|
142
|
+
Key: safeKey
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
sanitizeStorageKey,
|
|
149
|
+
isR2Enabled,
|
|
150
|
+
uploadToR2,
|
|
151
|
+
uploadImageWithVariants,
|
|
152
|
+
deleteFromR2
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=chunk-YEQFKOWD.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/r2-storage.ts"],"sourcesContent":["import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';\nimport { logError, logInfo } from '@withwiz/toolkit/core/logger/logger';\nimport {\n namespacedError,\n resolveR2CredentialsConfig,\n resolveR2PublicUrl,\n} from '../config';\n\n/**\n * storage object key 를 검증/정규화한다 (spec.md §4.6 / Sprint 1 S5).\n *\n * caller/user 파생 key 가 의도된 namespace 를 탈출(path traversal /\n * absolute / prefix-escape)하지 못하도록 한다. 위험 key 는 `@withwiz/cms-kit:`\n * 네임스페이스 에러로 즉시 거부한다. 양성(benign) key 는 *바이트 동일하게*\n * 통과시킨다 (blanket reject 아님 — 정규화로 인한 mangling 없음).\n *\n * 거부 규칙:\n * - 빈 값 / 비문자열\n * - 선행 `/` (절대/leading-slash: `/absolute`, `/news/x.jpg`)\n * - 백슬래시 포함 (`\\` — 윈도우식 절대/우회)\n * - 제어문자 (codepoint < 0x20)\n * - `.` / `..` path 세그먼트 (`../`, `a/../../b`, `news/../../secret`)\n */\nexport function sanitizeStorageKey(key: string): string {\n if (typeof key !== 'string' || key.length === 0) {\n throw namespacedError(\n `storage key is empty or not a string; refusing to send to storage.`,\n );\n }\n if (key.startsWith('/')) {\n throw namespacedError(\n `storage key \"${key}\" is absolute / leading-slash; it could escape the ` +\n `intended namespace. Use a relative key (e.g. \"news/x.jpg\").`,\n );\n }\n if (key.includes('\\\\')) {\n throw namespacedError(\n `storage key \"${key}\" contains a backslash; refusing (namespace-escape risk).`,\n );\n }\n for (let i = 0; i < key.length; i++) {\n if (key.charCodeAt(i) < 0x20) {\n throw namespacedError(\n `storage key contains control characters; refusing (namespace-escape risk).`,\n );\n }\n }\n const segments = key.split('/');\n if (segments.some((seg) => seg === '..' || seg === '.')) {\n throw namespacedError(\n `storage key \"${key}\" contains a path-traversal segment (\".\" / \"..\"); ` +\n `it could escape the intended namespace.`,\n );\n }\n // benign key: byte-identical passthrough.\n return key;\n}\n\nlet client: S3Client | null = null;\n// 자격증명/엔드포인트가 바뀌면 캐시된 S3Client 를 무효화하기 위한 스냅샷.\n// 같은 프로세스 내에서 `setCmsConfig` 가 재호출되거나 R2_* env 가 바뀐 경우\n// 다음 호출에서 새 클라이언트가 만들어진다.\nlet clientSnapshot: string | null = null;\n\nfunction requireR2Credentials(): {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n} {\n const c = resolveR2CredentialsConfig();\n const missing: string[] = [];\n if (!c.accountId) missing.push('accountId');\n if (!c.accessKeyId) missing.push('accessKeyId');\n if (!c.secretAccessKey) missing.push('secretAccessKey');\n if (!c.bucketName) missing.push('bucketName');\n if (missing.length > 0) {\n throw namespacedError(\n `R2 credentials are missing: ${missing.join(', ')}. ` +\n 'Inject `setCmsConfig({ storage: { r2: { accountId, accessKeyId, secretAccessKey, bucketName } } })` ' +\n 'or set the legacy R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME ' +\n 'environment variables. There is no safe default for storage credentials.',\n );\n }\n return c as {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucketName: string;\n };\n}\n\nfunction getClient(): S3Client {\n const c = requireR2Credentials();\n const snapshot = `${c.accountId}|${c.accessKeyId}`;\n if (!client || clientSnapshot !== snapshot) {\n client = new S3Client({\n region: 'auto',\n endpoint: `https://${c.accountId}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: c.accessKeyId,\n secretAccessKey: c.secretAccessKey,\n },\n });\n clientSnapshot = snapshot;\n }\n return client;\n}\n\nexport function isR2Enabled(): boolean {\n const c = resolveR2CredentialsConfig();\n return !!(c.accountId && c.accessKeyId && c.secretAccessKey && c.bucketName);\n}\n\nfunction buildPublicUrl(key: string, bucket: string): string {\n const base = resolveR2PublicUrl();\n return base ? `${base}/${key}` : `https://${bucket}.r2.dev/${key}`;\n}\n\nexport async function uploadToR2(\n key: string,\n buffer: Buffer,\n contentType: string,\n): Promise<{ url: string; key: string; size: number }> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n\n await s3.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n Body: buffer,\n ContentType: contentType,\n }),\n );\n\n return { url: buildPublicUrl(safeKey, bucket), key: safeKey, size: buffer.length };\n}\n\nexport interface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\nexport async function uploadImageWithVariants(\n originalKey: string,\n originalBuffer: Buffer,\n originalContentType: string,\n): Promise<{\n url: string;\n key: string;\n size: number;\n variants: ImageVariantUrls;\n variantKeys: string[];\n}> {\n const { generateImageVariants } = await import('./image-variants');\n\n const original = await uploadToR2(originalKey, originalBuffer, originalContentType);\n\n const baseKey = originalKey.replace(/\\.[^.]+$/, '');\n\n const variants: ImageVariantUrls = {};\n const variantKeys: string[] = [];\n\n try {\n const imageVariants = await generateImageVariants(originalBuffer, baseKey, originalContentType);\n const { bucketName: bucket } = requireR2Credentials();\n\n await Promise.all(\n imageVariants.map(async (v) => {\n try {\n await uploadToR2(v.key, v.buffer, v.contentType);\n variants[v.size] = buildPublicUrl(v.key, bucket);\n variantKeys.push(v.key);\n } catch (err) {\n logError(`[image-variant] Failed to upload variant ${v.key}`, {\n error: err instanceof Error ? err.message : err,\n originalKey,\n size: v.size,\n });\n }\n }),\n );\n\n if (variantKeys.length === 0) {\n logError(`[image-variant] No variants generated for ${originalKey}`);\n }\n } catch (err) {\n logError(`[image-variant] Failed to generate variants for ${originalKey}`, {\n error: err instanceof Error ? err.message : err,\n });\n }\n\n return {\n url: original.url,\n key: original.key,\n size: original.size,\n variants,\n variantKeys,\n };\n}\n\nexport async function deleteFromR2(key: string): Promise<void> {\n const safeKey = sanitizeStorageKey(key);\n const s3 = getClient();\n const { bucketName: bucket } = requireR2Credentials();\n await s3.send(\n new DeleteObjectCommand({\n Bucket: bucket,\n Key: safeKey,\n }),\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-YPBKY4KY.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkS7GOXCRMjs = require('./chunk-S7GOXCRM.js');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkKWKV2GAHjs = require('./chunk-KWKV2GAH.js');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
|
|
11
|
+
|
|
12
|
+
// src/hooks/useImageDropZone.ts
|
|
13
|
+
var _react = require('react');
|
|
14
|
+
var ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
15
|
+
var MAX_UPLOAD_SIZE = 5 * 1024 * 1024;
|
|
16
|
+
function validateFile(file) {
|
|
17
|
+
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
18
|
+
const allowed = ALLOWED_TYPES.map((t) => t.split("/")[1]).join(", ");
|
|
19
|
+
return `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4. (\uD5C8\uC6A9: ${allowed})`;
|
|
20
|
+
}
|
|
21
|
+
const sizeError = _chunkS7GOXCRMjs.validateImageSize.call(void 0, file);
|
|
22
|
+
if (sizeError) return sizeError;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async function uploadSingleFile(file) {
|
|
26
|
+
const fd = new FormData();
|
|
27
|
+
fd.append("file", file);
|
|
28
|
+
const { uploadEndpoint } = _chunkEG6NVPP2js.resolveRouteConfig.call(void 0, );
|
|
29
|
+
const res = await _chunkKWKV2GAHjs.adminFetch.call(void 0, uploadEndpoint, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: fd
|
|
32
|
+
});
|
|
33
|
+
const json = await res.json();
|
|
34
|
+
if (!json.success) {
|
|
35
|
+
throw new Error(_optionalChain([json, 'access', _ => _.error, 'optionalAccess', _2 => _2.message]) || "\uC5C5\uB85C\uB4DC \uC2E4\uD328");
|
|
36
|
+
}
|
|
37
|
+
return json.data;
|
|
38
|
+
}
|
|
39
|
+
function useImageDropZone(options) {
|
|
40
|
+
const {
|
|
41
|
+
multiple = false,
|
|
42
|
+
maxFiles,
|
|
43
|
+
onUpload,
|
|
44
|
+
onKeyTracked,
|
|
45
|
+
disabled = false
|
|
46
|
+
} = options;
|
|
47
|
+
const [isDragOver, setIsDragOver] = _react.useState.call(void 0, false);
|
|
48
|
+
const [isUploading, setIsUploading] = _react.useState.call(void 0, false);
|
|
49
|
+
const [isResizing, setIsResizing] = _react.useState.call(void 0, false);
|
|
50
|
+
const [error, setError] = _react.useState.call(void 0, null);
|
|
51
|
+
const dragCounterRef = _react.useRef.call(void 0, 0);
|
|
52
|
+
const processFiles = _react.useCallback.call(void 0,
|
|
53
|
+
async (files) => {
|
|
54
|
+
if (disabled || files.length === 0) return;
|
|
55
|
+
let toProcess = multiple ? files : [files[0]];
|
|
56
|
+
if (maxFiles && toProcess.length > maxFiles) {
|
|
57
|
+
toProcess = toProcess.slice(0, maxFiles);
|
|
58
|
+
setError(`\uCD5C\uB300 ${maxFiles}\uAC1C\uAE4C\uC9C0 \uC5C5\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
59
|
+
}
|
|
60
|
+
for (const file of toProcess) {
|
|
61
|
+
const validationError = validateFile(file);
|
|
62
|
+
if (validationError) {
|
|
63
|
+
setError(validationError);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setError(null);
|
|
68
|
+
setIsResizing(true);
|
|
69
|
+
let resizedFiles;
|
|
70
|
+
try {
|
|
71
|
+
resizedFiles = [];
|
|
72
|
+
for (const file of toProcess) {
|
|
73
|
+
const result = await _chunkS7GOXCRMjs.resizeImageIfNeeded.call(void 0, file);
|
|
74
|
+
if (result.wasResized && process.env.NODE_ENV === "development") {
|
|
75
|
+
console.log(
|
|
76
|
+
`[image-optimize] ${(result.originalSize / 1024 / 1024).toFixed(1)}MB \u2192 ${(result.newSize / 1024 / 1024).toFixed(1)}MB`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (result.file.size > MAX_UPLOAD_SIZE) {
|
|
80
|
+
setError(
|
|
81
|
+
`\uC774\uBBF8\uC9C0 \uCD5C\uC801\uD654 \uD6C4\uC5D0\uB3C4 5MB\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4. (${(result.file.size / 1024 / 1024).toFixed(1)}MB) \uB354 \uC791\uC740 \uC774\uBBF8\uC9C0\uB97C \uC0AC\uC6A9\uD574 \uC8FC\uC138\uC694.`
|
|
82
|
+
);
|
|
83
|
+
setIsResizing(false);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resizedFiles.push(result.file);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : "\uC774\uBBF8\uC9C0 \uCD5C\uC801\uD654 \uC911 \uC624\uB958 \uBC1C\uC0DD";
|
|
90
|
+
setError(msg);
|
|
91
|
+
setIsResizing(false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setIsResizing(false);
|
|
95
|
+
setIsUploading(true);
|
|
96
|
+
try {
|
|
97
|
+
for (const file of resizedFiles) {
|
|
98
|
+
const result = await uploadSingleFile(file);
|
|
99
|
+
onUpload(result);
|
|
100
|
+
if (result.key) _optionalChain([onKeyTracked, 'optionalCall', _3 => _3(result.key)]);
|
|
101
|
+
if (result.variantKeys) {
|
|
102
|
+
for (const vk of result.variantKeys) _optionalChain([onKeyTracked, 'optionalCall', _4 => _4(vk)]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : "\uC5C5\uB85C\uB4DC \uC911 \uC624\uB958 \uBC1C\uC0DD";
|
|
107
|
+
setError(msg);
|
|
108
|
+
} finally {
|
|
109
|
+
setIsUploading(false);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[multiple, maxFiles, onUpload, onKeyTracked, disabled]
|
|
113
|
+
);
|
|
114
|
+
const onDragEnter = _react.useCallback.call(void 0, (e) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
dragCounterRef.current++;
|
|
118
|
+
if (_optionalChain([e, 'access', _5 => _5.dataTransfer, 'optionalAccess', _6 => _6.types, 'access', _7 => _7.includes, 'call', _8 => _8("Files")])) {
|
|
119
|
+
setIsDragOver(true);
|
|
120
|
+
}
|
|
121
|
+
}, []);
|
|
122
|
+
const onDragOver = _react.useCallback.call(void 0, (e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
}, []);
|
|
126
|
+
const onDragLeave = _react.useCallback.call(void 0, (e) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
dragCounterRef.current--;
|
|
130
|
+
if (dragCounterRef.current === 0) {
|
|
131
|
+
setIsDragOver(false);
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
const onDrop = _react.useCallback.call(void 0,
|
|
135
|
+
(e) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
dragCounterRef.current = 0;
|
|
139
|
+
setIsDragOver(false);
|
|
140
|
+
if (disabled) return;
|
|
141
|
+
const files = Array.from(_optionalChain([e, 'access', _9 => _9.dataTransfer, 'optionalAccess', _10 => _10.files]) || []).filter(
|
|
142
|
+
(f) => f.type.startsWith("image/")
|
|
143
|
+
);
|
|
144
|
+
if (files.length === 0) {
|
|
145
|
+
setError("\uC774\uBBF8\uC9C0 \uD30C\uC77C\uB9CC \uC5C5\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
processFiles(files);
|
|
149
|
+
},
|
|
150
|
+
[processFiles, disabled]
|
|
151
|
+
);
|
|
152
|
+
const handleFileInput = _react.useCallback.call(void 0,
|
|
153
|
+
async (fileList) => {
|
|
154
|
+
const files = Array.from(fileList || []);
|
|
155
|
+
if (files.length > 0) {
|
|
156
|
+
await processFiles(files);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[processFiles]
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
isDragOver,
|
|
163
|
+
isUploading,
|
|
164
|
+
isResizing,
|
|
165
|
+
error,
|
|
166
|
+
dragHandlers: { onDragEnter, onDragOver, onDragLeave, onDrop },
|
|
167
|
+
handleFileInput
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
exports.useImageDropZone = useImageDropZone;
|
|
174
|
+
//# sourceMappingURL=chunk-YY7K5CXX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-YY7K5CXX.js","../src/hooks/useImageDropZone.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-YY7K5CXX.js","sourcesContent":[null,"\"use client\";\n\nimport { useState, useCallback, useRef, type DragEvent } from \"react\";\nimport { adminFetch } from \"../utils/admin-fetch\";\nimport {\n resizeImageIfNeeded,\n validateImageSize,\n} from \"../utils/image-resize\";\nimport { resolveRouteConfig } from \"../config\";\n\nconst ALLOWED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"];\nconst MAX_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB\n\ninterface ImageVariantUrls {\n lg?: string;\n md?: string;\n sm?: string;\n thumb?: string;\n}\n\ninterface UploadResult {\n url: string;\n key: string;\n size: number;\n variants?: ImageVariantUrls;\n variantKeys?: string[];\n}\n\ninterface UseImageDropZoneOptions {\n multiple?: boolean;\n maxFiles?: number;\n onUpload: (result: UploadResult) => void;\n onKeyTracked?: (key: string) => void;\n disabled?: boolean;\n}\n\nfunction validateFile(file: File): string | null {\n if (!ALLOWED_TYPES.includes(file.type)) {\n const allowed = ALLOWED_TYPES.map((t) => t.split(\"/\")[1]).join(\", \");\n return `지원하지 않는 파일 형식입니다. (허용: ${allowed})`;\n }\n const sizeError = validateImageSize(file);\n if (sizeError) return sizeError;\n return null;\n}\n\nasync function uploadSingleFile(file: File): Promise<UploadResult> {\n const fd = new FormData();\n fd.append(\"file\", file);\n // upload 엔드포인트는 §5 config boundary 에서 해석된다 (consumer override\n // 가능; 미설정 시 레거시 기본 경로 — spec.md §4.1 C2 / B5).\n const { uploadEndpoint } = resolveRouteConfig();\n const res = await adminFetch(uploadEndpoint, {\n method: \"POST\",\n body: fd,\n });\n const json = await res.json();\n if (!json.success) {\n throw new Error(json.error?.message || \"업로드 실패\");\n }\n return json.data;\n}\n\nexport function useImageDropZone(options: UseImageDropZoneOptions) {\n const {\n multiple = false,\n maxFiles,\n onUpload,\n onKeyTracked,\n disabled = false,\n } = options;\n\n const [isDragOver, setIsDragOver] = useState(false);\n const [isUploading, setIsUploading] = useState(false);\n const [isResizing, setIsResizing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const dragCounterRef = useRef(0);\n\n const processFiles = useCallback(\n async (files: File[]) => {\n if (disabled || files.length === 0) return;\n\n let toProcess = multiple ? files : [files[0]];\n\n if (maxFiles && toProcess.length > maxFiles) {\n toProcess = toProcess.slice(0, maxFiles);\n setError(`최대 ${maxFiles}개까지 업로드할 수 있습니다.`);\n }\n\n for (const file of toProcess) {\n const validationError = validateFile(file);\n if (validationError) {\n setError(validationError);\n return;\n }\n }\n\n setError(null);\n\n setIsResizing(true);\n let resizedFiles: File[];\n try {\n resizedFiles = [];\n for (const file of toProcess) {\n const result = await resizeImageIfNeeded(file);\n if (result.wasResized && process.env.NODE_ENV === \"development\") {\n console.log(\n `[image-optimize] ${(result.originalSize / 1024 / 1024).toFixed(1)}MB → ${(result.newSize / 1024 / 1024).toFixed(1)}MB`\n );\n }\n if (result.file.size > MAX_UPLOAD_SIZE) {\n setError(\n `이미지 최적화 후에도 5MB를 초과합니다. (${(result.file.size / 1024 / 1024).toFixed(1)}MB) 더 작은 이미지를 사용해 주세요.`\n );\n setIsResizing(false);\n return;\n }\n resizedFiles.push(result.file);\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"이미지 최적화 중 오류 발생\";\n setError(msg);\n setIsResizing(false);\n return;\n }\n setIsResizing(false);\n\n setIsUploading(true);\n try {\n for (const file of resizedFiles) {\n const result = await uploadSingleFile(file);\n onUpload(result);\n if (result.key) onKeyTracked?.(result.key);\n if (result.variantKeys) {\n for (const vk of result.variantKeys) onKeyTracked?.(vk);\n }\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : \"업로드 중 오류 발생\";\n setError(msg);\n } finally {\n setIsUploading(false);\n }\n },\n [multiple, maxFiles, onUpload, onKeyTracked, disabled]\n );\n\n const onDragEnter = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current++;\n if (e.dataTransfer?.types.includes(\"Files\")) {\n setIsDragOver(true);\n }\n }, []);\n\n const onDragOver = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n }, []);\n\n const onDragLeave = useCallback((e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current--;\n if (dragCounterRef.current === 0) {\n setIsDragOver(false);\n }\n }, []);\n\n const onDrop = useCallback(\n (e: DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current = 0;\n setIsDragOver(false);\n\n if (disabled) return;\n\n const files = Array.from(e.dataTransfer?.files || []).filter((f) =>\n f.type.startsWith(\"image/\")\n );\n\n if (files.length === 0) {\n setError(\"이미지 파일만 업로드할 수 있습니다.\");\n return;\n }\n\n processFiles(files);\n },\n [processFiles, disabled]\n );\n\n const handleFileInput = useCallback(\n async (fileList: FileList | null) => {\n const files = Array.from(fileList || []);\n if (files.length > 0) {\n await processFiles(files);\n }\n },\n [processFiles]\n );\n\n return {\n isDragOver,\n isUploading,\n isResizing,\n error,\n dragHandlers: { onDragEnter, onDragOver, onDragLeave, onDrop },\n handleFileInput,\n };\n}\n"]}
|
|
@@ -0,0 +1,181 @@
|
|
|
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; }// src/components/ResizableImage.tsx
|
|
2
|
+
var _core = require('@tiptap/core');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _react = require('@tiptap/react');
|
|
7
|
+
var _react3 = require('react');
|
|
8
|
+
var _jsxruntime = require('react/jsx-runtime');
|
|
9
|
+
function AlignIcon({ type }) {
|
|
10
|
+
const w = 12;
|
|
11
|
+
const lines = [
|
|
12
|
+
{ y: 0, lw: w },
|
|
13
|
+
{ y: 3.5, lw: 7 },
|
|
14
|
+
{ y: 7, lw: 10 }
|
|
15
|
+
];
|
|
16
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { width: w, height: "10", viewBox: `0 0 ${w} 10`, fill: "currentColor", children: lines.map((l, i) => {
|
|
17
|
+
const x = type === "center" ? (w - l.lw) / 2 : type === "right" ? w - l.lw : 0;
|
|
18
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "rect", { x, y: l.y, width: l.lw, height: "1.5", rx: ".5" }, i);
|
|
19
|
+
}) });
|
|
20
|
+
}
|
|
21
|
+
function ResizableImageComponent(props) {
|
|
22
|
+
const { node, updateAttributes, selected } = props;
|
|
23
|
+
const imgRef = _react3.useRef.call(void 0, null);
|
|
24
|
+
const [resizing, setResizing] = _react3.useState.call(void 0, false);
|
|
25
|
+
const src = node.attrs.src;
|
|
26
|
+
const alt = node.attrs.alt || "";
|
|
27
|
+
const title = node.attrs.title || void 0;
|
|
28
|
+
const width = node.attrs.width;
|
|
29
|
+
const align = node.attrs.align || "center";
|
|
30
|
+
const startResize = _react3.useCallback.call(void 0,
|
|
31
|
+
(e, handle) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
const startX = e.clientX;
|
|
35
|
+
const startY = e.clientY;
|
|
36
|
+
const startWidth = _optionalChain([imgRef, 'access', _ => _.current, 'optionalAccess', _2 => _2.offsetWidth]) || 300;
|
|
37
|
+
const startHeight = _optionalChain([imgRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.offsetHeight]) || 200;
|
|
38
|
+
const aspectRatio = startWidth / startHeight;
|
|
39
|
+
const isEdge = handle.length === 1;
|
|
40
|
+
const isHorizontal = handle === "e" || handle === "w";
|
|
41
|
+
const signX = handle.includes("e") ? 1 : -1;
|
|
42
|
+
const signY = handle.includes("s") ? 1 : -1;
|
|
43
|
+
const calcNewWidth = (ev) => {
|
|
44
|
+
const dx = (ev.clientX - startX) * signX;
|
|
45
|
+
const dy = (ev.clientY - startY) * signY;
|
|
46
|
+
let delta;
|
|
47
|
+
if (isEdge) {
|
|
48
|
+
delta = isHorizontal ? dx : dy * aspectRatio;
|
|
49
|
+
} else {
|
|
50
|
+
delta = Math.abs(dx) > Math.abs(dy) ? dx : dy * aspectRatio;
|
|
51
|
+
}
|
|
52
|
+
return Math.max(80, startWidth + delta);
|
|
53
|
+
};
|
|
54
|
+
const onMouseMove = (ev) => {
|
|
55
|
+
const newWidth = calcNewWidth(ev);
|
|
56
|
+
if (imgRef.current) {
|
|
57
|
+
imgRef.current.style.width = `${newWidth}px`;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const onMouseUp = (ev) => {
|
|
61
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
62
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
63
|
+
setResizing(false);
|
|
64
|
+
updateAttributes({ width: Math.round(calcNewWidth(ev)) });
|
|
65
|
+
};
|
|
66
|
+
setResizing(true);
|
|
67
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
68
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
69
|
+
},
|
|
70
|
+
[updateAttributes]
|
|
71
|
+
);
|
|
72
|
+
const handles = ["nw", "n", "ne", "w", "e", "sw", "s", "se"];
|
|
73
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
74
|
+
_react.NodeViewWrapper,
|
|
75
|
+
{
|
|
76
|
+
className: `resizable-image-wrapper resizable-image-align-${align} ${selected ? "selected" : ""} ${resizing ? "resizing" : ""}`,
|
|
77
|
+
children: [
|
|
78
|
+
selected && !resizing && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "resizable-image-align-toolbar", children: ["left", "center", "right"].map((a) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
79
|
+
"button",
|
|
80
|
+
{
|
|
81
|
+
type: "button",
|
|
82
|
+
className: `resizable-image-align-btn ${align === a ? "active" : ""}`,
|
|
83
|
+
onMouseDown: (e) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
updateAttributes({ align: a });
|
|
86
|
+
},
|
|
87
|
+
title: a === "left" ? "\uC67C\uCABD \uC815\uB82C" : a === "center" ? "\uAC00\uC6B4\uB370 \uC815\uB82C" : "\uC624\uB978\uCABD \uC815\uB82C",
|
|
88
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, AlignIcon, { type: a })
|
|
89
|
+
},
|
|
90
|
+
a
|
|
91
|
+
)) }),
|
|
92
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
className: "resizable-image-container",
|
|
96
|
+
style: { width: width ? `${width}px` : void 0 },
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
99
|
+
"img",
|
|
100
|
+
{
|
|
101
|
+
ref: imgRef,
|
|
102
|
+
src,
|
|
103
|
+
alt,
|
|
104
|
+
title,
|
|
105
|
+
style: { width: width ? `${width}px` : void 0 },
|
|
106
|
+
draggable: false
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
(selected || resizing) && handles.map((h) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
110
|
+
"div",
|
|
111
|
+
{
|
|
112
|
+
className: `resizable-image-handle resizable-image-handle-${h}`,
|
|
113
|
+
onMouseDown: (e) => startResize(e, h)
|
|
114
|
+
},
|
|
115
|
+
h
|
|
116
|
+
))
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
var ResizableImage = _core.Node.create({
|
|
125
|
+
name: "image",
|
|
126
|
+
group: "block",
|
|
127
|
+
atom: true,
|
|
128
|
+
draggable: true,
|
|
129
|
+
addAttributes() {
|
|
130
|
+
return {
|
|
131
|
+
src: { default: null },
|
|
132
|
+
alt: { default: null },
|
|
133
|
+
title: { default: null },
|
|
134
|
+
width: { default: null },
|
|
135
|
+
align: {
|
|
136
|
+
default: "center",
|
|
137
|
+
parseHTML: (element) => element.getAttribute("data-align") || "center",
|
|
138
|
+
renderHTML: (attributes) => ({
|
|
139
|
+
"data-align": attributes.align
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
parseHTML() {
|
|
145
|
+
return [{ tag: "img[src]" }];
|
|
146
|
+
},
|
|
147
|
+
renderHTML({ HTMLAttributes }) {
|
|
148
|
+
const attrs = { ...HTMLAttributes };
|
|
149
|
+
const styles = [];
|
|
150
|
+
if (attrs.width) {
|
|
151
|
+
styles.push(`width: ${attrs.width}px`);
|
|
152
|
+
}
|
|
153
|
+
const align = attrs["data-align"] || "center";
|
|
154
|
+
styles.push("display: block");
|
|
155
|
+
if (align === "center") {
|
|
156
|
+
styles.push("margin-left: auto", "margin-right: auto");
|
|
157
|
+
} else if (align === "right") {
|
|
158
|
+
styles.push("margin-left: auto");
|
|
159
|
+
}
|
|
160
|
+
if (styles.length) attrs.style = styles.join("; ");
|
|
161
|
+
return ["img", _core.mergeAttributes.call(void 0, attrs)];
|
|
162
|
+
},
|
|
163
|
+
addNodeView() {
|
|
164
|
+
return _react.ReactNodeViewRenderer.call(void 0, ResizableImageComponent);
|
|
165
|
+
},
|
|
166
|
+
addCommands() {
|
|
167
|
+
return {
|
|
168
|
+
setImage: (options) => ({ commands }) => {
|
|
169
|
+
return commands.insertContent({
|
|
170
|
+
type: this.name,
|
|
171
|
+
attrs: options
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
exports.ResizableImage = ResizableImage;
|
|
181
|
+
//# sourceMappingURL=chunk-ZFORODOX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-ZFORODOX.js","../src/components/ResizableImage.tsx"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-ZFORODOX.js","sourcesContent":[null,"\"use client\";\n\nimport { Node, mergeAttributes } from \"@tiptap/core\";\nimport {\n ReactNodeViewRenderer,\n NodeViewWrapper,\n type NodeViewProps,\n} from \"@tiptap/react\";\nimport { useCallback, useRef, useState } from \"react\";\n\ntype Handle = \"nw\" | \"ne\" | \"sw\" | \"se\" | \"n\" | \"s\" | \"e\" | \"w\";\n\nfunction AlignIcon({ type }: { type: \"left\" | \"center\" | \"right\" }) {\n const w = 12;\n const lines = [\n { y: 0, lw: w },\n { y: 3.5, lw: 7 },\n { y: 7, lw: 10 },\n ];\n return (\n <svg width={w} height=\"10\" viewBox={`0 0 ${w} 10`} fill=\"currentColor\">\n {lines.map((l, i) => {\n const x =\n type === \"center\"\n ? (w - l.lw) / 2\n : type === \"right\"\n ? w - l.lw\n : 0;\n return <rect key={i} x={x} y={l.y} width={l.lw} height=\"1.5\" rx=\".5\" />;\n })}\n </svg>\n );\n}\n\nfunction ResizableImageComponent(props: NodeViewProps) {\n const { node, updateAttributes, selected } = props;\n const imgRef = useRef<HTMLImageElement>(null);\n const [resizing, setResizing] = useState(false);\n\n const src = node.attrs.src as string;\n const alt = (node.attrs.alt as string) || \"\";\n const title = (node.attrs.title as string) || undefined;\n const width = node.attrs.width as number | null;\n const align = (node.attrs.align as string) || \"center\";\n\n const startResize = useCallback(\n (e: React.MouseEvent, handle: Handle) => {\n e.preventDefault();\n e.stopPropagation();\n\n const startX = e.clientX;\n const startY = e.clientY;\n const startWidth = imgRef.current?.offsetWidth || 300;\n const startHeight = imgRef.current?.offsetHeight || 200;\n const aspectRatio = startWidth / startHeight;\n\n const isEdge = handle.length === 1; // \"n\", \"s\", \"e\", \"w\"\n const isHorizontal = handle === \"e\" || handle === \"w\";\n\n // x 방향 부호: 오른쪽 핸들이면 +1, 왼쪽이면 -1\n const signX = handle.includes(\"e\") ? 1 : -1;\n // y 방향 부호: 아래쪽 핸들이면 +1, 위쪽이면 -1\n const signY = handle.includes(\"s\") ? 1 : -1;\n\n const calcNewWidth = (ev: MouseEvent) => {\n const dx = (ev.clientX - startX) * signX;\n const dy = (ev.clientY - startY) * signY;\n\n let delta: number;\n if (isEdge) {\n // 변 중간 핸들: 해당 축 방향만 사용하되 비율 유지\n delta = isHorizontal ? dx : dy * aspectRatio;\n } else {\n // 모서리 핸들: 더 큰 변화량 기준으로 비율 유지\n delta = Math.abs(dx) > Math.abs(dy) ? dx : dy * aspectRatio;\n }\n return Math.max(80, startWidth + delta);\n };\n\n const onMouseMove = (ev: MouseEvent) => {\n const newWidth = calcNewWidth(ev);\n if (imgRef.current) {\n imgRef.current.style.width = `${newWidth}px`;\n }\n };\n\n const onMouseUp = (ev: MouseEvent) => {\n document.removeEventListener(\"mousemove\", onMouseMove);\n document.removeEventListener(\"mouseup\", onMouseUp);\n setResizing(false);\n updateAttributes({ width: Math.round(calcNewWidth(ev)) });\n };\n\n setResizing(true);\n document.addEventListener(\"mousemove\", onMouseMove);\n document.addEventListener(\"mouseup\", onMouseUp);\n },\n [updateAttributes]\n );\n\n const handles: Handle[] = [\"nw\", \"n\", \"ne\", \"w\", \"e\", \"sw\", \"s\", \"se\"];\n\n return (\n <NodeViewWrapper\n className={`resizable-image-wrapper resizable-image-align-${align} ${selected ? \"selected\" : \"\"} ${resizing ? \"resizing\" : \"\"}`}\n >\n {selected && !resizing && (\n <div className=\"resizable-image-align-toolbar\">\n {([\"left\", \"center\", \"right\"] as const).map((a) => (\n <button\n key={a}\n type=\"button\"\n className={`resizable-image-align-btn ${align === a ? \"active\" : \"\"}`}\n onMouseDown={(e) => {\n e.preventDefault();\n updateAttributes({ align: a });\n }}\n title={a === \"left\" ? \"왼쪽 정렬\" : a === \"center\" ? \"가운데 정렬\" : \"오른쪽 정렬\"}\n >\n <AlignIcon type={a} />\n </button>\n ))}\n </div>\n )}\n <div\n className=\"resizable-image-container\"\n style={{ width: width ? `${width}px` : undefined }}\n >\n <img\n ref={imgRef}\n src={src}\n alt={alt}\n title={title}\n style={{ width: width ? `${width}px` : undefined }}\n draggable={false}\n />\n {(selected || resizing) &&\n handles.map((h) => (\n <div\n key={h}\n className={`resizable-image-handle resizable-image-handle-${h}`}\n onMouseDown={(e) => startResize(e, h)}\n />\n ))}\n </div>\n </NodeViewWrapper>\n );\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n resizableImage: {\n setImage: (options: {\n src: string;\n alt?: string;\n title?: string;\n width?: number;\n align?: string;\n }) => ReturnType;\n };\n }\n}\n\nexport const ResizableImage = Node.create({\n name: \"image\",\n group: \"block\",\n atom: true,\n draggable: true,\n\n addAttributes() {\n return {\n src: { default: null },\n alt: { default: null },\n title: { default: null },\n width: { default: null },\n align: {\n default: \"center\",\n parseHTML: (element: HTMLElement) =>\n element.getAttribute(\"data-align\") || \"center\",\n renderHTML: (attributes: Record<string, unknown>) => ({\n \"data-align\": attributes.align,\n }),\n },\n };\n },\n\n parseHTML() {\n return [{ tag: \"img[src]\" }];\n },\n\n renderHTML({ HTMLAttributes }) {\n const attrs = { ...HTMLAttributes };\n const styles: string[] = [];\n if (attrs.width) {\n styles.push(`width: ${attrs.width}px`);\n }\n const align = attrs[\"data-align\"] || \"center\";\n styles.push(\"display: block\");\n if (align === \"center\") {\n styles.push(\"margin-left: auto\", \"margin-right: auto\");\n } else if (align === \"right\") {\n styles.push(\"margin-left: auto\");\n }\n if (styles.length) attrs.style = styles.join(\"; \");\n return [\"img\", mergeAttributes(attrs)];\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ResizableImageComponent);\n },\n\n addCommands() {\n return {\n setImage:\n (options: {\n src: string;\n alt?: string;\n title?: string;\n width?: number;\n align?: string;\n }) =>\n ({ commands }) => {\n return commands.insertContent({\n type: this.name,\n attrs: options,\n });\n },\n } satisfies Partial<import(\"@tiptap/core\").RawCommands>;\n },\n});\n"]}
|