"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 = () => (
); const d = (loader: () => Promise) => 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(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(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 ; if (error || !dashboard) { return (

{error || "대시보드를 찾을 수 없습니다"}

대시보드 ID: {dashboardId}

); } return (
); } /** * /screen/[screenCode] URL을 screenId로 변환해서 ScreenViewPageWrapper를 렌더링 */ function ScreenCodeResolver({ screenCode }: { screenCode: string }) { const [screenId, setScreenId] = useState(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 (

화면을 찾을 수 없습니다

화면 코드: {screenCode}

); } if (screenId === null) { return ; } return ; } /** * 관리자 페이지를 URL 기반으로 동적 로딩하는 레지스트리. * 레지스트리에 없는 URL은 자동으로 파일 경로 기반 동적 임포트를 시도한다. */ const ADMIN_PAGE_REGISTRY: Record> = { // 관리자 메인 "/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; extractParams?: (url: string) => Record; } 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; params?: Record; } const dynamicRouteCache = new Map(); 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 (

페이지 로딩 불가

경로: {url}

AdminPageRenderer 레지스트리에 이 URL을 추가해주세요.

); } 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 ; } // 0-1) /screen/[code] → screenCode를 screenId로 변환 후 렌더링 const screenCodeMatch = cleanUrl.match(/^\/screen\/([^/]+)$/); if (screenCodeMatch) { return ; } // 0-2) /dashboard/[id] → DashboardTabRenderer 직접 렌더링 // Next.js의 params Promise를 우회하여 dashboardId를 직접 전달 const dashboardMatch = cleanUrl.match(/^\/dashboard\/([^/]+)$/); if (dashboardMatch) { return ; } 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 ; } const { component: PageComponent, params } = resolved; // 동적 라우트에서 추출한 파라미터가 있으면 params={Promise.resolve(...)}로 전달 // Next.js page 컴포넌트의 use(params)가 동기적으로 resolve됨 if (params) { return ; } return ; }