From ac913990a3857da98b7a670934fe9878541a6079 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 6 Apr 2026 10:46:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20POP=20=EC=84=A4=EC=A0=95=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=EB=A5=BC=20screen=5Flayouts=5Fpop=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - usePopSettings: pop_settings 테이블 대신 screen_layouts_pop.popConfig에서 읽기 - 화면별 독립 설정 (URL→screen_id 자동 매핑) - PC 설정 페이지: layout-pop API로 저장/조회 - "같은 유형의 모든 화면에 적용" 동기화 체크박스 - pop_settings 400 에러 완전 제거 - 신규 화면 등록: 판매출고(5), 출고유형(6), 공정실행(7), 생산관리(8) --- .../admin/screenMng/popSettingsMng/page.tsx | 172 ++++++++++++------ frontend/hooks/pop/usePopSettings.ts | 132 ++++++++++---- 2 files changed, 207 insertions(+), 97 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index 3bad27db..b0d0b9be 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; +import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Select, @@ -92,6 +93,7 @@ interface ScreenItem { name: string; url: string; settingsKey: string; + screenId: number; } interface ScreenGroup { @@ -107,9 +109,9 @@ const SCREEN_GROUPS: ScreenGroup[] = [ name: "입고", icon: "PackageOpen", screens: [ - { id: "purchase-inbound", name: "구매입고", url: "/pop/inbound/purchase", settingsKey: "inbound" }, - { id: "return-inbound", name: "반품입고", url: "/pop/inbound", settingsKey: "inbound" }, - { id: "subcontract-inbound", name: "사급자재", url: "/pop/inbound", settingsKey: "inbound" }, + { id: "purchase-inbound", name: "구매입고", url: "/pop/inbound/purchase", settingsKey: "inbound", screenId: 6528 }, + { id: "inbound-cart", name: "입고 장바구니", url: "/pop/inbound/cart", settingsKey: "inbound", screenId: 6527 }, + { id: "inbound-type", name: "입고유형선택", url: "/pop/inbound", settingsKey: "inbound", screenId: 6529 }, ], }, { @@ -117,8 +119,8 @@ const SCREEN_GROUPS: ScreenGroup[] = [ name: "출고", icon: "Truck", screens: [ - { id: "sales-outbound", name: "판매출고", url: "/pop/outbound/sales", settingsKey: "outbound" }, - { id: "subcontract-outbound", name: "외주출고", url: "/pop/outbound", settingsKey: "outbound" }, + { id: "sales-outbound", name: "판매출고", url: "/pop/outbound/sales", settingsKey: "outbound", screenId: 5 }, + { id: "outbound-type", name: "출고유형선택", url: "/pop/outbound", settingsKey: "outbound", screenId: 6 }, ], }, { @@ -126,8 +128,8 @@ const SCREEN_GROUPS: ScreenGroup[] = [ name: "생산", icon: "Factory", screens: [ - { id: "process-execution", name: "공정실행", url: "/pop/production/process", settingsKey: "processExecution" }, - { id: "work-instruction", name: "작업지시", url: "/pop/production/work", settingsKey: "processExecution" }, + { id: "process-execution", name: "공정실행", url: "/pop/production/process", settingsKey: "processExecution", screenId: 7 }, + { id: "production-main", name: "생산관리", url: "/pop/production", settingsKey: "processExecution", screenId: 8 }, ], }, { @@ -135,7 +137,7 @@ const SCREEN_GROUPS: ScreenGroup[] = [ name: "홈", icon: "Home", screens: [ - { id: "home-screen", name: "홈 화면", url: "/pop/home", settingsKey: "home" }, + { id: "home-screen", name: "홈 화면", url: "/pop/home", settingsKey: "home", screenId: 6526 }, ], }, { @@ -143,7 +145,7 @@ const SCREEN_GROUPS: ScreenGroup[] = [ name: "PLC", icon: "Cpu", screens: [ - { id: "plc-settings", name: "PLC 연동", url: "/pop/home", settingsKey: "plc" }, + { id: "plc-settings", name: "PLC 연동", url: "/pop/home", settingsKey: "plc", screenId: 6526 }, ], }, ]; @@ -690,45 +692,48 @@ export default function PopSettingsMngPage() { ); const [navCollapsed, setNavCollapsed] = useState(false); const [hasChanges, setHasChanges] = useState(false); + const [syncToAll, setSyncToAll] = useState(false); const [lastPath, setLastPath] = useState(""); const iframeRef = useRef(null); - // ---- Load settings ---- + // ---- Load settings from screen_layouts_pop per screen ---- const fetchSettings = useCallback(async () => { setLoading(true); try { - const res = await apiClient.get("/data/pop_settings?pageSize=1"); - const rows = res.data?.data?.data || res.data?.data || []; - if (rows.length > 0 && rows[0].settings_data) { - const parsed = - typeof rows[0].settings_data === "string" - ? JSON.parse(rows[0].settings_data) - : rows[0].settings_data; - const merged: PopSettings = { - ...DEFAULT_SETTINGS, - ...parsed, - screens: { - ...DEFAULT_SETTINGS.screens, - ...parsed.screens, - processExecution: { ...DEFAULT_SETTINGS.screens.processExecution, ...parsed.screens?.processExecution }, - inbound: { ...DEFAULT_SETTINGS.screens.inbound, ...parsed.screens?.inbound }, - outbound: { ...DEFAULT_SETTINGS.screens.outbound, ...parsed.screens?.outbound }, - home: { ...DEFAULT_SETTINGS.screens.home, ...parsed.screens?.home }, - plc: { ...DEFAULT_SETTINGS.screens.plc, ...parsed.screens?.plc }, - }, - }; - setSettings(merged); - } - } catch { - const local = localStorage.getItem("pop_settings"); - if (local) { - try { - const parsed = JSON.parse(local); - setSettings({ ...DEFAULT_SETTINGS, ...parsed }); - } catch { - /* use default */ + // Collect all unique screenIds from SCREEN_GROUPS + const allScreens: { screenId: number; settingsKey: string }[] = []; + for (const group of SCREEN_GROUPS) { + for (const screen of group.screens) { + if (!allScreens.some((s) => s.screenId === screen.screenId && s.settingsKey === screen.settingsKey)) { + allScreens.push({ screenId: screen.screenId, settingsKey: screen.settingsKey }); + } } } + + // Fetch popConfig from each screen's layout-pop + const merged: PopSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); + + await Promise.all( + allScreens.map(async ({ screenId, settingsKey }) => { + try { + const res = await apiClient.get(`/screen-management/screens/${screenId}/layout-pop`); + const popConfig = res.data?.data?.settings?.popConfig; + if (popConfig) { + const key = settingsKey as keyof PopSettings["screens"]; + (merged.screens as Record>)[key] = { + ...(merged.screens as Record>)[key], + ...popConfig, + }; + } + } catch { + // Screen may not have layout-pop yet, use defaults + } + }), + ); + + setSettings(merged); + } catch { + // Fallback: use defaults } setLoading(false); }, []); @@ -782,28 +787,57 @@ export default function PopSettingsMngPage() { setHasChanges(true); }; - // ---- Save ---- + // ---- Save to screen_layouts_pop per screen ---- const handleSave = async () => { + if (!selectedScreen) return; setSaving(true); + try { - await apiClient.post("/pop/execute-action", { - taskType: "data-save", - targetTable: "pop_settings", - columnMapping: { - id: crypto.randomUUID(), - company_code: user?.companyCode || "COMPANY_7", - settings_data: JSON.stringify(settings), - updated_by: user?.userId, - }, - }); + const currentKey = selectedScreen.settingsKey as keyof PopSettings["screens"]; + const popConfigToSave = (settings.screens as Record>)[currentKey]; + + // Helper: save popConfig to a single screen's layout-pop + const saveToScreen = async (screenId: number, popConfig: Record) => { + // 1. Get current layout + const layoutRes = await apiClient.get(`/screen-management/screens/${screenId}/layout-pop`).catch(() => null); + const layoutData = layoutRes?.data?.data || { version: "pop-5.0", components: {}, settings: {}, gridConfig: {} }; + + // 2. Update settings.popConfig + layoutData.settings = { + ...(layoutData.settings || {}), + popConfig, + }; + + // 3. Save + await apiClient.post(`/screen-management/screens/${screenId}/layout-pop`, layoutData); + }; + + // Save to the selected screen + await saveToScreen(selectedScreen.screenId, popConfigToSave); + + // If syncToAll is on, save to all other screens with the same settingsKey + if (syncToAll) { + const otherScreens: number[] = []; + for (const group of SCREEN_GROUPS) { + for (const screen of group.screens) { + if (screen.settingsKey === selectedScreen.settingsKey && screen.screenId !== selectedScreen.screenId) { + if (!otherScreens.includes(screen.screenId)) { + otherScreens.push(screen.screenId); + } + } + } + } + await Promise.all(otherScreens.map((sid) => saveToScreen(sid, popConfigToSave))); + } + setHasChanges(false); // Reload iframe to apply settings if (iframeRef.current) { iframeRef.current.contentWindow?.location.reload(); } + alert("설정이 저장되었습니다."); } catch { - localStorage.setItem("pop_settings", JSON.stringify(settings)); - alert("설정이 로컬에 저장되었습니다 (DB 테이블 생성 후 동기화 필요)"); + alert("저장에 실패했습니다."); } setSaving(false); }; @@ -883,13 +917,31 @@ export default function PopSettingsMngPage() { {/* Right: Settings form */}
{selectedScreen && currentFields.length > 0 ? ( - updateScreenSetting(currentSettingsKey, key, value)} - /> + <> + updateScreenSetting(currentSettingsKey, key, value)} + /> + {/* Sync to all screens with same settingsKey */} +
+
+ setSyncToAll(!!checked)} + /> + +
+

+ 저장 시 동일 설정키({currentSettingsKey})를 공유하는 화면에도 설정을 동기화합니다. +

+
+ ) : (
diff --git a/frontend/hooks/pop/usePopSettings.ts b/frontend/hooks/pop/usePopSettings.ts index df180faa..6eafa9cd 100644 --- a/frontend/hooks/pop/usePopSettings.ts +++ b/frontend/hooks/pop/usePopSettings.ts @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import { usePathname } from "next/navigation"; import { apiClient } from "@/lib/api/client"; export interface PopSettings { @@ -109,53 +110,110 @@ const DEFAULT_SETTINGS: PopSettings = { }, }; -let cachedSettings: PopSettings | null = null; +// URL -> screen_id mapping +const POP_SCREEN_MAP: Record = { + "/pop/home": 6526, + "/pop/inbound": 6529, + "/pop/inbound/purchase": 6528, + "/pop/inbound/cart": 6527, + "/pop/outbound": 6, + "/pop/outbound/sales": 5, + "/pop/production": 8, + "/pop/production/process": 7, +}; -export function usePopSettings() { - const [settings, setSettings] = useState(cachedSettings || DEFAULT_SETTINGS); - const [loading, setLoading] = useState(!cachedSettings); +// URL -> settingsKey mapping +const PATH_TO_SETTINGS_KEY: Record = { + "/pop/home": "home", + "/pop/inbound": "inbound", + "/pop/inbound/purchase": "inbound", + "/pop/inbound/cart": "inbound", + "/pop/outbound": "outbound", + "/pop/outbound/sales": "outbound", + "/pop/production": "processExecution", + "/pop/production/process": "processExecution", +}; + +function getScreenIdFromPath(pathname: string): number | null { + // Exact match first + if (POP_SCREEN_MAP[pathname]) return POP_SCREEN_MAP[pathname]; + // Longest-prefix match (e.g. /pop/production/process/xxx -> 7) + const sorted = Object.keys(POP_SCREEN_MAP).sort((a, b) => b.length - a.length); + for (const path of sorted) { + if (pathname.startsWith(path)) return POP_SCREEN_MAP[path]; + } + return null; +} + +function getSettingsKeyFromPath(pathname: string): keyof PopSettings["screens"] | null { + // Exact match first + if (PATH_TO_SETTINGS_KEY[pathname]) return PATH_TO_SETTINGS_KEY[pathname]; + // Longest-prefix match + const sorted = Object.keys(PATH_TO_SETTINGS_KEY).sort((a, b) => b.length - a.length); + for (const path of sorted) { + if (pathname.startsWith(path)) return PATH_TO_SETTINGS_KEY[path]; + } + return null; +} + +// Per-screenId cache to avoid redundant fetches +const screenCache: Record> = {}; + +export function usePopSettings(screenPath?: string) { + const autoPathname = usePathname(); + const pathname = screenPath || autoPathname || ""; + + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [loading, setLoading] = useState(true); const fetchSettings = useCallback(async () => { - if (cachedSettings) { setSettings(cachedSettings); setLoading(false); return; } + const screenId = getScreenIdFromPath(pathname); + const settingsKey = getSettingsKeyFromPath(pathname); + + if (!screenId || !settingsKey) { + setLoading(false); + return; + } + + // Use cache if available + if (screenCache[screenId]) { + const popConfig = screenCache[screenId]; + const merged = { ...DEFAULT_SETTINGS }; + merged.screens = { + ...merged.screens, + [settingsKey]: { ...merged.screens[settingsKey], ...popConfig }, + }; + setSettings(merged); + setLoading(false); + return; + } + try { - // pop_settings 테이블이 없을 수 있으므로 에러 시 기본값 사용 - const res = await apiClient.get("/data/pop_settings", { params: { pageSize: 1 } }).catch(() => null); - if (!res) { setLoading(false); return; } - const rows = res.data?.data?.data || res.data?.data || []; - if (rows.length > 0 && rows[0].settings_data) { - const parsed = typeof rows[0].settings_data === "string" - ? JSON.parse(rows[0].settings_data) : rows[0].settings_data; - const merged = { - ...DEFAULT_SETTINGS, - ...parsed, - screens: { - ...DEFAULT_SETTINGS.screens, - ...parsed.screens, - processExecution: { ...DEFAULT_SETTINGS.screens.processExecution, ...parsed.screens?.processExecution }, - inbound: { ...DEFAULT_SETTINGS.screens.inbound, ...parsed.screens?.inbound }, - outbound: { ...DEFAULT_SETTINGS.screens.outbound, ...parsed.screens?.outbound }, - home: { ...DEFAULT_SETTINGS.screens.home, ...parsed.screens?.home }, - plc: { ...DEFAULT_SETTINGS.screens.plc, ...parsed.screens?.plc }, - }, + const res = await apiClient + .get(`/screen-management/screens/${screenId}/layout-pop`) + .catch(() => null); + + if (res?.data?.data?.settings?.popConfig) { + const popConfig = res.data.data.settings.popConfig; + screenCache[screenId] = popConfig; + + const merged = { ...DEFAULT_SETTINGS }; + merged.screens = { + ...merged.screens, + [settingsKey]: { ...merged.screens[settingsKey], ...popConfig }, }; - cachedSettings = merged; setSettings(merged); } } catch { - // localStorage fallback - const local = localStorage.getItem("pop_settings"); - if (local) { - try { - const parsed = JSON.parse(local); - cachedSettings = { ...DEFAULT_SETTINGS, ...parsed }; - setSettings(cachedSettings); - } catch { /* use default */ } - } + // Use default settings on failure } - setLoading(false); - }, []); - useEffect(() => { fetchSettings(); }, [fetchSettings]); + setLoading(false); + }, [pathname]); + + useEffect(() => { + fetchSettings(); + }, [fetchSettings]); return { settings, loading }; }