feat: enhance split panel and tab component interactions
- Improved the drop handling logic for split panels and tabs, prioritizing the innermost container during drag-and-drop operations. - Added internal state management for selected components within split panels to enhance user experience. - Updated the rendering logic to ensure proper data attributes are set for design mode, facilitating better component identification. - Enhanced the dynamic component rendering to support updates and selections for nested components within tabs and split panels. Made-with: Cursor
This commit is contained in:
parent
b1e50f2e0a
commit
014979bebf
|
|
@ -2861,9 +2861,190 @@ export default function ScreenDesigner({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 탭 컨테이너 내부 드롭 처리 (중첩 구조 지원)
|
// 🎯 컨테이너 드롭 우선순위: 가장 안쪽(innermost) 컨테이너 우선
|
||||||
|
// 분할패널과 탭 둘 다 감지될 경우, DOM 트리에서 더 가까운 쪽을 우선 처리
|
||||||
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
||||||
if (tabsContainer) {
|
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||||
|
|
||||||
|
// 분할패널이 탭보다 안쪽에 있으면 분할패널 우선 처리
|
||||||
|
const splitPanelFirst =
|
||||||
|
splitPanelContainer &&
|
||||||
|
(!tabsContainer || tabsContainer.contains(splitPanelContainer));
|
||||||
|
|
||||||
|
if (splitPanelFirst && splitPanelContainer) {
|
||||||
|
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||||
|
const panelSide = splitPanelContainer.getAttribute("data-panel-side");
|
||||||
|
if (containerId && panelSide) {
|
||||||
|
// 분할 패널을 최상위 또는 중첩(탭 안)에서 찾기
|
||||||
|
let targetComponent: any = layout.components.find((c) => c.id === containerId);
|
||||||
|
let parentTabsId: string | null = null;
|
||||||
|
let parentTabId: string | null = null;
|
||||||
|
let parentSplitId: string | null = null;
|
||||||
|
let parentSplitSide: string | null = null;
|
||||||
|
|
||||||
|
if (!targetComponent) {
|
||||||
|
// 탭 안에 중첩된 분할패널 찾기
|
||||||
|
// top-level: overrides.type / overrides.tabs
|
||||||
|
// nested: componentType / componentConfig.tabs
|
||||||
|
for (const comp of layout.components) {
|
||||||
|
const compType = (comp as any)?.componentType || (comp as any)?.overrides?.type;
|
||||||
|
const compConfig = (comp as any)?.componentConfig || (comp as any)?.overrides || {};
|
||||||
|
|
||||||
|
if (compType === "tabs-widget" || compType === "v2-tabs-widget") {
|
||||||
|
const tabs = compConfig.tabs || [];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const found = (tab.components || []).find((c: any) => c.id === containerId);
|
||||||
|
if (found) {
|
||||||
|
targetComponent = found;
|
||||||
|
parentTabsId = comp.id;
|
||||||
|
parentTabId = tab.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") {
|
||||||
|
for (const side of ["leftPanel", "rightPanel"] as const) {
|
||||||
|
const panelComps = compConfig[side]?.components || [];
|
||||||
|
for (const pc of panelComps) {
|
||||||
|
const pct = pc.componentType || pc.overrides?.type;
|
||||||
|
if (pct === "tabs-widget" || pct === "v2-tabs-widget") {
|
||||||
|
const tabs = (pc.componentConfig || pc.overrides || {}).tabs || [];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const found = (tab.components || []).find((c: any) => c.id === containerId);
|
||||||
|
if (found) {
|
||||||
|
targetComponent = found;
|
||||||
|
parentSplitId = comp.id;
|
||||||
|
parentSplitSide = side === "leftPanel" ? "left" : "right";
|
||||||
|
parentTabsId = pc.id;
|
||||||
|
parentTabId = tab.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const compType = (targetComponent as any)?.componentType;
|
||||||
|
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||||
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||||
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||||
|
const panelConfig = currentConfig[panelKey] || {};
|
||||||
|
const currentComponents = panelConfig.components || [];
|
||||||
|
|
||||||
|
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||||
|
const cs1 = window.getComputedStyle(splitPanelContainer);
|
||||||
|
const dropX = (e.clientX - panelRect.left - (parseFloat(cs1.paddingLeft) || 0)) / zoomLevel;
|
||||||
|
const dropY = (e.clientY - panelRect.top - (parseFloat(cs1.paddingTop) || 0)) / zoomLevel;
|
||||||
|
|
||||||
|
const componentType = component.id || component.componentType || "v2-text-display";
|
||||||
|
|
||||||
|
const newPanelComponent = {
|
||||||
|
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
componentType: componentType,
|
||||||
|
label: component.name || component.label || "새 컴포넌트",
|
||||||
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||||
|
size: component.defaultSize || { width: 200, height: 100 },
|
||||||
|
componentConfig: component.defaultConfig || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedPanelConfig = {
|
||||||
|
...panelConfig,
|
||||||
|
components: [...currentComponents, newPanelComponent],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSplitPanel = {
|
||||||
|
...targetComponent,
|
||||||
|
componentConfig: {
|
||||||
|
...currentConfig,
|
||||||
|
[panelKey]: updatedPanelConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let newLayout;
|
||||||
|
if (parentTabsId && parentTabId) {
|
||||||
|
// 중첩: (최상위 분할패널 →) 탭 → 분할패널
|
||||||
|
const updateTabsComponent = (tabsComp: any) => {
|
||||||
|
const ck = tabsComp.componentConfig ? "componentConfig" : "overrides";
|
||||||
|
const cfg = tabsComp[ck] || {};
|
||||||
|
const tabs = cfg.tabs || [];
|
||||||
|
return {
|
||||||
|
...tabsComp,
|
||||||
|
[ck]: {
|
||||||
|
...cfg,
|
||||||
|
tabs: tabs.map((tab: any) =>
|
||||||
|
tab.id === parentTabId
|
||||||
|
? {
|
||||||
|
...tab,
|
||||||
|
components: (tab.components || []).map((c: any) =>
|
||||||
|
c.id === containerId ? updatedSplitPanel : c,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: tab,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parentSplitId && parentSplitSide) {
|
||||||
|
// 최상위 분할패널 → 탭 → 분할패널
|
||||||
|
const pKey = parentSplitSide === "left" ? "leftPanel" : "rightPanel";
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) => {
|
||||||
|
if (c.id === parentSplitId) {
|
||||||
|
const sc = (c as any).componentConfig || {};
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
componentConfig: {
|
||||||
|
...sc,
|
||||||
|
[pKey]: {
|
||||||
|
...sc[pKey],
|
||||||
|
components: (sc[pKey]?.components || []).map((pc: any) =>
|
||||||
|
pc.id === parentTabsId ? updateTabsComponent(pc) : pc,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 최상위 탭 → 분할패널
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) =>
|
||||||
|
c.id === parentTabsId ? updateTabsComponent(c) : c,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 최상위 분할패널
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) => (c.id === containerId ? updatedSplitPanel : c)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(newLayout);
|
||||||
|
saveToHistory(newLayout);
|
||||||
|
toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabsContainer && !splitPanelFirst) {
|
||||||
const containerId = tabsContainer.getAttribute("data-component-id");
|
const containerId = tabsContainer.getAttribute("data-component-id");
|
||||||
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
||||||
if (containerId && activeTabId) {
|
if (containerId && activeTabId) {
|
||||||
|
|
@ -3004,69 +3185,6 @@ export default function ScreenDesigner({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부 드롭 처리
|
|
||||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
|
||||||
if (splitPanelContainer) {
|
|
||||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
|
||||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
|
||||||
if (containerId && panelSide) {
|
|
||||||
const targetComponent = layout.components.find((c) => c.id === containerId);
|
|
||||||
const compType = (targetComponent as any)?.componentType;
|
|
||||||
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
|
||||||
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
||||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
|
||||||
const panelConfig = currentConfig[panelKey] || {};
|
|
||||||
const currentComponents = panelConfig.components || [];
|
|
||||||
|
|
||||||
// 드롭 위치 계산
|
|
||||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
|
||||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
|
||||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
|
||||||
|
|
||||||
// 새 컴포넌트 생성
|
|
||||||
const componentType = component.id || component.componentType || "v2-text-display";
|
|
||||||
|
|
||||||
console.log("🎯 분할 패널에 컴포넌트 드롭:", {
|
|
||||||
componentId: component.id,
|
|
||||||
componentType: componentType,
|
|
||||||
panelSide: panelSide,
|
|
||||||
dropPosition: { x: dropX, y: dropY },
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPanelComponent = {
|
|
||||||
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
componentType: componentType,
|
|
||||||
label: component.name || component.label || "새 컴포넌트",
|
|
||||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
|
||||||
size: component.defaultSize || { width: 200, height: 100 },
|
|
||||||
componentConfig: component.defaultConfig || {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedPanelConfig = {
|
|
||||||
...panelConfig,
|
|
||||||
components: [...currentComponents, newPanelComponent],
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedComponent = {
|
|
||||||
...targetComponent,
|
|
||||||
componentConfig: {
|
|
||||||
...currentConfig,
|
|
||||||
[panelKey]: updatedPanelConfig,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const newLayout = {
|
|
||||||
...layout,
|
|
||||||
components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)),
|
|
||||||
};
|
|
||||||
|
|
||||||
setLayout(newLayout);
|
|
||||||
saveToHistory(newLayout);
|
|
||||||
toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
|
||||||
return; // 분할 패널 처리 완료
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = canvasRef.current?.getBoundingClientRect();
|
const rect = canvasRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
|
|
@ -3378,15 +3496,12 @@ export default function ScreenDesigner({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const dragData = e.dataTransfer.getData("application/json");
|
const dragData = e.dataTransfer.getData("application/json");
|
||||||
// console.log("🎯 드롭 이벤트:", { dragData });
|
|
||||||
if (!dragData) {
|
if (!dragData) {
|
||||||
// console.log("❌ 드래그 데이터가 없습니다");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(dragData);
|
const parsedData = JSON.parse(dragData);
|
||||||
// console.log("📋 파싱된 데이터:", parsedData);
|
|
||||||
|
|
||||||
// 템플릿 드래그인 경우
|
// 템플릿 드래그인 경우
|
||||||
if (parsedData.type === "template") {
|
if (parsedData.type === "template") {
|
||||||
|
|
@ -3480,9 +3595,225 @@ export default function ScreenDesigner({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 탭 컨테이너 내부에 컬럼 드롭 시 처리 (중첩 구조 지원)
|
// 🎯 컨테이너 감지: innermost 우선 (분할패널 > 탭)
|
||||||
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
||||||
if (tabsContainer && type === "column" && column) {
|
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||||
|
|
||||||
|
// 분할패널이 탭 안에 있으면 분할패널이 innermost → 분할패널 우선
|
||||||
|
const splitPanelFirst =
|
||||||
|
splitPanelContainer &&
|
||||||
|
(!tabsContainer || tabsContainer.contains(splitPanelContainer));
|
||||||
|
|
||||||
|
// 🎯 분할패널 커스텀 모드 컨테이너 내부에 컬럼 드롭 시 처리 (우선 처리)
|
||||||
|
if (splitPanelFirst && splitPanelContainer && type === "column" && column) {
|
||||||
|
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||||
|
let panelSide = splitPanelContainer.getAttribute("data-panel-side");
|
||||||
|
|
||||||
|
// panelSide가 없으면 드롭 좌표와 splitRatio로 좌/우 판별
|
||||||
|
if (!panelSide) {
|
||||||
|
const splitRatio = parseInt(splitPanelContainer.getAttribute("data-split-ratio") || "40", 10);
|
||||||
|
const containerRect = splitPanelContainer.getBoundingClientRect();
|
||||||
|
const relativeX = e.clientX - containerRect.left;
|
||||||
|
const splitPoint = containerRect.width * (splitRatio / 100);
|
||||||
|
panelSide = relativeX < splitPoint ? "left" : "right";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerId && panelSide) {
|
||||||
|
// 최상위에서 찾기
|
||||||
|
let targetComponent: any = layout.components.find((c) => c.id === containerId);
|
||||||
|
let parentTabsId: string | null = null;
|
||||||
|
let parentTabId: string | null = null;
|
||||||
|
let parentSplitId: string | null = null;
|
||||||
|
let parentSplitSide: string | null = null;
|
||||||
|
|
||||||
|
if (!targetComponent) {
|
||||||
|
// 탭 안 중첩 분할패널 찾기
|
||||||
|
// top-level 컴포넌트: overrides.type / overrides.tabs
|
||||||
|
// nested 컴포넌트: componentType / componentConfig.tabs
|
||||||
|
for (const comp of layout.components) {
|
||||||
|
const ct = (comp as any)?.componentType || (comp as any)?.overrides?.type;
|
||||||
|
const compConfig = (comp as any)?.componentConfig || (comp as any)?.overrides || {};
|
||||||
|
|
||||||
|
if (ct === "tabs-widget" || ct === "v2-tabs-widget") {
|
||||||
|
const tabs = compConfig.tabs || [];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const found = (tab.components || []).find((c: any) => c.id === containerId);
|
||||||
|
if (found) {
|
||||||
|
targetComponent = found;
|
||||||
|
parentTabsId = comp.id;
|
||||||
|
parentTabId = tab.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
// 분할패널 → 탭 → 분할패널 중첩
|
||||||
|
if (ct === "split-panel-layout" || ct === "v2-split-panel-layout") {
|
||||||
|
for (const side of ["leftPanel", "rightPanel"] as const) {
|
||||||
|
const panelComps = compConfig[side]?.components || [];
|
||||||
|
for (const pc of panelComps) {
|
||||||
|
const pct = pc.componentType || pc.overrides?.type;
|
||||||
|
if (pct === "tabs-widget" || pct === "v2-tabs-widget") {
|
||||||
|
const tabs = (pc.componentConfig || pc.overrides || {}).tabs || [];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const found = (tab.components || []).find((c: any) => c.id === containerId);
|
||||||
|
if (found) {
|
||||||
|
targetComponent = found;
|
||||||
|
parentSplitId = comp.id;
|
||||||
|
parentSplitSide = side === "leftPanel" ? "left" : "right";
|
||||||
|
parentTabsId = pc.id;
|
||||||
|
parentTabId = tab.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
if (targetComponent) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compType = (targetComponent as any)?.componentType;
|
||||||
|
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||||
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||||
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||||
|
const panelConfig = currentConfig[panelKey] || {};
|
||||||
|
const currentComponents = panelConfig.components || [];
|
||||||
|
|
||||||
|
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||||
|
const computedStyle = window.getComputedStyle(splitPanelContainer);
|
||||||
|
const padLeft = parseFloat(computedStyle.paddingLeft) || 0;
|
||||||
|
const padTop = parseFloat(computedStyle.paddingTop) || 0;
|
||||||
|
const dropX = (e.clientX - panelRect.left - padLeft) / zoomLevel;
|
||||||
|
const dropY = (e.clientY - panelRect.top - padTop) / zoomLevel;
|
||||||
|
|
||||||
|
const v2Mapping = createV2ConfigFromColumn({
|
||||||
|
widgetType: column.widgetType,
|
||||||
|
columnName: column.columnName,
|
||||||
|
columnLabel: column.columnLabel,
|
||||||
|
codeCategory: column.codeCategory,
|
||||||
|
inputType: column.inputType,
|
||||||
|
required: column.required,
|
||||||
|
detailSettings: column.detailSettings,
|
||||||
|
referenceTable: column.referenceTable,
|
||||||
|
referenceColumn: column.referenceColumn,
|
||||||
|
displayColumn: column.displayColumn,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPanelComponent = {
|
||||||
|
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
componentType: v2Mapping.componentType,
|
||||||
|
label: column.columnLabel || column.columnName,
|
||||||
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||||
|
size: { width: 200, height: 36 },
|
||||||
|
inputType: column.inputType || column.widgetType,
|
||||||
|
widgetType: column.widgetType,
|
||||||
|
componentConfig: {
|
||||||
|
...v2Mapping.componentConfig,
|
||||||
|
columnName: column.columnName,
|
||||||
|
tableName: column.tableName,
|
||||||
|
inputType: column.inputType || column.widgetType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSplitPanel = {
|
||||||
|
...targetComponent,
|
||||||
|
componentConfig: {
|
||||||
|
...currentConfig,
|
||||||
|
[panelKey]: {
|
||||||
|
...panelConfig,
|
||||||
|
displayMode: "custom",
|
||||||
|
components: [...currentComponents, newPanelComponent],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let newLayout;
|
||||||
|
|
||||||
|
if (parentSplitId && parentSplitSide && parentTabsId && parentTabId) {
|
||||||
|
// 분할패널 → 탭 → 분할패널 3중 중첩
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) => {
|
||||||
|
if (c.id !== parentSplitId) return c;
|
||||||
|
const sc = (c as any).componentConfig || {};
|
||||||
|
const pk = parentSplitSide === "left" ? "leftPanel" : "rightPanel";
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
componentConfig: {
|
||||||
|
...sc,
|
||||||
|
[pk]: {
|
||||||
|
...sc[pk],
|
||||||
|
components: (sc[pk]?.components || []).map((pc: any) => {
|
||||||
|
if (pc.id !== parentTabsId) return pc;
|
||||||
|
return {
|
||||||
|
...pc,
|
||||||
|
componentConfig: {
|
||||||
|
...pc.componentConfig,
|
||||||
|
tabs: (pc.componentConfig?.tabs || []).map((tab: any) => {
|
||||||
|
if (tab.id !== parentTabId) return tab;
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
components: (tab.components || []).map((tc: any) =>
|
||||||
|
tc.id === containerId ? updatedSplitPanel : tc,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else if (parentTabsId && parentTabId) {
|
||||||
|
// 탭 → 분할패널 2중 중첩
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) => {
|
||||||
|
if (c.id !== parentTabsId) return c;
|
||||||
|
// top-level은 overrides, nested는 componentConfig
|
||||||
|
const configKey = (c as any).componentConfig ? "componentConfig" : "overrides";
|
||||||
|
const tabsConfig = (c as any)[configKey] || {};
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
[configKey]: {
|
||||||
|
...tabsConfig,
|
||||||
|
tabs: (tabsConfig.tabs || []).map((tab: any) => {
|
||||||
|
if (tab.id !== parentTabId) return tab;
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
components: (tab.components || []).map((tc: any) =>
|
||||||
|
tc.id === containerId ? updatedSplitPanel : tc,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 최상위 분할패널
|
||||||
|
newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: layout.components.map((c) => (c.id === containerId ? updatedSplitPanel : c)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("컬럼이 분할패널에 추가되었습니다");
|
||||||
|
setLayout(newLayout);
|
||||||
|
saveToHistory(newLayout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 탭 컨테이너 내부에 컬럼 드롭 시 처리 (중첩 구조 지원)
|
||||||
|
if (tabsContainer && !splitPanelFirst && type === "column" && column) {
|
||||||
const containerId = tabsContainer.getAttribute("data-component-id");
|
const containerId = tabsContainer.getAttribute("data-component-id");
|
||||||
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
||||||
if (containerId && activeTabId) {
|
if (containerId && activeTabId) {
|
||||||
|
|
@ -3648,9 +3979,8 @@ export default function ScreenDesigner({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부에 컬럼 드롭 시 처리
|
// 🎯 분할 패널 커스텀 모드 (탭 밖 최상위) 컬럼 드롭 처리
|
||||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
if (splitPanelContainer && !splitPanelFirst && type === "column" && column) {
|
||||||
if (splitPanelContainer && type === "column" && column) {
|
|
||||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
||||||
if (containerId && panelSide) {
|
if (containerId && panelSide) {
|
||||||
|
|
@ -3662,12 +3992,11 @@ export default function ScreenDesigner({
|
||||||
const panelConfig = currentConfig[panelKey] || {};
|
const panelConfig = currentConfig[panelKey] || {};
|
||||||
const currentComponents = panelConfig.components || [];
|
const currentComponents = panelConfig.components || [];
|
||||||
|
|
||||||
// 드롭 위치 계산
|
|
||||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
const cs2 = window.getComputedStyle(splitPanelContainer);
|
||||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
const dropX = (e.clientX - panelRect.left - (parseFloat(cs2.paddingLeft) || 0)) / zoomLevel;
|
||||||
|
const dropY = (e.clientY - panelRect.top - (parseFloat(cs2.paddingTop) || 0)) / zoomLevel;
|
||||||
|
|
||||||
// V2 컴포넌트 매핑 사용
|
|
||||||
const v2Mapping = createV2ConfigFromColumn({
|
const v2Mapping = createV2ConfigFromColumn({
|
||||||
widgetType: column.widgetType,
|
widgetType: column.widgetType,
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
GeneratedLocation,
|
GeneratedLocation,
|
||||||
RackStructureContext,
|
RackStructureContext,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { applyLocationPattern, DEFAULT_CODE_PATTERN, DEFAULT_NAME_PATTERN } from "./patternUtils";
|
|
||||||
|
|
||||||
// 기존 위치 데이터 타입
|
// 기존 위치 데이터 타입
|
||||||
interface ExistingLocation {
|
interface ExistingLocation {
|
||||||
|
|
@ -513,27 +512,23 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
return { totalLocations, totalRows, maxLevel };
|
return { totalLocations, totalRows, maxLevel };
|
||||||
}, [conditions]);
|
}, [conditions]);
|
||||||
|
|
||||||
// 위치 코드 생성 (패턴 기반)
|
// 위치 코드 생성
|
||||||
const generateLocationCode = useCallback(
|
const generateLocationCode = useCallback(
|
||||||
(row: number, level: number): { code: string; name: string } => {
|
(row: number, level: number): { code: string; name: string } => {
|
||||||
const vars = {
|
const warehouseCode = context?.warehouseCode || "WH001";
|
||||||
warehouse: context?.warehouseCode || "WH001",
|
const floor = context?.floor || "1";
|
||||||
warehouseName: context?.warehouseName || "",
|
const zone = context?.zone || "A";
|
||||||
floor: context?.floor || "1",
|
|
||||||
zone: context?.zone || "A",
|
|
||||||
row,
|
|
||||||
level,
|
|
||||||
};
|
|
||||||
|
|
||||||
const codePattern = config.codePattern || DEFAULT_CODE_PATTERN;
|
// 코드 생성 (예: WH001-1층D구역-01-1)
|
||||||
const namePattern = config.namePattern || DEFAULT_NAME_PATTERN;
|
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
||||||
|
|
||||||
return {
|
// 이름 생성 - zone에 이미 "구역"이 포함되어 있으면 그대로 사용
|
||||||
code: applyLocationPattern(codePattern, vars),
|
const zoneName = zone.includes("구역") ? zone : `${zone}구역`;
|
||||||
name: applyLocationPattern(namePattern, vars),
|
const name = `${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`;
|
||||||
};
|
|
||||||
|
return { code, name };
|
||||||
},
|
},
|
||||||
[context, config.codePattern, config.namePattern],
|
[context],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 미리보기 생성
|
// 미리보기 생성
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
@ -12,47 +12,6 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { RackStructureComponentConfig, FieldMapping } from "./types";
|
import { RackStructureComponentConfig, FieldMapping } from "./types";
|
||||||
import { applyLocationPattern, DEFAULT_CODE_PATTERN, DEFAULT_NAME_PATTERN, PATTERN_VARIABLES } from "./patternUtils";
|
|
||||||
|
|
||||||
// 패턴 미리보기 서브 컴포넌트
|
|
||||||
const PatternPreview: React.FC<{
|
|
||||||
codePattern?: string;
|
|
||||||
namePattern?: string;
|
|
||||||
}> = ({ codePattern, namePattern }) => {
|
|
||||||
const sampleVars = {
|
|
||||||
warehouse: "WH002",
|
|
||||||
warehouseName: "2창고",
|
|
||||||
floor: "2층",
|
|
||||||
zone: "A구역",
|
|
||||||
row: 1,
|
|
||||||
level: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewCode = useMemo(
|
|
||||||
() => applyLocationPattern(codePattern || DEFAULT_CODE_PATTERN, sampleVars),
|
|
||||||
[codePattern],
|
|
||||||
);
|
|
||||||
const previewName = useMemo(
|
|
||||||
() => applyLocationPattern(namePattern || DEFAULT_NAME_PATTERN, sampleVars),
|
|
||||||
[namePattern],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-primary/20 bg-primary/5 p-2.5">
|
|
||||||
<div className="mb-1.5 text-[10px] font-medium text-primary">미리보기 (2창고 / 2층 / A구역 / 1열 / 3단)</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span className="w-14 shrink-0 text-muted-foreground">위치코드:</span>
|
|
||||||
<code className="rounded bg-background px-1.5 py-0.5 font-mono text-foreground">{previewCode}</code>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span className="w-14 shrink-0 text-muted-foreground">위치명:</span>
|
|
||||||
<code className="rounded bg-background px-1.5 py-0.5 font-mono text-foreground">{previewName}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RackStructureConfigPanelProps {
|
interface RackStructureConfigPanelProps {
|
||||||
config: RackStructureComponentConfig;
|
config: RackStructureComponentConfig;
|
||||||
|
|
@ -246,61 +205,6 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위치코드 패턴 설정 */}
|
|
||||||
<div className="space-y-3 border-t pt-3">
|
|
||||||
<div className="text-sm font-medium text-foreground">위치코드/위치명 패턴</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
변수를 조합하여 위치코드와 위치명 생성 규칙을 설정하세요
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* 위치코드 패턴 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">위치코드 패턴</Label>
|
|
||||||
<Input
|
|
||||||
value={config.codePattern || ""}
|
|
||||||
onChange={(e) => handleChange("codePattern", e.target.value || undefined)}
|
|
||||||
placeholder="{warehouse}-{floor}{zone}-{row:02}-{level}"
|
|
||||||
className="h-8 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
||||||
비워두면 기본값: {"{warehouse}-{floor}{zone}-{row:02}-{level}"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 위치명 패턴 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">위치명 패턴</Label>
|
|
||||||
<Input
|
|
||||||
value={config.namePattern || ""}
|
|
||||||
onChange={(e) => handleChange("namePattern", e.target.value || undefined)}
|
|
||||||
placeholder="{zone}-{row:02}열-{level}단"
|
|
||||||
className="h-8 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
||||||
비워두면 기본값: {"{zone}-{row:02}열-{level}단"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 실시간 미리보기 */}
|
|
||||||
<PatternPreview
|
|
||||||
codePattern={config.codePattern}
|
|
||||||
namePattern={config.namePattern}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 사용 가능한 변수 목록 */}
|
|
||||||
<div className="rounded-md border bg-muted/50 p-2">
|
|
||||||
<div className="mb-1 text-[10px] font-medium text-foreground">사용 가능한 변수</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
|
|
||||||
{PATTERN_VARIABLES.map((v) => (
|
|
||||||
<div key={v.token} className="flex items-center gap-1 text-[10px]">
|
|
||||||
<code className="rounded bg-primary/10 px-1 font-mono text-primary">{v.token}</code>
|
|
||||||
<span className="text-muted-foreground">{v.description}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 제한 설정 */}
|
{/* 제한 설정 */}
|
||||||
<div className="space-y-3 border-t pt-3">
|
<div className="space-y-3 border-t pt-3">
|
||||||
<div className="text-sm font-medium text-foreground">제한 설정</div>
|
<div className="text-sm font-medium text-foreground">제한 설정</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
// rack-structure는 v2-rack-structure의 patternUtils를 재사용
|
|
||||||
export {
|
|
||||||
applyLocationPattern,
|
|
||||||
DEFAULT_CODE_PATTERN,
|
|
||||||
DEFAULT_NAME_PATTERN,
|
|
||||||
PATTERN_VARIABLES,
|
|
||||||
} from "../v2-rack-structure/patternUtils";
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
GeneratedLocation,
|
GeneratedLocation,
|
||||||
RackStructureContext,
|
RackStructureContext,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { applyLocationPattern, DEFAULT_CODE_PATTERN, DEFAULT_NAME_PATTERN } from "./patternUtils";
|
|
||||||
|
|
||||||
// 기존 위치 데이터 타입
|
// 기존 위치 데이터 타입
|
||||||
interface ExistingLocation {
|
interface ExistingLocation {
|
||||||
|
|
@ -494,27 +493,23 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
return { totalLocations, totalRows, maxLevel };
|
return { totalLocations, totalRows, maxLevel };
|
||||||
}, [conditions]);
|
}, [conditions]);
|
||||||
|
|
||||||
// 위치 코드 생성 (패턴 기반)
|
// 위치 코드 생성
|
||||||
const generateLocationCode = useCallback(
|
const generateLocationCode = useCallback(
|
||||||
(row: number, level: number): { code: string; name: string } => {
|
(row: number, level: number): { code: string; name: string } => {
|
||||||
const vars = {
|
const warehouseCode = context?.warehouseCode || "WH001";
|
||||||
warehouse: context?.warehouseCode || "WH001",
|
const floor = context?.floor || "1";
|
||||||
warehouseName: context?.warehouseName || "",
|
const zone = context?.zone || "A";
|
||||||
floor: context?.floor || "1",
|
|
||||||
zone: context?.zone || "A",
|
|
||||||
row,
|
|
||||||
level,
|
|
||||||
};
|
|
||||||
|
|
||||||
const codePattern = config.codePattern || DEFAULT_CODE_PATTERN;
|
// 코드 생성 (예: WH001-1층D구역-01-1)
|
||||||
const namePattern = config.namePattern || DEFAULT_NAME_PATTERN;
|
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
||||||
|
|
||||||
return {
|
// 이름 생성 - zone에 이미 "구역"이 포함되어 있으면 그대로 사용
|
||||||
code: applyLocationPattern(codePattern, vars),
|
const zoneName = zone.includes("구역") ? zone : `${zone}구역`;
|
||||||
name: applyLocationPattern(namePattern, vars),
|
const name = `${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`;
|
||||||
};
|
|
||||||
|
return { code, name };
|
||||||
},
|
},
|
||||||
[context, config.codePattern, config.namePattern],
|
[context],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 미리보기 생성
|
// 미리보기 생성
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
@ -12,47 +12,6 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { RackStructureComponentConfig, FieldMapping } from "./types";
|
import { RackStructureComponentConfig, FieldMapping } from "./types";
|
||||||
import { applyLocationPattern, DEFAULT_CODE_PATTERN, DEFAULT_NAME_PATTERN, PATTERN_VARIABLES } from "./patternUtils";
|
|
||||||
|
|
||||||
// 패턴 미리보기 서브 컴포넌트
|
|
||||||
const PatternPreview: React.FC<{
|
|
||||||
codePattern?: string;
|
|
||||||
namePattern?: string;
|
|
||||||
}> = ({ codePattern, namePattern }) => {
|
|
||||||
const sampleVars = {
|
|
||||||
warehouse: "WH002",
|
|
||||||
warehouseName: "2창고",
|
|
||||||
floor: "2층",
|
|
||||||
zone: "A구역",
|
|
||||||
row: 1,
|
|
||||||
level: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewCode = useMemo(
|
|
||||||
() => applyLocationPattern(codePattern || DEFAULT_CODE_PATTERN, sampleVars),
|
|
||||||
[codePattern],
|
|
||||||
);
|
|
||||||
const previewName = useMemo(
|
|
||||||
() => applyLocationPattern(namePattern || DEFAULT_NAME_PATTERN, sampleVars),
|
|
||||||
[namePattern],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-primary/20 bg-primary/5 p-2.5">
|
|
||||||
<div className="mb-1.5 text-[10px] font-medium text-primary">미리보기 (2창고 / 2층 / A구역 / 1열 / 3단)</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span className="w-14 shrink-0 text-muted-foreground">위치코드:</span>
|
|
||||||
<code className="rounded bg-background px-1.5 py-0.5 font-mono text-foreground">{previewCode}</code>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span className="w-14 shrink-0 text-muted-foreground">위치명:</span>
|
|
||||||
<code className="rounded bg-background px-1.5 py-0.5 font-mono text-foreground">{previewName}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RackStructureConfigPanelProps {
|
interface RackStructureConfigPanelProps {
|
||||||
config: RackStructureComponentConfig;
|
config: RackStructureComponentConfig;
|
||||||
|
|
@ -246,61 +205,6 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위치코드 패턴 설정 */}
|
|
||||||
<div className="space-y-3 border-t pt-3">
|
|
||||||
<div className="text-sm font-medium text-foreground">위치코드/위치명 패턴</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
변수를 조합하여 위치코드와 위치명 생성 규칙을 설정하세요
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* 위치코드 패턴 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">위치코드 패턴</Label>
|
|
||||||
<Input
|
|
||||||
value={config.codePattern || ""}
|
|
||||||
onChange={(e) => handleChange("codePattern", e.target.value || undefined)}
|
|
||||||
placeholder="{warehouse}-{floor}{zone}-{row:02}-{level}"
|
|
||||||
className="h-8 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
||||||
비워두면 기본값: {"{warehouse}-{floor}{zone}-{row:02}-{level}"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 위치명 패턴 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">위치명 패턴</Label>
|
|
||||||
<Input
|
|
||||||
value={config.namePattern || ""}
|
|
||||||
onChange={(e) => handleChange("namePattern", e.target.value || undefined)}
|
|
||||||
placeholder="{zone}-{row:02}열-{level}단"
|
|
||||||
className="h-8 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
||||||
비워두면 기본값: {"{zone}-{row:02}열-{level}단"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 실시간 미리보기 */}
|
|
||||||
<PatternPreview
|
|
||||||
codePattern={config.codePattern}
|
|
||||||
namePattern={config.namePattern}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 사용 가능한 변수 목록 */}
|
|
||||||
<div className="rounded-md border bg-muted/50 p-2">
|
|
||||||
<div className="mb-1 text-[10px] font-medium text-foreground">사용 가능한 변수</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
|
|
||||||
{PATTERN_VARIABLES.map((v) => (
|
|
||||||
<div key={v.token} className="flex items-center gap-1 text-[10px]">
|
|
||||||
<code className="rounded bg-primary/10 px-1 font-mono text-primary">{v.token}</code>
|
|
||||||
<span className="text-muted-foreground">{v.description}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 제한 설정 */}
|
{/* 제한 설정 */}
|
||||||
<div className="space-y-3 border-t pt-3">
|
<div className="space-y-3 border-t pt-3">
|
||||||
<div className="text-sm font-medium text-foreground">제한 설정</div>
|
<div className="text-sm font-medium text-foreground">제한 설정</div>
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
/**
|
|
||||||
* 위치코드/위치명 패턴 변환 유틸리티
|
|
||||||
*
|
|
||||||
* 사용 가능한 변수:
|
|
||||||
* {warehouse} - 창고 코드 (예: WH002)
|
|
||||||
* {warehouseName} - 창고명 (예: 2창고)
|
|
||||||
* {floor} - 층 (예: 2층)
|
|
||||||
* {zone} - 구역 (예: A구역)
|
|
||||||
* {row} - 열 번호 (예: 1)
|
|
||||||
* {row:02} - 열 번호 2자리 (예: 01)
|
|
||||||
* {row:03} - 열 번호 3자리 (예: 001)
|
|
||||||
* {level} - 단 번호 (예: 1)
|
|
||||||
* {level:02} - 단 번호 2자리 (예: 01)
|
|
||||||
* {level:03} - 단 번호 3자리 (예: 001)
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface PatternVariables {
|
|
||||||
warehouse?: string;
|
|
||||||
warehouseName?: string;
|
|
||||||
floor?: string;
|
|
||||||
zone?: string;
|
|
||||||
row: number;
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기본 패턴 (하드코딩 대체)
|
|
||||||
export const DEFAULT_CODE_PATTERN = "{warehouse}-{floor}{zone}-{row:02}-{level}";
|
|
||||||
export const DEFAULT_NAME_PATTERN = "{zone}-{row:02}열-{level}단";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 패턴 문자열에서 변수를 치환하여 결과 문자열 반환
|
|
||||||
*/
|
|
||||||
export function applyLocationPattern(pattern: string, vars: PatternVariables): string {
|
|
||||||
let result = pattern;
|
|
||||||
|
|
||||||
// zone에 "구역" 포함 여부에 따른 처리 없이 있는 그대로 치환
|
|
||||||
const simpleVars: Record<string, string | undefined> = {
|
|
||||||
warehouse: vars.warehouse,
|
|
||||||
warehouseName: vars.warehouseName,
|
|
||||||
floor: vars.floor,
|
|
||||||
zone: vars.zone,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 단순 문자열 변수 치환
|
|
||||||
for (const [key, value] of Object.entries(simpleVars)) {
|
|
||||||
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 숫자 변수 (row, level) - zero-pad 지원
|
|
||||||
const numericVars: Record<string, number> = {
|
|
||||||
row: vars.row,
|
|
||||||
level: vars.level,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(numericVars)) {
|
|
||||||
// {row:02}, {level:03} 같은 zero-pad 패턴
|
|
||||||
const padRegex = new RegExp(`\\{${key}:(\\d+)\\}`, "g");
|
|
||||||
result = result.replace(padRegex, (_, padWidth) => {
|
|
||||||
return value.toString().padStart(parseInt(padWidth), "0");
|
|
||||||
});
|
|
||||||
|
|
||||||
// {row}, {level} 같은 단순 패턴
|
|
||||||
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 패턴에서 사용 가능한 변수 목록
|
|
||||||
export const PATTERN_VARIABLES = [
|
|
||||||
{ token: "{warehouse}", description: "창고 코드", example: "WH002" },
|
|
||||||
{ token: "{warehouseName}", description: "창고명", example: "2창고" },
|
|
||||||
{ token: "{floor}", description: "층", example: "2층" },
|
|
||||||
{ token: "{zone}", description: "구역", example: "A구역" },
|
|
||||||
{ token: "{row}", description: "열 번호", example: "1" },
|
|
||||||
{ token: "{row:02}", description: "열 번호 (2자리)", example: "01" },
|
|
||||||
{ token: "{row:03}", description: "열 번호 (3자리)", example: "001" },
|
|
||||||
{ token: "{level}", description: "단 번호", example: "1" },
|
|
||||||
{ token: "{level:02}", description: "단 번호 (2자리)", example: "01" },
|
|
||||||
{ token: "{level:03}", description: "단 번호 (3자리)", example: "001" },
|
|
||||||
];
|
|
||||||
|
|
@ -271,8 +271,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
||||||
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
|
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
|
||||||
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
|
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
|
||||||
// 🆕 외부에서 전달받은 선택 상태 사용 (탭 컴포넌트와 동일 구조)
|
// 내부 선택 상태 (외부 prop 없을 때 fallback)
|
||||||
const selectedPanelComponentId = externalSelectedPanelComponentId || null;
|
const [internalSelectedCompId, setInternalSelectedCompId] = useState<string | null>(null);
|
||||||
|
const selectedPanelComponentId = externalSelectedPanelComponentId ?? internalSelectedCompId;
|
||||||
// 🆕 커스텀 모드: 분할패널 내 탭 컴포넌트의 선택 상태 관리
|
// 🆕 커스텀 모드: 분할패널 내 탭 컴포넌트의 선택 상태 관리
|
||||||
const [nestedTabSelectedCompId, setNestedTabSelectedCompId] = useState<string | undefined>(undefined);
|
const [nestedTabSelectedCompId, setNestedTabSelectedCompId] = useState<string | undefined>(undefined);
|
||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
|
|
@ -3052,9 +3053,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CardContent className="flex-1 overflow-auto p-4">
|
<CardContent
|
||||||
|
className="flex-1 overflow-auto p-4"
|
||||||
|
{...(isDesignMode ? {
|
||||||
|
"data-split-panel-container": "true",
|
||||||
|
"data-component-id": component.id,
|
||||||
|
"data-panel-side": "left",
|
||||||
|
} : {})}
|
||||||
|
>
|
||||||
{/* 좌측 데이터 목록/테이블/커스텀 */}
|
{/* 좌측 데이터 목록/테이블/커스텀 */}
|
||||||
{console.log("🔍 [SplitPanel] 왼쪽 패널 displayMode:", componentConfig.leftPanel?.displayMode, "isDesignMode:", isDesignMode)}
|
|
||||||
{componentConfig.leftPanel?.displayMode === "custom" ? (
|
{componentConfig.leftPanel?.displayMode === "custom" ? (
|
||||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||||
<div
|
<div
|
||||||
|
|
@ -3158,10 +3165,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 패널 컴포넌트 선택 시 탭 내 선택 해제
|
|
||||||
if (comp.componentType !== "v2-tabs-widget") {
|
if (comp.componentType !== "v2-tabs-widget") {
|
||||||
setNestedTabSelectedCompId(undefined);
|
setNestedTabSelectedCompId(undefined);
|
||||||
}
|
}
|
||||||
|
setInternalSelectedCompId(comp.id);
|
||||||
onSelectPanelComponent?.("left", comp.id, comp);
|
onSelectPanelComponent?.("left", comp.id, comp);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -3917,7 +3924,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CardContent className="flex-1 overflow-auto p-4">
|
<CardContent
|
||||||
|
className="flex-1 overflow-auto p-4"
|
||||||
|
{...(isDesignMode ? {
|
||||||
|
"data-split-panel-container": "true",
|
||||||
|
"data-component-id": component.id,
|
||||||
|
"data-panel-side": "right",
|
||||||
|
} : {})}
|
||||||
|
>
|
||||||
{/* 추가 탭 컨텐츠 */}
|
{/* 추가 탭 컨텐츠 */}
|
||||||
{activeTabIndex > 0 ? (
|
{activeTabIndex > 0 ? (
|
||||||
(() => {
|
(() => {
|
||||||
|
|
@ -4289,6 +4303,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
if (comp.componentType !== "v2-tabs-widget") {
|
if (comp.componentType !== "v2-tabs-widget") {
|
||||||
setNestedTabSelectedCompId(undefined);
|
setNestedTabSelectedCompId(undefined);
|
||||||
}
|
}
|
||||||
|
setInternalSelectedCompId(comp.id);
|
||||||
onSelectPanelComponent?.("right", comp.id, comp);
|
onSelectPanelComponent?.("right", comp.id, comp);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -417,12 +417,39 @@ const TabsDesignEditor: React.FC<{
|
||||||
width: displayWidth,
|
width: displayWidth,
|
||||||
height: displayHeight,
|
height: displayHeight,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
>
|
>
|
||||||
<div className="h-full w-full pointer-events-none">
|
<div className={cn(
|
||||||
|
"h-full w-full",
|
||||||
|
comp.componentType !== "v2-split-panel-layout" && comp.componentType !== "split-panel-layout" && "pointer-events-none"
|
||||||
|
)}>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={componentData as any}
|
component={componentData as any}
|
||||||
isDesignMode={true}
|
isDesignMode={true}
|
||||||
formData={{}}
|
formData={{}}
|
||||||
|
{...(comp.componentType === "v2-split-panel-layout" || comp.componentType === "split-panel-layout" ? {
|
||||||
|
onUpdateComponent: (updated: any) => {
|
||||||
|
if (!onUpdateComponent) return;
|
||||||
|
const updatedTabs = tabs.map((t) => {
|
||||||
|
if (t.id !== activeTabId) return t;
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
components: (t.components || []).map((c) =>
|
||||||
|
c.id === comp.id ? { ...c, componentConfig: updated.componentConfig || updated.overrides || c.componentConfig } : c
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const configKey = component.componentConfig ? "componentConfig" : "overrides";
|
||||||
|
const existingConfig = component[configKey] || {};
|
||||||
|
onUpdateComponent({
|
||||||
|
...component,
|
||||||
|
[configKey]: { ...existingConfig, tabs: updatedTabs },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSelectPanelComponent: (panelSide: string, compId: string, panelComp: any) => {
|
||||||
|
onSelectTabComponent?.(activeTabId, comp.id, { ...comp, _selectedPanelSide: panelSide, _selectedPanelCompId: compId, _selectedPanelComp: panelComp } as any);
|
||||||
|
},
|
||||||
|
} : {})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue