@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,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adminFetch
|
|
3
|
+
} from "./chunk-T7EVFD4G.mjs";
|
|
4
|
+
import {
|
|
5
|
+
resolveBrandConfig,
|
|
6
|
+
resolveRouteConfig
|
|
7
|
+
} from "./chunk-BWKKHCDG.mjs";
|
|
8
|
+
|
|
9
|
+
// src/components/AdminShell.tsx
|
|
10
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
13
|
+
import dynamic from "next/dynamic";
|
|
14
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var Toaster = dynamic(
|
|
16
|
+
() => import("sonner").then((m) => m.Toaster),
|
|
17
|
+
{ ssr: false }
|
|
18
|
+
);
|
|
19
|
+
function AdminShell({
|
|
20
|
+
children,
|
|
21
|
+
brandLabel,
|
|
22
|
+
brandHref,
|
|
23
|
+
adminHref,
|
|
24
|
+
navItems,
|
|
25
|
+
loginPath,
|
|
26
|
+
meEndpoint,
|
|
27
|
+
logoutEndpoint
|
|
28
|
+
}) {
|
|
29
|
+
const pathname = usePathname();
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const routerRef = useRef(router);
|
|
32
|
+
routerRef.current = router;
|
|
33
|
+
const brandSuppliedViaProps = brandLabel !== void 0 || navItems !== void 0;
|
|
34
|
+
const brandCfg = resolveBrandConfig(brandSuppliedViaProps);
|
|
35
|
+
const routeCfg = resolveRouteConfig();
|
|
36
|
+
const resolvedBrandLabel = brandLabel ?? brandCfg.brandLabel ?? null;
|
|
37
|
+
const resolvedBrandHref = brandHref ?? brandCfg.brandHref;
|
|
38
|
+
const resolvedAdminHref = adminHref ?? brandCfg.adminHref;
|
|
39
|
+
const resolvedNav = navItems ?? brandCfg.navItems;
|
|
40
|
+
const resolvedLoginPath = loginPath ?? routeCfg.loginPath;
|
|
41
|
+
const resolvedMeEndpoint = meEndpoint ?? routeCfg.meEndpoint;
|
|
42
|
+
const resolvedLogoutEndpoint = logoutEndpoint ?? routeCfg.logoutEndpoint;
|
|
43
|
+
const [checking, setChecking] = useState(true);
|
|
44
|
+
const [user, setUser] = useState(null);
|
|
45
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
46
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
47
|
+
if (typeof window !== "undefined") {
|
|
48
|
+
return localStorage.getItem("admin_sidebar_collapsed") === "true";
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
const DEFAULT_WIDTH = 200;
|
|
53
|
+
const MIN_WIDTH = 200;
|
|
54
|
+
const MAX_WIDTH = 400;
|
|
55
|
+
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
|
56
|
+
if (typeof window !== "undefined") {
|
|
57
|
+
const saved = localStorage.getItem("admin_sidebar_width");
|
|
58
|
+
return saved ? Number(saved) : DEFAULT_WIDTH;
|
|
59
|
+
}
|
|
60
|
+
return DEFAULT_WIDTH;
|
|
61
|
+
});
|
|
62
|
+
const isResizing = useRef(false);
|
|
63
|
+
const widthRef = useRef(sidebarWidth);
|
|
64
|
+
const [dragging, setDragging] = useState(false);
|
|
65
|
+
const isLoginPage = pathname === resolvedLoginPath;
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (isLoginPage) {
|
|
68
|
+
setChecking(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
async function checkAuth() {
|
|
73
|
+
try {
|
|
74
|
+
const res = await adminFetch(resolvedMeEndpoint);
|
|
75
|
+
if (cancelled) return;
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
routerRef.current.replace(resolvedLoginPath);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const data = await res.json();
|
|
81
|
+
if (data.success && data.data?.user) {
|
|
82
|
+
const u = data.data.user;
|
|
83
|
+
setUser(
|
|
84
|
+
(prev) => prev?.email === u.email && prev?.id === u.id ? prev : u
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
if (!cancelled) {
|
|
89
|
+
routerRef.current.replace(resolvedLoginPath);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
setChecking(false);
|
|
94
|
+
}
|
|
95
|
+
checkAuth();
|
|
96
|
+
return () => {
|
|
97
|
+
cancelled = true;
|
|
98
|
+
};
|
|
99
|
+
}, [isLoginPage]);
|
|
100
|
+
const handleMouseMove = useCallback((e) => {
|
|
101
|
+
if (!isResizing.current) return;
|
|
102
|
+
const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));
|
|
103
|
+
widthRef.current = newWidth;
|
|
104
|
+
setSidebarWidth(newWidth);
|
|
105
|
+
}, []);
|
|
106
|
+
const handleMouseUp = useCallback(() => {
|
|
107
|
+
if (!isResizing.current) return;
|
|
108
|
+
isResizing.current = false;
|
|
109
|
+
setDragging(false);
|
|
110
|
+
document.body.style.cursor = "";
|
|
111
|
+
document.body.style.userSelect = "";
|
|
112
|
+
localStorage.setItem("admin_sidebar_width", String(widthRef.current));
|
|
113
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
114
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
115
|
+
}, [handleMouseMove]);
|
|
116
|
+
if (isLoginPage) {
|
|
117
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
118
|
+
}
|
|
119
|
+
if (checking) {
|
|
120
|
+
return /* @__PURE__ */ jsx("div", { className: "admin-auth-loading", children: "\uC778\uC99D \uD655\uC778 \uC911..." });
|
|
121
|
+
}
|
|
122
|
+
async function handleLogout() {
|
|
123
|
+
await fetch(resolvedLogoutEndpoint, { method: "POST", credentials: "same-origin" });
|
|
124
|
+
router.replace(resolvedLoginPath);
|
|
125
|
+
}
|
|
126
|
+
function toggleSidebar() {
|
|
127
|
+
setCollapsed((prev) => {
|
|
128
|
+
const next = !prev;
|
|
129
|
+
localStorage.setItem("admin_sidebar_collapsed", String(next));
|
|
130
|
+
return next;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function startResize(e) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
isResizing.current = true;
|
|
136
|
+
setDragging(true);
|
|
137
|
+
document.body.style.cursor = "col-resize";
|
|
138
|
+
document.body.style.userSelect = "none";
|
|
139
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
140
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
141
|
+
}
|
|
142
|
+
return /* @__PURE__ */ jsxs("div", { className: `admin-layout${collapsed ? " admin-sidebar-collapsed" : ""}${dragging ? " admin-resizing" : ""}${mobileOpen ? " admin-sidebar-mobile-open" : ""}`, children: [
|
|
143
|
+
/* @__PURE__ */ jsx(
|
|
144
|
+
"button",
|
|
145
|
+
{
|
|
146
|
+
className: "admin-mobile-toggle",
|
|
147
|
+
onClick: () => setMobileOpen(true),
|
|
148
|
+
"aria-label": "\uBA54\uB274 \uC5F4\uAE30",
|
|
149
|
+
children: "\u2630"
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
mobileOpen && /* @__PURE__ */ jsx("div", { className: "admin-sidebar-overlay", onClick: () => setMobileOpen(false) }),
|
|
153
|
+
/* @__PURE__ */ jsxs(
|
|
154
|
+
"aside",
|
|
155
|
+
{
|
|
156
|
+
className: "admin-sidebar",
|
|
157
|
+
style: !collapsed ? { width: sidebarWidth } : void 0,
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsxs("div", { className: "admin-sidebar-header", children: [
|
|
160
|
+
!collapsed && /* @__PURE__ */ jsxs("div", { className: "admin-sidebar-logo", children: [
|
|
161
|
+
resolvedBrandLabel && /* @__PURE__ */ jsx("a", { href: resolvedBrandHref, className: "admin-logo-home", title: "\uC0AC\uC774\uD2B8 \uBCF4\uAE30", target: "_blank", rel: "noopener noreferrer", children: resolvedBrandLabel }),
|
|
162
|
+
/* @__PURE__ */ jsx(Link, { href: resolvedAdminHref, className: "admin-logo-admin", children: "Admin" })
|
|
163
|
+
] }),
|
|
164
|
+
/* @__PURE__ */ jsx(
|
|
165
|
+
"button",
|
|
166
|
+
{
|
|
167
|
+
className: "admin-sidebar-toggle",
|
|
168
|
+
onClick: () => {
|
|
169
|
+
if (mobileOpen) {
|
|
170
|
+
setMobileOpen(false);
|
|
171
|
+
} else {
|
|
172
|
+
toggleSidebar();
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
title: collapsed ? "\uBA54\uB274 \uD3BC\uCE58\uAE30" : "\uBA54\uB274 \uC811\uAE30",
|
|
176
|
+
children: mobileOpen ? "\u2715" : collapsed ? "\u203A" : "\u2039"
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
] }),
|
|
180
|
+
!collapsed && user && /* @__PURE__ */ jsx("div", { className: "admin-sidebar-user", children: user.email }),
|
|
181
|
+
/* @__PURE__ */ jsx("nav", { className: "admin-sidebar-nav", children: resolvedNav.map((item) => /* @__PURE__ */ jsx(
|
|
182
|
+
Link,
|
|
183
|
+
{
|
|
184
|
+
href: item.href,
|
|
185
|
+
className: "admin-sidebar-link",
|
|
186
|
+
title: item.label,
|
|
187
|
+
onClick: () => setMobileOpen(false),
|
|
188
|
+
children: collapsed ? item.glyph : item.label
|
|
189
|
+
},
|
|
190
|
+
item.href
|
|
191
|
+
)) }),
|
|
192
|
+
/* @__PURE__ */ jsx("div", { className: "admin-sidebar-footer", children: /* @__PURE__ */ jsx("button", { className: "admin-sidebar-logout", onClick: handleLogout, title: "\uB85C\uADF8\uC544\uC6C3", children: collapsed ? "\u2715" : "\uB85C\uADF8\uC544\uC6C3" }) }),
|
|
193
|
+
!collapsed && /* @__PURE__ */ jsx(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
className: "admin-sidebar-resize",
|
|
197
|
+
onMouseDown: startResize
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
/* @__PURE__ */ jsx(
|
|
204
|
+
"main",
|
|
205
|
+
{
|
|
206
|
+
className: "admin-main",
|
|
207
|
+
style: !collapsed ? { marginLeft: sidebarWidth, width: `calc(100vw - ${sidebarWidth}px)` } : void 0,
|
|
208
|
+
children
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ jsx(Toaster, { position: "top-center", richColors: true, closeButton: true, duration: 3e3 })
|
|
212
|
+
] });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
AdminShell
|
|
217
|
+
};
|
|
218
|
+
//# sourceMappingURL=chunk-FEPGJ7WN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/AdminShell.tsx"],"sourcesContent":["\"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"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/utils/image-variant-utils.ts
|
|
2
|
+
var IMAGE_VARIANT_SIZES = {
|
|
3
|
+
lg: 1920,
|
|
4
|
+
md: 960,
|
|
5
|
+
sm: 480,
|
|
6
|
+
thumb: 240
|
|
7
|
+
};
|
|
8
|
+
function getVariantUrl(url, size = "thumb") {
|
|
9
|
+
return url.replace(/\.[^.]+$/, `-${size}.webp`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
exports.IMAGE_VARIANT_SIZES = IMAGE_VARIANT_SIZES; exports.getVariantUrl = getVariantUrl;
|
|
16
|
+
//# sourceMappingURL=chunk-HXVWBFCN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-HXVWBFCN.js","../src/utils/image-variant-utils.ts"],"names":[],"mappings":"AAAA;ACAO,IAAM,oBAAA,EAAsB;AAAA,EACjC,EAAA,EAAI,IAAA;AAAA,EACJ,EAAA,EAAI,GAAA;AAAA,EACJ,EAAA,EAAI,GAAA;AAAA,EACJ,KAAA,EAAO;AACT,CAAA;AAQO,SAAS,aAAA,CAAc,GAAA,EAAa,KAAA,EAAoB,OAAA,EAAiB;AAC9E,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,CAAA,CAAA,EAAI,IAAI,CAAA,KAAA,CAAO,CAAA;AAChD;ADLA;AACA;AACE;AACA;AACF,yFAAC","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-HXVWBFCN.js","sourcesContent":[null,"export const IMAGE_VARIANT_SIZES = {\n lg: 1920,\n md: 960,\n sm: 480,\n thumb: 240,\n} as const;\n\nexport type VariantSize = keyof typeof IMAGE_VARIANT_SIZES;\n\n/**\n * 이미지 URL에서 특정 variant URL을 생성합니다.\n * 예: (\"https://cdn.example.com/path/abc.jpg\", \"thumb\") → \"https://cdn.example.com/path/abc-thumb.webp\"\n */\nexport function getVariantUrl(url: string, size: VariantSize = 'thumb'): string {\n return url.replace(/\\.[^.]+$/, `-${size}.webp`);\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/hooks/useScrollReveal.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
function useScrollReveal() {
|
|
4
|
+
const ref = useRef(null);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const el = ref.current;
|
|
7
|
+
if (!el) return;
|
|
8
|
+
el.classList.add("scroll-reveal");
|
|
9
|
+
const observer = new IntersectionObserver(
|
|
10
|
+
(entries) => {
|
|
11
|
+
entries.forEach((entry) => {
|
|
12
|
+
if (entry.isIntersecting) {
|
|
13
|
+
entry.target.classList.add("scroll-reveal--visible");
|
|
14
|
+
observer.unobserve(entry.target);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
|
|
19
|
+
);
|
|
20
|
+
observer.observe(el);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, []);
|
|
23
|
+
return ref;
|
|
24
|
+
}
|
|
25
|
+
function useScrollRevealAll(selector) {
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const elements = document.querySelectorAll(selector);
|
|
28
|
+
elements.forEach((el) => {
|
|
29
|
+
el.classList.add("scroll-reveal");
|
|
30
|
+
});
|
|
31
|
+
const observer = new IntersectionObserver(
|
|
32
|
+
(entries) => {
|
|
33
|
+
entries.forEach((entry) => {
|
|
34
|
+
if (entry.isIntersecting) {
|
|
35
|
+
entry.target.classList.add("scroll-reveal--visible");
|
|
36
|
+
observer.unobserve(entry.target);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
|
|
41
|
+
);
|
|
42
|
+
elements.forEach((el) => observer.observe(el));
|
|
43
|
+
return () => observer.disconnect();
|
|
44
|
+
}, [selector]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
useScrollReveal,
|
|
49
|
+
useScrollRevealAll
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=chunk-I5YMTJV6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useScrollReveal.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef } from 'react';\n\nexport function useScrollReveal<T extends HTMLElement>() {\n const ref = useRef<T>(null);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n el.classList.add('scroll-reveal');\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add('scroll-reveal--visible');\n observer.unobserve(entry.target);\n }\n });\n },\n { threshold: 0.1, rootMargin: '0px 0px -50px 0px' },\n );\n\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n return ref;\n}\n\nexport function useScrollRevealAll(selector: string) {\n useEffect(() => {\n const elements = document.querySelectorAll(selector);\n\n elements.forEach((el) => {\n el.classList.add('scroll-reveal');\n });\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add('scroll-reveal--visible');\n observer.unobserve(entry.target);\n }\n });\n },\n { threshold: 0.1, rootMargin: '0px 0px -50px 0px' },\n );\n\n elements.forEach((el) => observer.observe(el));\n return () => observer.disconnect();\n }, [selector]);\n}\n"],"mappings":";AAEA,SAAS,WAAW,cAAc;AAE3B,SAAS,kBAAyC;AACvD,QAAM,MAAM,OAAU,IAAI;AAE1B,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AAET,OAAG,UAAU,IAAI,eAAe;AAEhC,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,gBAAQ,QAAQ,CAAC,UAAU;AACzB,cAAI,MAAM,gBAAgB;AACxB,kBAAM,OAAO,UAAU,IAAI,wBAAwB;AACnD,qBAAS,UAAU,MAAM,MAAM;AAAA,UACjC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA,EAAE,WAAW,KAAK,YAAY,oBAAoB;AAAA,IACpD;AAEA,aAAS,QAAQ,EAAE;AACnB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;AAEO,SAAS,mBAAmB,UAAkB;AACnD,YAAU,MAAM;AACd,UAAM,WAAW,SAAS,iBAAiB,QAAQ;AAEnD,aAAS,QAAQ,CAAC,OAAO;AACvB,SAAG,UAAU,IAAI,eAAe;AAAA,IAClC,CAAC;AAED,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,gBAAQ,QAAQ,CAAC,UAAU;AACzB,cAAI,MAAM,gBAAgB;AACxB,kBAAM,OAAO,UAAU,IAAI,wBAAwB;AACnD,qBAAS,UAAU,MAAM,MAAM;AAAA,UACjC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA,EAAE,WAAW,KAAK,YAAY,oBAAoB;AAAA,IACpD;AAEA,aAAS,QAAQ,CAAC,OAAO,SAAS,QAAQ,EAAE,CAAC;AAC7C,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,QAAQ,CAAC;AACf;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/types/common.ts
|
|
2
|
+
function buildPaginatedResult(items, total, page, limit) {
|
|
3
|
+
const totalPages = Math.ceil(total / limit);
|
|
4
|
+
return {
|
|
5
|
+
items,
|
|
6
|
+
pagination: { page, pageSize: limit, total, totalPages, hasMore: page < totalPages }
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
buildPaginatedResult
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=chunk-IMNWW7Q5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types/common.ts"],"sourcesContent":["export interface PaginatedResult<T> {\n items: T[];\n pagination: {\n page: number;\n pageSize: number;\n total: number;\n totalPages: number;\n hasMore: boolean;\n };\n}\n\nexport type SortOrder = 'asc' | 'desc';\n\nexport function buildPaginatedResult<T>(\n items: T[],\n total: number,\n page: number,\n limit: number,\n): PaginatedResult<T> {\n const totalPages = Math.ceil(total / limit);\n return {\n items,\n pagination: { page, pageSize: limit, total, totalPages, hasMore: page < totalPages },\n };\n}\n"],"mappings":";AAaO,SAAS,qBACd,OACA,OACA,MACA,OACoB;AACpB,QAAM,aAAa,KAAK,KAAK,QAAQ,KAAK;AAC1C,SAAO;AAAA,IACL;AAAA,IACA,YAAY,EAAE,MAAM,UAAU,OAAO,OAAO,YAAY,SAAS,OAAO,WAAW;AAAA,EACrF;AACF;","names":[]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/utils/image-resize.ts
|
|
2
|
+
var MAX_DIMENSION = 1920;
|
|
3
|
+
var TARGET_SIZE = 4 * 1024 * 1024;
|
|
4
|
+
var SKIP_THRESHOLD = 500 * 1024;
|
|
5
|
+
var GIF_MAX_SIZE = 5 * 1024 * 1024;
|
|
6
|
+
var ABSOLUTE_MAX_SIZE = 50 * 1024 * 1024;
|
|
7
|
+
var SKIP_RESIZE_TYPES = ["image/gif"];
|
|
8
|
+
async function resizeImageIfNeeded(file) {
|
|
9
|
+
const originalSize = file.size;
|
|
10
|
+
if (SKIP_RESIZE_TYPES.includes(file.type)) {
|
|
11
|
+
return { file, wasResized: false, originalSize, newSize: originalSize };
|
|
12
|
+
}
|
|
13
|
+
const img = await loadImage(file);
|
|
14
|
+
const { width, height } = img;
|
|
15
|
+
const longestEdge = Math.max(width, height);
|
|
16
|
+
if (file.size <= SKIP_THRESHOLD && longestEdge <= MAX_DIMENSION) {
|
|
17
|
+
return { file, wasResized: false, originalSize, newSize: originalSize };
|
|
18
|
+
}
|
|
19
|
+
const outputMime = detectOutputMime(file.type);
|
|
20
|
+
let targetW = width;
|
|
21
|
+
let targetH = height;
|
|
22
|
+
if (longestEdge > MAX_DIMENSION) {
|
|
23
|
+
const scale = MAX_DIMENSION / longestEdge;
|
|
24
|
+
targetW = Math.round(width * scale);
|
|
25
|
+
targetH = Math.round(height * scale);
|
|
26
|
+
}
|
|
27
|
+
const qualitySteps = [0.85, 0.8, 0.75, 0.65, 0.55, 0.5];
|
|
28
|
+
for (const quality of qualitySteps) {
|
|
29
|
+
const blob = await canvasToBlob(img, targetW, targetH, outputMime, quality);
|
|
30
|
+
if (blob.size <= TARGET_SIZE) {
|
|
31
|
+
return toResult(blob, file.name, outputMime, originalSize);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const scaleSteps = [0.8, 0.65, 0.5, 0.4];
|
|
35
|
+
for (const scale of scaleSteps) {
|
|
36
|
+
const newW = Math.round(targetW * scale);
|
|
37
|
+
const newH = Math.round(targetH * scale);
|
|
38
|
+
const blob = await canvasToBlob(img, newW, newH, outputMime, 0.75);
|
|
39
|
+
if (blob.size <= TARGET_SIZE) {
|
|
40
|
+
return toResult(blob, file.name, outputMime, originalSize);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const finalW = Math.round(targetW * 0.3);
|
|
44
|
+
const finalH = Math.round(targetH * 0.3);
|
|
45
|
+
const finalBlob = await canvasToBlob(img, finalW, finalH, outputMime, 0.5);
|
|
46
|
+
return toResult(finalBlob, file.name, outputMime, originalSize);
|
|
47
|
+
}
|
|
48
|
+
function validateImageSize(file) {
|
|
49
|
+
if (file.size > ABSOLUTE_MAX_SIZE) {
|
|
50
|
+
return `\uD30C\uC77C \uD06C\uAE30\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4. (${(file.size / 1024 / 1024).toFixed(0)}MB, \uCD5C\uB300 50MB)`;
|
|
51
|
+
}
|
|
52
|
+
if (SKIP_RESIZE_TYPES.includes(file.type) && file.size > GIF_MAX_SIZE) {
|
|
53
|
+
return `GIF \uD30C\uC77C\uC740 5MB \uC774\uD558\uB9CC \uC5C5\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. (${(file.size / 1024 / 1024).toFixed(1)}MB)`;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function detectOutputMime(inputType) {
|
|
58
|
+
if (typeof document === "undefined") return "image/jpeg";
|
|
59
|
+
const supportsWebP = document.createElement("canvas").toDataURL("image/webp").startsWith("data:image/webp");
|
|
60
|
+
if (supportsWebP) return "image/webp";
|
|
61
|
+
return inputType === "image/png" ? "image/jpeg" : inputType || "image/jpeg";
|
|
62
|
+
}
|
|
63
|
+
function loadImage(file) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const img = new Image();
|
|
66
|
+
const url = URL.createObjectURL(file);
|
|
67
|
+
img.onload = () => {
|
|
68
|
+
URL.revokeObjectURL(url);
|
|
69
|
+
resolve(img);
|
|
70
|
+
};
|
|
71
|
+
img.onerror = () => {
|
|
72
|
+
URL.revokeObjectURL(url);
|
|
73
|
+
reject(new Error("\uC774\uBBF8\uC9C0 \uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
|
|
74
|
+
};
|
|
75
|
+
img.src = url;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function canvasToBlob(img, width, height, mime, quality) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const canvas = document.createElement("canvas");
|
|
81
|
+
canvas.width = width;
|
|
82
|
+
canvas.height = height;
|
|
83
|
+
const ctx = canvas.getContext("2d");
|
|
84
|
+
if (!ctx) {
|
|
85
|
+
reject(new Error("\uC774\uBBF8\uC9C0 \uCC98\uB9AC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (mime !== "image/png") {
|
|
89
|
+
ctx.fillStyle = "#FFFFFF";
|
|
90
|
+
ctx.fillRect(0, 0, width, height);
|
|
91
|
+
}
|
|
92
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
93
|
+
canvas.toBlob(
|
|
94
|
+
(blob) => {
|
|
95
|
+
if (blob) resolve(blob);
|
|
96
|
+
else reject(new Error("\uC774\uBBF8\uC9C0 \uBCC0\uD658\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."));
|
|
97
|
+
},
|
|
98
|
+
mime,
|
|
99
|
+
quality
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function toResult(blob, originalName, mime, originalSize) {
|
|
104
|
+
const ext = mime === "image/jpeg" ? ".jpg" : mime === "image/webp" ? ".webp" : ".png";
|
|
105
|
+
const baseName = originalName.replace(/\.[^.]+$/, "");
|
|
106
|
+
const file = new File([blob], `${baseName}${ext}`, { type: mime });
|
|
107
|
+
return { file, wasResized: true, originalSize, newSize: blob.size };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
resizeImageIfNeeded,
|
|
112
|
+
validateImageSize
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=chunk-J5N76ASB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/image-resize.ts"],"sourcesContent":["const MAX_DIMENSION = 1920; // px\nconst TARGET_SIZE = 4 * 1024 * 1024; // 4MB\nconst SKIP_THRESHOLD = 500 * 1024; // 500KB\nconst GIF_MAX_SIZE = 5 * 1024 * 1024; // 5MB\nconst ABSOLUTE_MAX_SIZE = 50 * 1024 * 1024; // 50MB\n\nconst SKIP_RESIZE_TYPES = [\"image/gif\"];\n\nexport interface ResizeResult {\n file: File;\n wasResized: boolean;\n originalSize: number;\n newSize: number;\n}\n\nexport async function resizeImageIfNeeded(\n file: File\n): Promise<ResizeResult> {\n const originalSize = file.size;\n\n if (SKIP_RESIZE_TYPES.includes(file.type)) {\n return { file, wasResized: false, originalSize, newSize: originalSize };\n }\n\n const img = await loadImage(file);\n const { width, height } = img;\n const longestEdge = Math.max(width, height);\n\n if (file.size <= SKIP_THRESHOLD && longestEdge <= MAX_DIMENSION) {\n return { file, wasResized: false, originalSize, newSize: originalSize };\n }\n\n const outputMime = detectOutputMime(file.type);\n\n let targetW = width;\n let targetH = height;\n if (longestEdge > MAX_DIMENSION) {\n const scale = MAX_DIMENSION / longestEdge;\n targetW = Math.round(width * scale);\n targetH = Math.round(height * scale);\n }\n\n const qualitySteps = [0.85, 0.8, 0.75, 0.65, 0.55, 0.5];\n for (const quality of qualitySteps) {\n const blob = await canvasToBlob(img, targetW, targetH, outputMime, quality);\n if (blob.size <= TARGET_SIZE) {\n return toResult(blob, file.name, outputMime, originalSize);\n }\n }\n\n const scaleSteps = [0.8, 0.65, 0.5, 0.4];\n for (const scale of scaleSteps) {\n const newW = Math.round(targetW * scale);\n const newH = Math.round(targetH * scale);\n const blob = await canvasToBlob(img, newW, newH, outputMime, 0.75);\n if (blob.size <= TARGET_SIZE) {\n return toResult(blob, file.name, outputMime, originalSize);\n }\n }\n\n const finalW = Math.round(targetW * 0.3);\n const finalH = Math.round(targetH * 0.3);\n const finalBlob = await canvasToBlob(img, finalW, finalH, outputMime, 0.5);\n return toResult(finalBlob, file.name, outputMime, originalSize);\n}\n\nexport function validateImageSize(file: File): string | null {\n if (file.size > ABSOLUTE_MAX_SIZE) {\n return `파일 크기가 너무 큽니다. (${(file.size / 1024 / 1024).toFixed(0)}MB, 최대 50MB)`;\n }\n if (SKIP_RESIZE_TYPES.includes(file.type) && file.size > GIF_MAX_SIZE) {\n return `GIF 파일은 5MB 이하만 업로드할 수 있습니다. (${(file.size / 1024 / 1024).toFixed(1)}MB)`;\n }\n return null;\n}\n\nfunction detectOutputMime(inputType: string): string {\n if (typeof document === \"undefined\") return \"image/jpeg\";\n const supportsWebP = document\n .createElement(\"canvas\")\n .toDataURL(\"image/webp\")\n .startsWith(\"data:image/webp\");\n if (supportsWebP) return \"image/webp\";\n return inputType === \"image/png\" ? \"image/jpeg\" : inputType || \"image/jpeg\";\n}\n\nfunction loadImage(file: File): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n const url = URL.createObjectURL(file);\n img.onload = () => {\n URL.revokeObjectURL(url);\n resolve(img);\n };\n img.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(\"이미지 로드에 실패했습니다. 파일이 손상되었을 수 있습니다.\"));\n };\n img.src = url;\n });\n}\n\nfunction canvasToBlob(\n img: HTMLImageElement,\n width: number,\n height: number,\n mime: string,\n quality: number\n): Promise<Blob> {\n return new Promise((resolve, reject) => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n reject(new Error(\"이미지 처리에 실패했습니다.\"));\n return;\n }\n if (mime !== \"image/png\") {\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, width, height);\n }\n ctx.drawImage(img, 0, 0, width, height);\n canvas.toBlob(\n (blob) => {\n if (blob) resolve(blob);\n else reject(new Error(\"이미지 변환에 실패했습니다.\"));\n },\n mime,\n quality\n );\n });\n}\n\nfunction toResult(\n blob: Blob,\n originalName: string,\n mime: string,\n originalSize: number\n): ResizeResult {\n const ext =\n mime === \"image/jpeg\" ? \".jpg\" : mime === \"image/webp\" ? \".webp\" : \".png\";\n const baseName = originalName.replace(/\\.[^.]+$/, \"\");\n const file = new File([blob], `${baseName}${ext}`, { type: mime });\n return { file, wasResized: true, originalSize, newSize: blob.size };\n}\n"],"mappings":";AAAA,IAAM,gBAAgB;AACtB,IAAM,cAAc,IAAI,OAAO;AAC/B,IAAM,iBAAiB,MAAM;AAC7B,IAAM,eAAe,IAAI,OAAO;AAChC,IAAM,oBAAoB,KAAK,OAAO;AAEtC,IAAM,oBAAoB,CAAC,WAAW;AAStC,eAAsB,oBACpB,MACuB;AACvB,QAAM,eAAe,KAAK;AAE1B,MAAI,kBAAkB,SAAS,KAAK,IAAI,GAAG;AACzC,WAAO,EAAE,MAAM,YAAY,OAAO,cAAc,SAAS,aAAa;AAAA,EACxE;AAEA,QAAM,MAAM,MAAM,UAAU,IAAI;AAChC,QAAM,EAAE,OAAO,OAAO,IAAI;AAC1B,QAAM,cAAc,KAAK,IAAI,OAAO,MAAM;AAE1C,MAAI,KAAK,QAAQ,kBAAkB,eAAe,eAAe;AAC/D,WAAO,EAAE,MAAM,YAAY,OAAO,cAAc,SAAS,aAAa;AAAA,EACxE;AAEA,QAAM,aAAa,iBAAiB,KAAK,IAAI;AAE7C,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,cAAc,eAAe;AAC/B,UAAM,QAAQ,gBAAgB;AAC9B,cAAU,KAAK,MAAM,QAAQ,KAAK;AAClC,cAAU,KAAK,MAAM,SAAS,KAAK;AAAA,EACrC;AAEA,QAAM,eAAe,CAAC,MAAM,KAAK,MAAM,MAAM,MAAM,GAAG;AACtD,aAAW,WAAW,cAAc;AAClC,UAAM,OAAO,MAAM,aAAa,KAAK,SAAS,SAAS,YAAY,OAAO;AAC1E,QAAI,KAAK,QAAQ,aAAa;AAC5B,aAAO,SAAS,MAAM,KAAK,MAAM,YAAY,YAAY;AAAA,IAC3D;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,KAAK,MAAM,KAAK,GAAG;AACvC,aAAW,SAAS,YAAY;AAC9B,UAAM,OAAO,KAAK,MAAM,UAAU,KAAK;AACvC,UAAM,OAAO,KAAK,MAAM,UAAU,KAAK;AACvC,UAAM,OAAO,MAAM,aAAa,KAAK,MAAM,MAAM,YAAY,IAAI;AACjE,QAAI,KAAK,QAAQ,aAAa;AAC5B,aAAO,SAAS,MAAM,KAAK,MAAM,YAAY,YAAY;AAAA,IAC3D;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,MAAM,UAAU,GAAG;AACvC,QAAM,SAAS,KAAK,MAAM,UAAU,GAAG;AACvC,QAAM,YAAY,MAAM,aAAa,KAAK,QAAQ,QAAQ,YAAY,GAAG;AACzE,SAAO,SAAS,WAAW,KAAK,MAAM,YAAY,YAAY;AAChE;AAEO,SAAS,kBAAkB,MAA2B;AAC3D,MAAI,KAAK,OAAO,mBAAmB;AACjC,WAAO,sEAAoB,KAAK,OAAO,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EAChE;AACA,MAAI,kBAAkB,SAAS,KAAK,IAAI,KAAK,KAAK,OAAO,cAAc;AACrE,WAAO,6GAAkC,KAAK,OAAO,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EAC9E;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,WAA2B;AACnD,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,eAAe,SAClB,cAAc,QAAQ,EACtB,UAAU,YAAY,EACtB,WAAW,iBAAiB;AAC/B,MAAI,aAAc,QAAO;AACzB,SAAO,cAAc,cAAc,eAAe,aAAa;AACjE;AAEA,SAAS,UAAU,MAAuC;AACxD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,MAAM;AACtB,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,QAAI,SAAS,MAAM;AACjB,UAAI,gBAAgB,GAAG;AACvB,cAAQ,GAAG;AAAA,IACb;AACA,QAAI,UAAU,MAAM;AAClB,UAAI,gBAAgB,GAAG;AACvB,aAAO,IAAI,MAAM,gKAAmC,CAAC;AAAA,IACvD;AACA,QAAI,MAAM;AAAA,EACZ,CAAC;AACH;AAEA,SAAS,aACP,KACA,OACA,QACA,MACA,SACe;AACf,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,UAAM,MAAM,OAAO,WAAW,IAAI;AAClC,QAAI,CAAC,KAAK;AACR,aAAO,IAAI,MAAM,6EAAiB,CAAC;AACnC;AAAA,IACF;AACA,QAAI,SAAS,aAAa;AACxB,UAAI,YAAY;AAChB,UAAI,SAAS,GAAG,GAAG,OAAO,MAAM;AAAA,IAClC;AACA,QAAI,UAAU,KAAK,GAAG,GAAG,OAAO,MAAM;AACtC,WAAO;AAAA,MACL,CAAC,SAAS;AACR,YAAI,KAAM,SAAQ,IAAI;AAAA,YACjB,QAAO,IAAI,MAAM,6EAAiB,CAAC;AAAA,MAC1C;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,SACP,MACA,cACA,MACA,cACc;AACd,QAAM,MACJ,SAAS,eAAe,SAAS,SAAS,eAAe,UAAU;AACrE,QAAM,WAAW,aAAa,QAAQ,YAAY,EAAE;AACpD,QAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,GAAG,QAAQ,GAAG,GAAG,IAAI,EAAE,MAAM,KAAK,CAAC;AACjE,SAAO,EAAE,MAAM,YAAY,MAAM,cAAc,SAAS,KAAK,KAAK;AACpE;","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/utils/cn.ts
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
4
|
+
function cn(...inputs) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/utils/api-response.ts
|
|
9
|
+
import { NextResponse } from "next/server";
|
|
10
|
+
var NextApiResponse = class {
|
|
11
|
+
static success(data, status = 200) {
|
|
12
|
+
return NextResponse.json({ success: true, data }, { status });
|
|
13
|
+
}
|
|
14
|
+
static paginated(items, page, pageSize, total, dataKey = "items") {
|
|
15
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
16
|
+
return this.success({
|
|
17
|
+
[dataKey]: items,
|
|
18
|
+
pagination: { page, pageSize, total, totalPages, hasMore: page < totalPages }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
static created(data) {
|
|
22
|
+
return this.success(data, 201);
|
|
23
|
+
}
|
|
24
|
+
static noContent() {
|
|
25
|
+
return new NextResponse(null, { status: 204 });
|
|
26
|
+
}
|
|
27
|
+
static error(message, status = 400, code) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ success: false, error: { message, ...code && { code } } },
|
|
30
|
+
{ status }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
static notFound(message = "Not Found") {
|
|
34
|
+
return this.error(message, 404, "NOT_FOUND");
|
|
35
|
+
}
|
|
36
|
+
static unauthorized(message = "Unauthorized") {
|
|
37
|
+
return this.error(message, 401, "UNAUTHORIZED");
|
|
38
|
+
}
|
|
39
|
+
static forbidden(message = "Forbidden") {
|
|
40
|
+
return this.error(message, 403, "FORBIDDEN");
|
|
41
|
+
}
|
|
42
|
+
static serverError(message = "Internal Server Error") {
|
|
43
|
+
return this.error(message, 500, "INTERNAL_SERVER_ERROR");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
cn,
|
|
49
|
+
NextApiResponse
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=chunk-JBV26CUG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/cn.ts","../src/utils/api-response.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { NextResponse } from 'next/server';\n\nexport class NextApiResponse {\n static success<T>(data: T, status: number = 200) {\n return NextResponse.json({ success: true, data }, { status });\n }\n\n static paginated<T>(\n items: T[],\n page: number,\n pageSize: number,\n total: number,\n dataKey: string = 'items',\n ) {\n const totalPages = Math.ceil(total / pageSize);\n return this.success({\n [dataKey]: items,\n pagination: { page, pageSize, total, totalPages, hasMore: page < totalPages },\n });\n }\n\n static created<T>(data: T) {\n return this.success(data, 201);\n }\n\n static noContent() {\n return new NextResponse(null, { status: 204 });\n }\n\n static error(message: string, status: number = 400, code?: string) {\n return NextResponse.json(\n { success: false, error: { message, ...(code && { code }) } },\n { status },\n );\n }\n\n static notFound(message: string = 'Not Found') {\n return this.error(message, 404, 'NOT_FOUND');\n }\n\n static unauthorized(message: string = 'Unauthorized') {\n return this.error(message, 401, 'UNAUTHORIZED');\n }\n\n static forbidden(message: string = 'Forbidden') {\n return this.error(message, 403, 'FORBIDDEN');\n }\n\n static serverError(message: string = 'Internal Server Error') {\n return this.error(message, 500, 'INTERNAL_SERVER_ERROR');\n }\n}\n"],"mappings":";AAAA,SAA0B,YAAY;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ACLA,SAAS,oBAAoB;AAEtB,IAAM,kBAAN,MAAsB;AAAA,EAC3B,OAAO,QAAW,MAAS,SAAiB,KAAK;AAC/C,WAAO,aAAa,KAAK,EAAE,SAAS,MAAM,KAAK,GAAG,EAAE,OAAO,CAAC;AAAA,EAC9D;AAAA,EAEA,OAAO,UACL,OACA,MACA,UACA,OACA,UAAkB,SAClB;AACA,UAAM,aAAa,KAAK,KAAK,QAAQ,QAAQ;AAC7C,WAAO,KAAK,QAAQ;AAAA,MAClB,CAAC,OAAO,GAAG;AAAA,MACX,YAAY,EAAE,MAAM,UAAU,OAAO,YAAY,SAAS,OAAO,WAAW;AAAA,IAC9E,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,QAAW,MAAS;AACzB,WAAO,KAAK,QAAQ,MAAM,GAAG;AAAA,EAC/B;AAAA,EAEA,OAAO,YAAY;AACjB,WAAO,IAAI,aAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/C;AAAA,EAEA,OAAO,MAAM,SAAiB,SAAiB,KAAK,MAAe;AACjE,WAAO,aAAa;AAAA,MAClB,EAAE,SAAS,OAAO,OAAO,EAAE,SAAS,GAAI,QAAQ,EAAE,KAAK,EAAG,EAAE;AAAA,MAC5D,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEA,OAAO,SAAS,UAAkB,aAAa;AAC7C,WAAO,KAAK,MAAM,SAAS,KAAK,WAAW;AAAA,EAC7C;AAAA,EAEA,OAAO,aAAa,UAAkB,gBAAgB;AACpD,WAAO,KAAK,MAAM,SAAS,KAAK,cAAc;AAAA,EAChD;AAAA,EAEA,OAAO,UAAU,UAAkB,aAAa;AAC9C,WAAO,KAAK,MAAM,SAAS,KAAK,WAAW;AAAA,EAC7C;AAAA,EAEA,OAAO,YAAY,UAAkB,yBAAyB;AAC5D,WAAO,KAAK,MAAM,SAAS,KAAK,uBAAuB;AAAA,EACzD;AACF;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkEG6NVPP2js = require('./chunk-EG6NVPP2.js');
|
|
5
|
+
|
|
6
|
+
// src/infrastructure/middleware/wrappers.ts
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
var _wrappers = require('@withwiz/toolkit/next/middleware/wrappers');
|
|
13
|
+
var _ratelimit = require('@withwiz/toolkit/next/middleware/rate-limit');
|
|
14
|
+
function createInMemoryLimiter(limit, windowMs) {
|
|
15
|
+
const store = /* @__PURE__ */ new Map();
|
|
16
|
+
return {
|
|
17
|
+
check: async (identifier) => {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const entry = store.get(identifier);
|
|
20
|
+
if (!entry || now > entry.resetAt) {
|
|
21
|
+
store.set(identifier, { count: 1, resetAt: now + windowMs });
|
|
22
|
+
return { success: true, remaining: limit - 1, resetIn: windowMs };
|
|
23
|
+
}
|
|
24
|
+
entry.count++;
|
|
25
|
+
const remaining = Math.max(0, limit - entry.count);
|
|
26
|
+
const resetIn = entry.resetAt - now;
|
|
27
|
+
return { success: entry.count <= limit, remaining, resetIn };
|
|
28
|
+
},
|
|
29
|
+
config: { limit }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var extractClientIdentity = (headers) => _chunkEG6NVPP2js.resolveClientIdentity.call(void 0, headers);
|
|
33
|
+
_ratelimit.setRateLimitAdapter.call(void 0, {
|
|
34
|
+
rateLimiters: {
|
|
35
|
+
api: createInMemoryLimiter(120, 6e4),
|
|
36
|
+
auth: createInMemoryLimiter(10, 6e4),
|
|
37
|
+
admin: createInMemoryLimiter(200, 6e4)
|
|
38
|
+
},
|
|
39
|
+
extractClientIp: extractClientIdentity,
|
|
40
|
+
isEnabled: async () => _chunkEG6NVPP2js.resolveRateLimitEnabled.call(void 0, )
|
|
41
|
+
});
|
|
42
|
+
var withPublicApi = _wrappers.withPublicApi;
|
|
43
|
+
var withAdminApi = _wrappers.withAdminApi;
|
|
44
|
+
var withAuthApi = _wrappers.withAuthApi;
|
|
45
|
+
var withCustomApi = _wrappers.withCustomApi;
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
exports.withPublicApi = withPublicApi; exports.withAdminApi = withAdminApi; exports.withAuthApi = withAuthApi; exports.withCustomApi = withCustomApi;
|
|
53
|
+
//# sourceMappingURL=chunk-JMWXVXK5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-JMWXVXK5.js","../src/infrastructure/middleware/wrappers.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACF,sDAA4B;AAC5B;AACA;ACJA;AACE;AACA;AACA;AACA;AAAiB,qEACZ;AACP,wEAAoC;AAGpC,SAAS,qBAAA,CAAsB,KAAA,EAAe,QAAA,EAAkB;AAC9D,EAAA,MAAM,MAAA,kBAAQ,IAAI,GAAA,CAAgD,CAAA;AAClE,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,MAAA,CAAO,UAAA,EAAA,GAAuB;AACnC,MAAA,MAAM,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,CAAA;AACrB,MAAA,MAAM,MAAA,EAAQ,KAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AAClC,MAAA,GAAA,CAAI,CAAC,MAAA,GAAS,IAAA,EAAM,KAAA,CAAM,OAAA,EAAS;AACjC,QAAA,KAAA,CAAM,GAAA,CAAI,UAAA,EAAY,EAAE,KAAA,EAAO,CAAA,EAAG,OAAA,EAAS,IAAA,EAAM,SAAS,CAAC,CAAA;AAC3D,QAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,CAAA,EAAG,OAAA,EAAS,SAAS,CAAA;AAAA,MAClE;AACA,MAAA,KAAA,CAAM,KAAA,EAAA;AACN,MAAA,MAAM,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,KAAK,CAAA;AACjD,MAAA,MAAM,QAAA,EAAU,KAAA,CAAM,QAAA,EAAU,GAAA;AAChC,MAAA,OAAO,EAAE,OAAA,EAAS,KAAA,CAAM,MAAA,GAAS,KAAA,EAAO,SAAA,EAAW,QAAQ,CAAA;AAAA,IAC7D,CAAA;AAAA,IACA,MAAA,EAAQ,EAAE,MAAM;AAAA,EAClB,CAAA;AACF;AAeA,IAAM,sBAAA,EAAwB,CAAC,OAAA,EAAA,GAC7B,oDAAA,OAA6B,CAAA;AAE/B,4CAAA;AAAoB,EAClB,YAAA,EAAc;AAAA,IACZ,GAAA,EAAK,qBAAA,CAAsB,GAAA,EAAK,GAAM,CAAA;AAAA,IACtC,IAAA,EAAM,qBAAA,CAAsB,EAAA,EAAI,GAAM,CAAA;AAAA,IACtC,KAAA,EAAO,qBAAA,CAAsB,GAAA,EAAK,GAAM;AAAA,EAC1C,CAAA;AAAA,EACA,eAAA,EAAiB,qBAAA;AAAA,EACjB,SAAA,EAAW,MAAA,CAAA,EAAA,GAAY,sDAAA;AACzB,CAAC,CAAA;AAIM,IAAM,cAAA,EAAgB,uBAAA;AACtB,IAAM,aAAA,EAAe,sBAAA;AACrB,IAAM,YAAA,EAAc,qBAAA;AACpB,IAAM,cAAA,EAAgB,uBAAA;ADf7B;AACA;AACE;AACA;AACA;AACA;AACF,qJAAC","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-cms-kit/dist/chunk-JMWXVXK5.js","sourcesContent":[null,"import type { NextRequest } from 'next/server';\nimport {\n withPublicApi as _withPublicApi,\n withAdminApi as _withAdminApi,\n withAuthApi as _withAuthApi,\n withCustomApi as _withCustomApi,\n} from '@withwiz/toolkit/next/middleware/wrappers';\nimport { setRateLimitAdapter } from '@withwiz/toolkit/next/middleware/rate-limit';\nimport { resolveClientIdentity, resolveRateLimitEnabled } from '../../config';\n\nfunction createInMemoryLimiter(limit: number, windowMs: number) {\n const store = new Map<string, { count: number; resetAt: number }>();\n return {\n check: async (identifier: string) => {\n const now = Date.now();\n const entry = store.get(identifier);\n if (!entry || now > entry.resetAt) {\n store.set(identifier, { count: 1, resetAt: now + windowMs });\n return { success: true, remaining: limit - 1, resetIn: windowMs };\n }\n entry.count++;\n const remaining = Math.max(0, limit - entry.count);\n const resetIn = entry.resetAt - now;\n return { success: entry.count <= limit, remaining, resetIn };\n },\n config: { limit },\n };\n}\n\n/**\n * rate-limit client-identity 추출 (spec.md §4.6 / Sprint 1 S4).\n *\n * 더 이상 spoofable 한 `x-forwarded-for` 의 첫 값을 무조건 신뢰하지 않고\n * `127.0.0.1` 매직 fallback 도 쓰지 않는다. 식별자 추출은 §5 config\n * boundary 의 `resolveClientIdentity` 로 위임되며, consumer 가 자신의 proxy\n * topology / trusted hop 수를 아는 `rateLimit.identityExtractor` 를 주입하면\n * 그것이 사용된다. 미설정 시 안전 기본값(헤더만 바꿔 회전 불가)을 쓴다.\n *\n * NOTE: import-time `setRateLimitAdapter(...)` 무조건 호출 자체의 제거는\n * §4.3 / Sprint 2 범위다. Sprint 1 은 §4.6 보안 결함(identity 추출)만 고치고\n * 그것을 overridable 하게 만든다 (in-memory limiter 기본값은 유지).\n */\nconst extractClientIdentity = (headers: Headers): string =>\n resolveClientIdentity(headers);\n\nsetRateLimitAdapter({\n rateLimiters: {\n api: createInMemoryLimiter(120, 60_000),\n auth: createInMemoryLimiter(10, 60_000),\n admin: createInMemoryLimiter(200, 60_000),\n },\n extractClientIp: extractClientIdentity,\n isEnabled: async () => resolveRateLimitEnabled(),\n});\n\ntype NextRouteHandler = (request: NextRequest, context?: any) => Promise<Response>;\n\nexport const withPublicApi = _withPublicApi as unknown as (handler: any) => NextRouteHandler;\nexport const withAdminApi = _withAdminApi as unknown as (handler: any) => NextRouteHandler;\nexport const withAuthApi = _withAuthApi as unknown as (handler: any) => NextRouteHandler;\nexport const withCustomApi = _withCustomApi as unknown as (handler: any, configureChain: any) => NextRouteHandler;\n\nexport type { IApiContext, IUser, TApiHandler } from '@withwiz/toolkit/next/middleware/types';\n"]}
|