feat: POP 설정 저장소를 screen_layouts_pop으로 전환
- usePopSettings: pop_settings 테이블 대신 screen_layouts_pop.popConfig에서 읽기 - 화면별 독립 설정 (URL→screen_id 자동 매핑) - PC 설정 페이지: layout-pop API로 저장/조회 - "같은 유형의 모든 화면에 적용" 동기화 체크박스 - pop_settings 400 에러 완전 제거 - 신규 화면 등록: 판매출고(5), 출고유형(6), 공정실행(7), 생산관리(8)
This commit is contained in:
@@ -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<HTMLIFrameElement>(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<string, Record<string, unknown>>)[key] = {
|
||||
...(merged.screens as Record<string, Record<string, unknown>>)[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<string, Record<string, unknown>>)[currentKey];
|
||||
|
||||
// Helper: save popConfig to a single screen's layout-pop
|
||||
const saveToScreen = async (screenId: number, popConfig: Record<string, unknown>) => {
|
||||
// 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 */}
|
||||
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 border-l bg-background">
|
||||
{selectedScreen && currentFields.length > 0 ? (
|
||||
<SettingsForm
|
||||
screenName={selectedScreen.name}
|
||||
settingsKey={currentSettingsKey}
|
||||
fields={currentFields}
|
||||
values={currentValues}
|
||||
onChange={(key, value) => updateScreenSetting(currentSettingsKey, key, value)}
|
||||
/>
|
||||
<>
|
||||
<SettingsForm
|
||||
screenName={selectedScreen.name}
|
||||
settingsKey={currentSettingsKey}
|
||||
fields={currentFields}
|
||||
values={currentValues}
|
||||
onChange={(key, value) => updateScreenSetting(currentSettingsKey, key, value)}
|
||||
/>
|
||||
{/* Sync to all screens with same settingsKey */}
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="sync-to-all"
|
||||
checked={syncToAll}
|
||||
onCheckedChange={(checked) => setSyncToAll(!!checked)}
|
||||
/>
|
||||
<Label htmlFor="sync-to-all" className="text-sm cursor-pointer">
|
||||
같은 유형의 모든 화면에 적용
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-6">
|
||||
저장 시 동일 설정키({currentSettingsKey})를 공유하는 화면에도 설정을 동기화합니다.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Settings2 className="h-12 w-12 mb-3 opacity-30" />
|
||||
|
||||
@@ -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<string, number> = {
|
||||
"/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<PopSettings>(cachedSettings || DEFAULT_SETTINGS);
|
||||
const [loading, setLoading] = useState(!cachedSettings);
|
||||
// URL -> settingsKey mapping
|
||||
const PATH_TO_SETTINGS_KEY: Record<string, keyof PopSettings["screens"]> = {
|
||||
"/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<number, Record<string, unknown>> = {};
|
||||
|
||||
export function usePopSettings(screenPath?: string) {
|
||||
const autoPathname = usePathname();
|
||||
const pathname = screenPath || autoPathname || "";
|
||||
|
||||
const [settings, setSettings] = useState<PopSettings>(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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user