ERP-node/frontend/components/layout/AdminPageRenderer.tsx

402 lines
16 KiB
TypeScript

"use client";
import React, { useMemo, useState, useEffect } from "react";
import dynamic from "next/dynamic";
import { Loader2 } from "lucide-react";
import { ScreenViewPageWrapper } from "@/app/(main)/screens/[screenId]/page";
import { apiClient } from "@/lib/api/client";
const LoadingFallback = () => (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
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} />;
}
/**
* 관리자 페이지를 URL 기반으로 동적 로딩하는 레지스트리.
* 레지스트리에 없는 URL은 자동으로 파일 경로 기반 동적 임포트를 시도한다.
*/
const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
// 관리자 메인
"/admin": d(() => import("@/app/(main)/admin/page")),
// 메뉴 관리
"/admin/menu": d(() => import("@/app/(main)/admin/menu/page")),
// 사용자 관리
"/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")),
// 화면 관리
"/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")),
// 시스템 관리
"/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")),
// 자동화 관리
"/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")),
// 메일
"/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")),
// 배치 관리
"/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")),
};
/**
* 동적 라우트 패턴 매칭 (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;
}
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) {
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]);
if (!resolved) {
return <AdminPageFallback url={url} />;
}
const { component: PageComponent, params } = resolved;
// 동적 라우트에서 추출한 파라미터가 있으면 params={Promise.resolve(...)}로 전달
// Next.js page 컴포넌트의 use(params)가 동기적으로 resolve됨
if (params) {
return <PageComponent params={Promise.resolve(params)} />;
}
return <PageComponent />;
}