feat: implement packaging unit and item management APIs
- Added CRUD operations for packaging units and their associated items in the new `packagingController.ts`. - Implemented routes for managing packaging units and items in `packagingRoutes.ts`. - Enhanced error handling and logging for better traceability. - Ensured company code filtering for data access based on user roles. Made-with: Cursor
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
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">
|
||||
@@ -10,11 +12,52 @@ const LoadingFallback = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 관리자 페이지를 URL 기반으로 동적 로딩하는 레지스트리.
|
||||
* 사이드바 메뉴에서 접근하는 주요 페이지를 명시적으로 매핑한다.
|
||||
* 매핑되지 않은 URL은 catch-all fallback으로 처리된다.
|
||||
*/
|
||||
function ScreenCodeResolver({ screenCode }: { screenCode: string }) {
|
||||
const [screenId, setScreenId] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const numericId = parseInt(screenCode);
|
||||
if (!isNaN(numericId)) {
|
||||
setScreenId(numericId);
|
||||
setLoading(false);
|
||||
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);
|
||||
const target = exact || arr[0];
|
||||
if (target) setScreenId(target.screenId || target.screen_id);
|
||||
} catch {
|
||||
console.error("스크린 코드 변환 실패:", screenCode);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
resolve();
|
||||
}, [screenCode]);
|
||||
|
||||
if (loading) return <LoadingFallback />;
|
||||
if (!screenId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">화면을 찾을 수 없습니다 (코드: {screenCode})</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <ScreenViewPageWrapper screenIdProp={screenId} />;
|
||||
}
|
||||
|
||||
const DashboardViewPage = dynamic(
|
||||
() => import("@/app/(main)/dashboard/[dashboardId]/page"),
|
||||
{ ssr: false, loading: LoadingFallback },
|
||||
);
|
||||
|
||||
const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
// 관리자 메인
|
||||
"/admin": dynamic(() => import("@/app/(main)/admin/page"), { ssr: false, loading: LoadingFallback }),
|
||||
@@ -62,6 +105,16 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/admin/batch-management": dynamic(() => import("@/app/(main)/admin/batch-management/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/batch-management-new": dynamic(() => import("@/app/(main)/admin/batch-management-new/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
// 결재 관리
|
||||
"/admin/approvalTemplate": dynamic(() => import("@/app/(main)/admin/approvalTemplate/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/approvalBox": dynamic(() => import("@/app/(main)/admin/approvalBox/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/approvalMng": dynamic(() => import("@/app/(main)/admin/approvalMng/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
// 시스템
|
||||
"/admin/audit-log": dynamic(() => import("@/app/(main)/admin/audit-log/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/system-notices": dynamic(() => import("@/app/(main)/admin/system-notices/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/aiAssistant": dynamic(() => import("@/app/(main)/admin/aiAssistant/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
// 기타
|
||||
"/admin/cascading-management": dynamic(() => import("@/app/(main)/admin/cascading-management/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/admin/cascading-relations": dynamic(() => import("@/app/(main)/admin/cascading-relations/page"), { ssr: false, loading: LoadingFallback }),
|
||||
@@ -73,18 +126,115 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/admin/auto-fill": dynamic(() => import("@/app/(main)/admin/auto-fill/page"), { ssr: false, loading: LoadingFallback }),
|
||||
};
|
||||
|
||||
// 매핑되지 않은 URL용 Fallback
|
||||
const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
|
||||
"/admin/aiAssistant/dashboard": () => import("@/app/(main)/admin/aiAssistant/dashboard/page"),
|
||||
"/admin/aiAssistant/history": () => import("@/app/(main)/admin/aiAssistant/history/page"),
|
||||
"/admin/aiAssistant/api-keys": () => import("@/app/(main)/admin/aiAssistant/api-keys/page"),
|
||||
"/admin/aiAssistant/api-test": () => import("@/app/(main)/admin/aiAssistant/api-test/page"),
|
||||
"/admin/aiAssistant/usage": () => import("@/app/(main)/admin/aiAssistant/usage/page"),
|
||||
"/admin/aiAssistant/chat": () => import("@/app/(main)/admin/aiAssistant/chat/page"),
|
||||
"/admin/screenMng/barcodeList": () => import("@/app/(main)/admin/screenMng/barcodeList/page"),
|
||||
"/admin/automaticMng/batchmngList/create": () => import("@/app/(main)/admin/automaticMng/batchmngList/create/page"),
|
||||
"/admin/systemMng/dataflow/node-editorList": () => import("@/app/(main)/admin/systemMng/dataflow/node-editorList/page"),
|
||||
"/admin/standards/new": () => import("@/app/(main)/admin/standards/new/page"),
|
||||
};
|
||||
|
||||
const DYNAMIC_ADMIN_PATTERNS: Array<{
|
||||
pattern: RegExp;
|
||||
getImport: (match: RegExpMatchArray) => Promise<any>;
|
||||
extractParams: (match: RegExpMatchArray) => Record<string, string>;
|
||||
}> = [
|
||||
{
|
||||
pattern: /^\/admin\/userMng\/rolesList\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/userMng/rolesList/[id]/page"),
|
||||
extractParams: (m) => ({ id: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/screenMng\/dashboardList\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/screenMng/dashboardList/[id]/page"),
|
||||
extractParams: (m) => ({ id: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/automaticMng\/flowMgmtList\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/automaticMng/flowMgmtList/[id]/page"),
|
||||
extractParams: (m) => ({ id: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/automaticMng\/batchmngList\/edit\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page"),
|
||||
extractParams: (m) => ({ id: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/screenMng\/barcodeList\/designer\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/screenMng/barcodeList/designer/[labelId]/page"),
|
||||
extractParams: (m) => ({ labelId: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/screenMng\/reportList\/designer\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/screenMng/reportList/designer/[reportId]/page"),
|
||||
extractParams: (m) => ({ reportId: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/systemMng\/dataflow\/edit\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/systemMng/dataflow/edit/[diagramId]/page"),
|
||||
extractParams: (m) => ({ diagramId: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/userMng\/companyList\/([^/]+)\/departments$/,
|
||||
getImport: () => import("@/app/(main)/admin/userMng/companyList/[companyCode]/departments/page"),
|
||||
extractParams: (m) => ({ companyCode: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/standards\/([^/]+)\/edit$/,
|
||||
getImport: () => import("@/app/(main)/admin/standards/[webType]/edit/page"),
|
||||
extractParams: (m) => ({ webType: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^\/admin\/standards\/([^/]+)$/,
|
||||
getImport: () => import("@/app/(main)/admin/standards/[webType]/page"),
|
||||
extractParams: (m) => ({ webType: m[1] }),
|
||||
},
|
||||
];
|
||||
|
||||
function DynamicAdminLoader({ url, params }: { url: string; params?: Record<string, string> }) {
|
||||
const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const staticImport = DYNAMIC_ADMIN_IMPORTS[url];
|
||||
if (staticImport) {
|
||||
staticImport()
|
||||
.then((mod) => setComponent(() => mod.default))
|
||||
.catch(() => setFailed(true));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { pattern, getImport, extractParams } of DYNAMIC_ADMIN_PATTERNS) {
|
||||
const match = url.match(pattern);
|
||||
if (match) {
|
||||
getImport()
|
||||
.then((mod) => setComponent(() => mod.default))
|
||||
.catch(() => setFailed(true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setFailed(true);
|
||||
}, [url]);
|
||||
|
||||
if (failed) return <AdminPageFallback url={url} />;
|
||||
if (!Component) return <LoadingFallback />;
|
||||
if (params) return <Component params={Promise.resolve(params)} />;
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
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>
|
||||
<p className="mt-1 text-sm text-muted-foreground">경로: {url}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">해당 페이지가 존재하지 않습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -95,15 +245,58 @@ interface AdminPageRendererProps {
|
||||
}
|
||||
|
||||
export function AdminPageRenderer({ url }: AdminPageRendererProps) {
|
||||
const PageComponent = useMemo(() => {
|
||||
// URL에서 쿼리스트링/해시 제거 후 매칭
|
||||
const cleanUrl = url.split("?")[0].split("#")[0].replace(/\/$/, "");
|
||||
return ADMIN_PAGE_REGISTRY[cleanUrl] || null;
|
||||
}, [url]);
|
||||
const cleanUrl = url.split("?")[0].split("#")[0].replace(/\/$/, "");
|
||||
|
||||
if (!PageComponent) {
|
||||
return <AdminPageFallback url={url} />;
|
||||
console.log("[AdminPageRenderer] 렌더링:", { url, cleanUrl });
|
||||
|
||||
// 화면 할당: /screens/[id]
|
||||
const screensIdMatch = cleanUrl.match(/^\/screens\/(\d+)$/);
|
||||
if (screensIdMatch) {
|
||||
console.log("[AdminPageRenderer] → /screens/[id] 매칭:", screensIdMatch[1]);
|
||||
return <ScreenViewPageWrapper screenIdProp={parseInt(screensIdMatch[1])} />;
|
||||
}
|
||||
|
||||
return <PageComponent />;
|
||||
// 화면 할당: /screen/[code] (구 형식)
|
||||
const screenCodeMatch = cleanUrl.match(/^\/screen\/([^/]+)$/);
|
||||
if (screenCodeMatch) {
|
||||
console.log("[AdminPageRenderer] → /screen/[code] 매칭:", screenCodeMatch[1]);
|
||||
return <ScreenCodeResolver screenCode={screenCodeMatch[1]} />;
|
||||
}
|
||||
|
||||
// 대시보드 할당: /dashboard/[id]
|
||||
const dashboardMatch = cleanUrl.match(/^\/dashboard\/([^/]+)$/);
|
||||
if (dashboardMatch) {
|
||||
console.log("[AdminPageRenderer] → /dashboard/[id] 매칭:", dashboardMatch[1]);
|
||||
return <DashboardViewPage params={Promise.resolve({ dashboardId: dashboardMatch[1] })} />;
|
||||
}
|
||||
|
||||
// URL 직접 입력: 레지스트리 매칭
|
||||
const PageComponent = useMemo(() => {
|
||||
return ADMIN_PAGE_REGISTRY[cleanUrl] || null;
|
||||
}, [cleanUrl]);
|
||||
|
||||
if (PageComponent) {
|
||||
console.log("[AdminPageRenderer] → 레지스트리 매칭:", cleanUrl);
|
||||
return <PageComponent />;
|
||||
}
|
||||
|
||||
// 레지스트리에 없으면 동적 import 시도
|
||||
// 동적 라우트 패턴 매칭 (params 추출)
|
||||
for (const { pattern, extractParams } of DYNAMIC_ADMIN_PATTERNS) {
|
||||
const match = cleanUrl.match(pattern);
|
||||
if (match) {
|
||||
const params = extractParams(match);
|
||||
console.log("[AdminPageRenderer] → 동적 라우트 매칭:", cleanUrl, params);
|
||||
return <DynamicAdminLoader url={cleanUrl} params={params} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 정적 동적 import 목록에 있으면
|
||||
if (DYNAMIC_ADMIN_IMPORTS[cleanUrl]) {
|
||||
console.log("[AdminPageRenderer] → 동적 import:", cleanUrl);
|
||||
return <DynamicAdminLoader url={cleanUrl} />;
|
||||
}
|
||||
|
||||
console.error("[AdminPageRenderer] 미등록 URL:", cleanUrl);
|
||||
return <AdminPageFallback url={url} />;
|
||||
}
|
||||
|
||||
@@ -202,12 +202,26 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten
|
||||
|
||||
const children = convertMenuToUI(allMenus, userInfo, menuId, tabTitle);
|
||||
|
||||
const menuUrl = menu.menu_url || menu.MENU_URL || "#";
|
||||
const screenCode = menu.screen_code || menu.SCREEN_CODE || null;
|
||||
const menuType = String(menu.menu_type ?? menu.MENU_TYPE ?? "");
|
||||
|
||||
let screenId: number | null = null;
|
||||
const screensMatch = menuUrl.match(/^\/screens\/(\d+)/);
|
||||
if (screensMatch) {
|
||||
screenId = parseInt(screensMatch[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
id: menuId,
|
||||
objid: menuId,
|
||||
name: displayName,
|
||||
tabTitle,
|
||||
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON),
|
||||
url: menu.menu_url || menu.MENU_URL || "#",
|
||||
url: menuUrl,
|
||||
screenCode,
|
||||
screenId,
|
||||
menuType,
|
||||
children: children.length > 0 ? children : undefined,
|
||||
hasChildren: children.length > 0,
|
||||
};
|
||||
@@ -341,42 +355,76 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
const handleMenuClick = async (menu: any) => {
|
||||
if (menu.hasChildren) {
|
||||
toggleMenu(menu.id);
|
||||
} else {
|
||||
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("currentMenuName", menuName);
|
||||
return;
|
||||
}
|
||||
|
||||
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("currentMenuName", menuName);
|
||||
}
|
||||
|
||||
const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0");
|
||||
const isAdminMenu = menu.menuType === "0";
|
||||
|
||||
console.log("[handleMenuClick] 메뉴 클릭:", {
|
||||
menuName,
|
||||
menuObjid,
|
||||
menuType: menu.menuType,
|
||||
isAdminMenu,
|
||||
screenId: menu.screenId,
|
||||
screenCode: menu.screenCode,
|
||||
url: menu.url,
|
||||
fullMenu: menu,
|
||||
});
|
||||
|
||||
// 관리자 메뉴 (menu_type = 0): URL 직접 입력 → admin 탭
|
||||
if (isAdminMenu) {
|
||||
if (menu.url && menu.url !== "#") {
|
||||
console.log("[handleMenuClick] → admin 탭:", menu.url);
|
||||
openTab({ type: "admin", title: menuName, adminUrl: menu.url });
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
} else {
|
||||
toast.warning("이 메뉴에는 연결된 페이지가 없습니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 메뉴 (menu_type = 1, 2): 화면/대시보드 할당
|
||||
// 1) screenId가 메뉴 URL에서 추출된 경우 바로 screen 탭
|
||||
if (menu.screenId) {
|
||||
console.log("[handleMenuClick] → screen 탭 (URL에서 screenId 추출):", menu.screenId);
|
||||
openTab({ type: "screen", title: menuName, screenId: menu.screenId, menuObjid });
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) screen_menu_assignments 테이블 조회
|
||||
if (menuObjid) {
|
||||
try {
|
||||
const menuObjid = menu.objid || menu.id;
|
||||
console.log("[handleMenuClick] → screen_menu_assignments 조회 시도, menuObjid:", menuObjid);
|
||||
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||
|
||||
console.log("[handleMenuClick] → 조회 결과:", assignedScreens);
|
||||
if (assignedScreens.length > 0) {
|
||||
const firstScreen = assignedScreens[0];
|
||||
openTab({
|
||||
type: "screen",
|
||||
title: menuName,
|
||||
screenId: firstScreen.screenId,
|
||||
menuObjid: parseInt(menuObjid),
|
||||
});
|
||||
console.log("[handleMenuClick] → screen 탭 (assignments):", assignedScreens[0].screenId);
|
||||
openTab({ type: "screen", title: menuName, screenId: assignedScreens[0].screenId, menuObjid });
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
console.warn("할당된 화면 조회 실패");
|
||||
}
|
||||
|
||||
if (menu.url && menu.url !== "#") {
|
||||
openTab({
|
||||
type: "admin",
|
||||
title: menuName,
|
||||
adminUrl: menu.url,
|
||||
});
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
} else {
|
||||
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
||||
} catch (err) {
|
||||
console.error("[handleMenuClick] 할당된 화면 조회 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 대시보드 할당 (/dashboard/xxx) → admin 탭으로 렌더링 (AdminPageRenderer가 처리)
|
||||
if (menu.url && menu.url.startsWith("/dashboard/")) {
|
||||
console.log("[handleMenuClick] → 대시보드 탭:", menu.url);
|
||||
openTab({ type: "admin", title: menuName, adminUrl: menu.url });
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn("[handleMenuClick] 어떤 조건에도 매칭 안 됨:", { menuName, menuType: menu.menuType, url: menu.url, screenId: menu.screenId });
|
||||
toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요.");
|
||||
};
|
||||
|
||||
const handleModeSwitch = () => {
|
||||
|
||||
@@ -226,6 +226,14 @@ function TabPageRenderer({
|
||||
tab: { id: string; type: string; screenId?: number; menuObjid?: number; adminUrl?: string };
|
||||
refreshKey: number;
|
||||
}) {
|
||||
console.log("[TabPageRenderer] 탭 렌더링:", {
|
||||
tabId: tab.id,
|
||||
type: tab.type,
|
||||
screenId: tab.screenId,
|
||||
adminUrl: tab.adminUrl,
|
||||
menuObjid: tab.menuObjid,
|
||||
});
|
||||
|
||||
if (tab.type === "screen" && tab.screenId != null) {
|
||||
return (
|
||||
<ScreenViewPageWrapper
|
||||
@@ -244,5 +252,6 @@ function TabPageRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
console.warn("[TabPageRenderer] 렌더링 불가 - 매칭 조건 없음:", tab);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user