2026-02-27 14:25:53 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-03-11 22:06:22 +09:00
|
|
|
import React, { useMemo, useState, useEffect } from "react";
|
2026-02-27 14:25:53 +09:00
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
|
import { Loader2 } from "lucide-react";
|
2026-03-11 22:06:22 +09:00
|
|
|
import { ScreenViewPageWrapper } from "@/app/(main)/screens/[screenId]/page";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
const LoadingFallback = () => (
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-11 22:06:22 +09:00
|
|
|
const d = (loader: () => Promise<any>) =>
|
|
|
|
|
dynamic(loader, { ssr: false, loading: LoadingFallback });
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* /dashboard/[dashboardId] URL을 탭 내에서 직접 렌더링
|
|
|
|
|
* Next.js params Promise 없이 dashboardId를 직접 전달
|
|
|
|
|
*/
|
|
|
|
|
const LazyDashboardViewer = d(() => import("@/components/dashboard/DashboardViewer").then((mod) => ({
|
|
|
|
|
default: mod.DashboardViewer,
|
|
|
|
|
})));
|
|
|
|
|
|
|
|
|
|
function DashboardTabRenderer({ dashboardId }: { dashboardId: string }) {
|
|
|
|
|
const [dashboard, setDashboard] = useState<any>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const load = async () => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
|
|
|
|
const data = await dashboardApi.getDashboard(dashboardId);
|
|
|
|
|
setDashboard({ ...data, elements: data.elements || [] });
|
|
|
|
|
} catch {
|
|
|
|
|
const saved = JSON.parse(localStorage.getItem("savedDashboards") || "[]");
|
|
|
|
|
const found = saved.find((d: any) => d.id === dashboardId);
|
|
|
|
|
if (found) {
|
|
|
|
|
setDashboard(found);
|
|
|
|
|
} else {
|
|
|
|
|
setError("대시보드를 찾을 수 없습니다");
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
load();
|
|
|
|
|
}, [dashboardId]);
|
|
|
|
|
|
|
|
|
|
if (isLoading) return <LoadingFallback />;
|
|
|
|
|
|
|
|
|
|
if (error || !dashboard) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-lg font-semibold text-foreground">{error || "대시보드를 찾을 수 없습니다"}</p>
|
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">대시보드 ID: {dashboardId}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full">
|
|
|
|
|
<LazyDashboardViewer
|
|
|
|
|
elements={dashboard.elements}
|
|
|
|
|
dashboardId={dashboard.id}
|
|
|
|
|
dashboardTitle={dashboard.title}
|
|
|
|
|
backgroundColor={dashboard.settings?.backgroundColor}
|
|
|
|
|
resolution={dashboard.settings?.resolution}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* /screen/[screenCode] URL을 screenId로 변환해서 ScreenViewPageWrapper를 렌더링
|
|
|
|
|
*/
|
|
|
|
|
function ScreenCodeResolver({ screenCode }: { screenCode: string }) {
|
|
|
|
|
const [screenId, setScreenId] = useState<number | null>(null);
|
|
|
|
|
const [error, setError] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const numericId = parseInt(screenCode);
|
|
|
|
|
if (!isNaN(numericId)) {
|
|
|
|
|
setScreenId(numericId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolve = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await apiClient.get("/screen-management/screens", {
|
|
|
|
|
params: { searchTerm: screenCode, size: 50 },
|
|
|
|
|
});
|
|
|
|
|
const items = res.data?.data?.data || res.data?.data || [];
|
|
|
|
|
const arr = Array.isArray(items) ? items : [];
|
|
|
|
|
const exact = arr.find((s: any) => s.screenCode === screenCode || s.screen_code === screenCode);
|
|
|
|
|
const target = exact || arr[0];
|
|
|
|
|
if (target) {
|
|
|
|
|
setScreenId(target.screenId || target.screen_id);
|
|
|
|
|
} else {
|
|
|
|
|
setError(true);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setError(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
resolve();
|
|
|
|
|
}, [screenCode]);
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-lg font-semibold text-foreground">화면을 찾을 수 없습니다</p>
|
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
|
|
|
화면 코드: {screenCode}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (screenId === null) {
|
|
|
|
|
return <LoadingFallback />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ScreenViewPageWrapper screenIdProp={screenId} />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 14:25:53 +09:00
|
|
|
/**
|
|
|
|
|
* 관리자 페이지를 URL 기반으로 동적 로딩하는 레지스트리.
|
2026-03-11 22:06:22 +09:00
|
|
|
* 레지스트리에 없는 URL은 자동으로 파일 경로 기반 동적 임포트를 시도한다.
|
2026-02-27 14:25:53 +09:00
|
|
|
*/
|
|
|
|
|
const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
|
|
|
|
// 관리자 메인
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin": d(() => import("@/app/(main)/admin/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 메뉴 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/menu": d(() => import("@/app/(main)/admin/menu/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 사용자 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/userMng/userMngList": d(() => import("@/app/(main)/admin/userMng/userMngList/page")),
|
|
|
|
|
"/admin/userMng/rolesList": d(() => import("@/app/(main)/admin/userMng/rolesList/page")),
|
|
|
|
|
"/admin/userMng/userAuthList": d(() => import("@/app/(main)/admin/userMng/userAuthList/page")),
|
|
|
|
|
"/admin/userMng/companyList": d(() => import("@/app/(main)/admin/userMng/companyList/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 화면 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/screenMng/screenMngList": d(() => import("@/app/(main)/admin/screenMng/screenMngList/page")),
|
|
|
|
|
"/admin/screenMng/popScreenMngList": d(() => import("@/app/(main)/admin/screenMng/popScreenMngList/page")),
|
|
|
|
|
"/admin/screenMng/dashboardList": d(() => import("@/app/(main)/admin/screenMng/dashboardList/page")),
|
|
|
|
|
"/admin/screenMng/reportList": d(() => import("@/app/(main)/admin/screenMng/reportList/page")),
|
|
|
|
|
"/admin/screenMng/barcodeList": d(() => import("@/app/(main)/admin/screenMng/barcodeList/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 시스템 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/systemMng/commonCodeList": d(() => import("@/app/(main)/admin/systemMng/commonCodeList/page")),
|
|
|
|
|
"/admin/systemMng/tableMngList": d(() => import("@/app/(main)/admin/systemMng/tableMngList/page")),
|
|
|
|
|
"/admin/systemMng/i18nList": d(() => import("@/app/(main)/admin/systemMng/i18nList/page")),
|
|
|
|
|
"/admin/systemMng/collection-managementList": d(() => import("@/app/(main)/admin/systemMng/collection-managementList/page")),
|
|
|
|
|
"/admin/systemMng/dataflow": d(() => import("@/app/(main)/admin/systemMng/dataflow/page")),
|
|
|
|
|
"/admin/systemMng/dataflow/node-editorList": d(() => import("@/app/(main)/admin/systemMng/dataflow/node-editorList/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 자동화 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/automaticMng/flowMgmtList": d(() => import("@/app/(main)/admin/automaticMng/flowMgmtList/page")),
|
|
|
|
|
"/admin/automaticMng/batchmngList": d(() => import("@/app/(main)/admin/automaticMng/batchmngList/page")),
|
|
|
|
|
"/admin/automaticMng/exconList": d(() => import("@/app/(main)/admin/automaticMng/exconList/page")),
|
|
|
|
|
"/admin/automaticMng/exCallConfList": d(() => import("@/app/(main)/admin/automaticMng/exCallConfList/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 메일
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/automaticMng/mail/send": d(() => import("@/app/(main)/admin/automaticMng/mail/send/page")),
|
|
|
|
|
"/admin/automaticMng/mail/receive": d(() => import("@/app/(main)/admin/automaticMng/mail/receive/page")),
|
|
|
|
|
"/admin/automaticMng/mail/sent": d(() => import("@/app/(main)/admin/automaticMng/mail/sent/page")),
|
|
|
|
|
"/admin/automaticMng/mail/drafts": d(() => import("@/app/(main)/admin/automaticMng/mail/drafts/page")),
|
|
|
|
|
"/admin/automaticMng/mail/trash": d(() => import("@/app/(main)/admin/automaticMng/mail/trash/page")),
|
|
|
|
|
"/admin/automaticMng/mail/accounts": d(() => import("@/app/(main)/admin/automaticMng/mail/accounts/page")),
|
|
|
|
|
"/admin/automaticMng/mail/templates": d(() => import("@/app/(main)/admin/automaticMng/mail/templates/page")),
|
|
|
|
|
"/admin/automaticMng/mail/dashboardList": d(() => import("@/app/(main)/admin/automaticMng/mail/dashboardList/page")),
|
|
|
|
|
"/admin/automaticMng/mail/bulk-send": d(() => import("@/app/(main)/admin/automaticMng/mail/bulk-send/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
|
|
|
|
|
// 배치 관리
|
2026-03-11 22:06:22 +09:00
|
|
|
"/admin/batch-management": d(() => import("@/app/(main)/admin/batch-management/page")),
|
|
|
|
|
"/admin/batch-management-new": d(() => import("@/app/(main)/admin/batch-management-new/page")),
|
|
|
|
|
|
|
|
|
|
// 결재 관리
|
|
|
|
|
"/admin/approvalTemplate": d(() => import("@/app/(main)/admin/approvalTemplate/page")),
|
|
|
|
|
"/admin/approvalMng": d(() => import("@/app/(main)/admin/approvalMng/page")),
|
|
|
|
|
"/admin/approvalBox": d(() => import("@/app/(main)/admin/approvalBox/page")),
|
|
|
|
|
|
|
|
|
|
// AI 어시스턴트
|
|
|
|
|
"/admin/aiAssistant": d(() => import("@/app/(main)/admin/aiAssistant/page")),
|
|
|
|
|
"/admin/aiAssistant/usage": d(() => import("@/app/(main)/admin/aiAssistant/usage/page")),
|
|
|
|
|
"/admin/aiAssistant/history": d(() => import("@/app/(main)/admin/aiAssistant/history/page")),
|
|
|
|
|
"/admin/aiAssistant/api-keys": d(() => import("@/app/(main)/admin/aiAssistant/api-keys/page")),
|
|
|
|
|
"/admin/aiAssistant/dashboard": d(() => import("@/app/(main)/admin/aiAssistant/dashboard/page")),
|
|
|
|
|
"/admin/aiAssistant/chat": d(() => import("@/app/(main)/admin/aiAssistant/chat/page")),
|
|
|
|
|
"/admin/aiAssistant/api-test": d(() => import("@/app/(main)/admin/aiAssistant/api-test/page")),
|
|
|
|
|
|
|
|
|
|
// 기타 관리
|
|
|
|
|
"/admin/cascading-management": d(() => import("@/app/(main)/admin/cascading-management/page")),
|
|
|
|
|
"/admin/cascading-relations": d(() => import("@/app/(main)/admin/cascading-relations/page")),
|
|
|
|
|
"/admin/layouts": d(() => import("@/app/(main)/admin/layouts/page")),
|
|
|
|
|
"/admin/templates": d(() => import("@/app/(main)/admin/templates/page")),
|
|
|
|
|
"/admin/monitoring": d(() => import("@/app/(main)/admin/monitoring/page")),
|
|
|
|
|
"/admin/standards": d(() => import("@/app/(main)/admin/standards/page")),
|
|
|
|
|
"/admin/flow-external-db": d(() => import("@/app/(main)/admin/flow-external-db/page")),
|
|
|
|
|
"/admin/auto-fill": d(() => import("@/app/(main)/admin/auto-fill/page")),
|
|
|
|
|
"/admin/system-notices": d(() => import("@/app/(main)/admin/system-notices/page")),
|
|
|
|
|
"/admin/audit-log": d(() => import("@/app/(main)/admin/audit-log/page")),
|
|
|
|
|
|
|
|
|
|
// 개발/테스트
|
|
|
|
|
"/admin/debug": d(() => import("@/app/(main)/admin/debug/page")),
|
|
|
|
|
"/admin/debug-simple": d(() => import("@/app/(main)/admin/debug-simple/page")),
|
|
|
|
|
"/admin/debug-layout": d(() => import("@/app/(main)/admin/debug-layout/page")),
|
|
|
|
|
"/admin/test": d(() => import("@/app/(main)/admin/test/page")),
|
|
|
|
|
"/admin/ui-components-demo": d(() => import("@/app/(main)/admin/ui-components-demo/page")),
|
|
|
|
|
"/admin/validation-demo": d(() => import("@/app/(main)/admin/validation-demo/page")),
|
|
|
|
|
"/admin/token-test": d(() => import("@/app/(main)/admin/token-test/page")),
|
|
|
|
|
|
|
|
|
|
// === 사용자 화면 (admin이 아닌 URL 기반 메뉴) ===
|
|
|
|
|
"/approval": d(() => import("@/app/(main)/approval/page")),
|
|
|
|
|
"/dashboard": d(() => import("@/app/(main)/dashboard/page")),
|
|
|
|
|
"/multilang": d(() => import("@/app/(main)/multilang/page")),
|
|
|
|
|
"/test-flow": d(() => import("@/app/(main)/test-flow/page")),
|
|
|
|
|
"/main": d(() => import("@/app/(main)/main/page")),
|
2026-02-27 14:25:53 +09:00
|
|
|
};
|
|
|
|
|
|
2026-03-11 22:06:22 +09:00
|
|
|
/**
|
|
|
|
|
* 동적 라우트 패턴 매칭 (URL 경로에 동적 세그먼트가 포함된 경우)
|
|
|
|
|
* /admin/screenMng/dashboardList/123 → dashboardList/[id] 페이지에 매핑
|
|
|
|
|
*
|
|
|
|
|
* extractParams: URL에서 동적 파라미터를 추출 (use(params)를 쓰는 페이지용)
|
|
|
|
|
* 추출된 값은 params={Promise.resolve(...)}로 전달되어
|
|
|
|
|
* Next.js 라우팅 컨텍스트 없이도 use(params)가 정상 동작함
|
|
|
|
|
*/
|
|
|
|
|
interface DynamicRouteEntry {
|
|
|
|
|
pattern: RegExp;
|
|
|
|
|
loader: () => Promise<any>;
|
|
|
|
|
extractParams?: (url: string) => Record<string, string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DYNAMIC_ROUTE_PATTERNS: DynamicRouteEntry[] = [
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/userMng\/rolesList\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/userMng/rolesList/[id]/page"),
|
|
|
|
|
extractParams: (url) => ({ id: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/userMng\/companyList\/([^/]+)\/departments$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/userMng/companyList/[companyCode]/departments/page"),
|
|
|
|
|
extractParams: (url) => ({ companyCode: url.split("/")[4] }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/automaticMng\/batchmngList\/create$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/automaticMng/batchmngList/create/page"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/automaticMng\/batchmngList\/edit\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page"),
|
|
|
|
|
extractParams: (url) => ({ id: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/automaticMng\/flowMgmtList\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/automaticMng/flowMgmtList/[id]/page"),
|
|
|
|
|
extractParams: (url) => ({ id: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/standards\/new$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/standards/new/page"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/standards\/([^/]+)\/edit$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/standards/[webType]/edit/page"),
|
|
|
|
|
extractParams: (url) => ({ webType: url.split("/")[3] }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/standards\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/standards/[webType]/page"),
|
|
|
|
|
extractParams: (url) => ({ webType: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/systemMng\/dataflow\/edit\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/systemMng/dataflow/edit/[diagramId]/page"),
|
|
|
|
|
extractParams: (url) => ({ diagramId: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/screenMng\/barcodeList\/designer\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/screenMng/barcodeList/designer/[labelId]/page"),
|
|
|
|
|
extractParams: (url) => ({ labelId: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/screenMng\/reportList\/designer\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/screenMng/reportList/designer/[reportId]/page"),
|
|
|
|
|
extractParams: (url) => ({ reportId: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pattern: /^\/admin\/screenMng\/dashboardList\/([^/]+)$/,
|
|
|
|
|
loader: () => import("@/app/(main)/admin/screenMng/dashboardList/[id]/page"),
|
|
|
|
|
extractParams: (url) => ({ id: url.split("/").pop()! }),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface DynamicRouteResult {
|
|
|
|
|
component: React.ComponentType<any>;
|
|
|
|
|
params?: Record<string, string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dynamicRouteCache = new Map<string, DynamicRouteResult>();
|
|
|
|
|
|
|
|
|
|
function resolveDynamicRoute(cleanUrl: string): DynamicRouteResult | null {
|
|
|
|
|
if (dynamicRouteCache.has(cleanUrl)) {
|
|
|
|
|
return dynamicRouteCache.get(cleanUrl)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const entry of DYNAMIC_ROUTE_PATTERNS) {
|
|
|
|
|
if (entry.pattern.test(cleanUrl)) {
|
|
|
|
|
const comp = d(entry.loader);
|
|
|
|
|
const params = entry.extractParams?.(cleanUrl);
|
|
|
|
|
const result: DynamicRouteResult = { component: comp, params };
|
|
|
|
|
dynamicRouteCache.set(cleanUrl, result);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 14:25:53 +09:00
|
|
|
function AdminPageFallback({ url }: { url: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-lg font-semibold text-foreground">페이지 로딩 불가</p>
|
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
|
|
|
경로: {url}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-2 text-xs text-muted-foreground">
|
|
|
|
|
AdminPageRenderer 레지스트리에 이 URL을 추가해주세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AdminPageRendererProps {
|
|
|
|
|
url: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AdminPageRenderer({ url }: AdminPageRendererProps) {
|
2026-03-11 22:06:22 +09:00
|
|
|
const cleanUrl = url.split("?")[0].split("#")[0].replace(/\/$/, "");
|
|
|
|
|
|
|
|
|
|
// 0) /screens/[id] → ScreenViewPageWrapper 직접 렌더링
|
|
|
|
|
// 탭 시스템에서는 useParams()가 동작하지 않으므로 screenId를 직접 추출해서 전달
|
|
|
|
|
const screenIdMatch = cleanUrl.match(/^\/screens\/(\d+)$/);
|
|
|
|
|
if (screenIdMatch) {
|
|
|
|
|
const screenId = parseInt(screenIdMatch[1]);
|
|
|
|
|
return <ScreenViewPageWrapper screenIdProp={screenId} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 0-1) /screen/[code] → screenCode를 screenId로 변환 후 렌더링
|
|
|
|
|
const screenCodeMatch = cleanUrl.match(/^\/screen\/([^/]+)$/);
|
|
|
|
|
if (screenCodeMatch) {
|
|
|
|
|
return <ScreenCodeResolver screenCode={screenCodeMatch[1]} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 0-2) /dashboard/[id] → DashboardTabRenderer 직접 렌더링
|
|
|
|
|
// Next.js의 params Promise를 우회하여 dashboardId를 직접 전달
|
|
|
|
|
const dashboardMatch = cleanUrl.match(/^\/dashboard\/([^/]+)$/);
|
|
|
|
|
if (dashboardMatch) {
|
|
|
|
|
return <DashboardTabRenderer dashboardId={dashboardMatch[1]} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolved = useMemo(() => {
|
|
|
|
|
// 1) 정적 레지스트리 매칭
|
|
|
|
|
if (ADMIN_PAGE_REGISTRY[cleanUrl]) {
|
|
|
|
|
return { component: ADMIN_PAGE_REGISTRY[cleanUrl] } as DynamicRouteResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2) 동적 라우트 패턴 매칭 (/admin/xxx/[id] 등)
|
|
|
|
|
const dynamicMatch = resolveDynamicRoute(cleanUrl);
|
|
|
|
|
if (dynamicMatch) {
|
|
|
|
|
return dynamicMatch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}, [cleanUrl]);
|
2026-02-27 14:25:53 +09:00
|
|
|
|
2026-03-11 22:06:22 +09:00
|
|
|
if (!resolved) {
|
2026-02-27 14:25:53 +09:00
|
|
|
return <AdminPageFallback url={url} />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 22:06:22 +09:00
|
|
|
const { component: PageComponent, params } = resolved;
|
|
|
|
|
|
|
|
|
|
// 동적 라우트에서 추출한 파라미터가 있으면 params={Promise.resolve(...)}로 전달
|
|
|
|
|
// Next.js page 컴포넌트의 use(params)가 동기적으로 resolve됨
|
|
|
|
|
if (params) {
|
|
|
|
|
return <PageComponent params={Promise.resolve(params)} />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 14:25:53 +09:00
|
|
|
return <PageComponent />;
|
|
|
|
|
}
|