diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index fbcf8243..4a693867 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useEffect, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; @@ -16,7 +16,7 @@ import { useAuth } from "@/hooks/useAuth"; import { uploadFilesAndCreateData } from "@/lib/api/file"; import { toast } from "sonner"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; -import { CascadingDropdownConfig } from "@/types/screen-management"; +import { CascadingDropdownConfig, LayerDefinition } from "@/types/screen-management"; import { ComponentData, WidgetComponent, @@ -164,6 +164,8 @@ interface InteractiveScreenViewerProps { enableAutoSave?: boolean; showToastMessages?: boolean; }; + // ๐Ÿ†• ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ์ง€์› + layers?: LayerDefinition[]; } export const InteractiveScreenViewer: React.FC = ({ @@ -178,6 +180,7 @@ export const InteractiveScreenViewer: React.FC = ( tableColumns = [], showValidationPanel = false, validationOptions = {}, + layers = [], // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ชฉ๋ก }) => { // component๊ฐ€ ์—†์œผ๋ฉด ๋นˆ div ๋ฐ˜ํ™˜ if (!component) { @@ -206,9 +209,71 @@ export const InteractiveScreenViewer: React.FC = ( // ํŒ์—… ์ „์šฉ formData ์ƒํƒœ const [popupFormData, setPopupFormData] = useState>({}); + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ƒํƒœ ๊ด€๋ฆฌ (๋Ÿฐํƒ€์ž„์šฉ) + const [activeLayerIds, setActiveLayerIds] = useState([]); + + // ๐Ÿ†• ์ดˆ๊ธฐ ๋ ˆ์ด์–ด ์„ค์ • (visible์ธ ๋ ˆ์ด์–ด๋“ค) + useEffect(() => { + if (layers.length > 0) { + const initialActiveLayers = layers.filter((l) => l.isVisible).map((l) => l.id); + setActiveLayerIds(initialActiveLayers); + } + }, [layers]); + + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ œ์–ด ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ + const handleLayerAction = useCallback((action: string, layerId: string) => { + setActiveLayerIds((prev) => { + switch (action) { + case "show": + return [...new Set([...prev, layerId])]; + case "hide": + return prev.filter((id) => id !== layerId); + case "toggle": + return prev.includes(layerId) + ? prev.filter((id) => id !== layerId) + : [...prev, layerId]; + case "exclusive": + // ํ•ด๋‹น ๋ ˆ์ด์–ด๋งŒ ํ‘œ์‹œ (๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ๊ฐ™์€ ํŠน์ˆ˜ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ์— ํ™œ์šฉ) + return [...prev, layerId]; + default: + return prev; + } + }); + }, []); + // ํ†ตํ•ฉ๋œ ํผ ๋ฐ์ดํ„ฐ const finalFormData = { ...localFormData, ...externalFormData }; + // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ๋กœ์ง (formData ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ํ‰๊ฐ€) + useEffect(() => { + layers.forEach((layer) => { + if (layer.type === "conditional" && layer.condition) { + const { targetComponentId, operator, value } = layer.condition; + // ์ปดํฌ๋„ŒํŠธ ID๋ฅผ ํ‚ค๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ - columnName ๋งคํ•‘์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ + const targetValue = finalFormData[targetComponentId]; + + let isMatch = false; + switch (operator) { + case "eq": + isMatch = targetValue == value; + break; + case "neq": + isMatch = targetValue != value; + break; + case "in": + isMatch = Array.isArray(value) && value.includes(targetValue); + break; + } + + if (isMatch) { + handleLayerAction("show", layer.id); + } else { + handleLayerAction("hide", layer.id); + } + } + }); + }, [finalFormData, layers, handleLayerAction]); + // ๊ฐœ์„ ๋œ ๊ฒ€์ฆ ์‹œ์Šคํ…œ (์„ ํƒ์  ํ™œ์„ฑํ™”) const enhancedValidation = enableEnhancedValidation && screenInfo && tableColumns.length > 0 ? useFormValidation( @@ -1395,7 +1460,6 @@ export const InteractiveScreenViewer: React.FC = ( > = ( ))} - , + ); } @@ -2124,6 +2188,159 @@ export const InteractiveScreenViewer: React.FC = ( } : component; + // ๐Ÿ†• ๋ ˆ์ด์–ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ•จ์ˆ˜ + const renderLayerComponents = useCallback((layer: LayerDefinition) => { + // ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์€ ๋ ˆ์ด์–ด๋Š” ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ + if (!activeLayerIds.includes(layer.id)) return null; + + // ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ + if (layer.type === "modal") { + const modalStyle: React.CSSProperties = { + ...(layer.overlayConfig?.backgroundColor && { backgroundColor: layer.overlayConfig.backgroundColor }), + ...(layer.overlayConfig?.backdropBlur && { backdropFilter: `blur(${layer.overlayConfig.backdropBlur}px)` }), + }; + + return ( + handleLayerAction("hide", layer.id)}> + + + {layer.name} + +
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+
+
+ ); + } + + // ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ + if (layer.type === "drawer") { + const drawerPosition = layer.overlayConfig?.position || "right"; + const drawerWidth = layer.overlayConfig?.width || "400px"; + const drawerHeight = layer.overlayConfig?.height || "100%"; + + const drawerPositionStyles: Record = { + right: { right: 0, top: 0, width: drawerWidth, height: "100%" }, + left: { left: 0, top: 0, width: drawerWidth, height: "100%" }, + bottom: { bottom: 0, left: 0, width: "100%", height: drawerHeight }, + top: { top: 0, left: 0, width: "100%", height: drawerHeight }, + }; + + return ( +
handleLayerAction("hide", layer.id)} + > + {/* ๋ฐฑ๋“œ๋กญ */} +
+ {/* ๋“œ๋กœ์–ด ํŒจ๋„ */} +
e.stopPropagation()} + > +
+

{layer.name}

+ +
+
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+
+
+ ); + } + + // ์ผ๋ฐ˜/์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด (base, conditional) + return ( +
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+ ); + }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo]); + return ( @@ -2147,6 +2364,9 @@ export const InteractiveScreenViewer: React.FC = (
+ {/* ๐Ÿ†• ๋ ˆ์ด์–ด ๋ Œ๋”๋ง */} + {layers.length > 0 && layers.map(renderLayerComponents)} + {/* ๊ฐœ์„ ๋œ ๊ฒ€์ฆ ํŒจ๋„ (์„ ํƒ์  ํ‘œ์‹œ) */} {showValidationPanel && enhancedValidation && (
diff --git a/frontend/components/screen/LayerManagerPanel.tsx b/frontend/components/screen/LayerManagerPanel.tsx new file mode 100644 index 00000000..cd482602 --- /dev/null +++ b/frontend/components/screen/LayerManagerPanel.tsx @@ -0,0 +1,331 @@ +import React, { useState, useMemo } from "react"; +import { useLayer } from "@/contexts/LayerContext"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { + Eye, + EyeOff, + Lock, + Unlock, + Plus, + Trash2, + GripVertical, + Layers, + SplitSquareVertical, + PanelRight, + ChevronDown, + Settings2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { LayerType, LayerDefinition, ComponentData } from "@/types/screen-management"; + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜ +const getLayerTypeIcon = (type: LayerType) => { + switch (type) { + case "base": + return ; + case "conditional": + return ; + case "modal": + return ; + case "drawer": + return ; + default: + return ; + } +}; + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ๋ผ๋ฒจ +function getLayerTypeLabel(type: LayerType): string { + switch (type) { + case "base": + return "๊ธฐ๋ณธ"; + case "conditional": + return "์กฐ๊ฑด๋ถ€"; + case "modal": + return "๋ชจ๋‹ฌ"; + case "drawer": + return "๋“œ๋กœ์–ด"; + default: + return type; + } +} + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ์ƒ‰์ƒ +function getLayerTypeColor(type: LayerType): string { + switch (type) { + case "base": + return "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"; + case "conditional": + return "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300"; + case "modal": + return "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300"; + case "drawer": + return "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"; + default: + return "bg-gray-100 text-gray-700 dark:bg-gray-900 dark:text-gray-300"; + } +} + +interface LayerItemProps { + layer: LayerDefinition; + isActive: boolean; + componentCount: number; // ๐Ÿ†• ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ์ˆ˜ (layout.components ๊ธฐ๋ฐ˜) + onSelect: () => void; + onToggleVisibility: () => void; + onToggleLock: () => void; + onRemove: () => void; + onUpdateName: (name: string) => void; +} + +const LayerItem: React.FC = ({ + layer, + isActive, + componentCount, + onSelect, + onToggleVisibility, + onToggleLock, + onRemove, + onUpdateName, +}) => { + const [isEditing, setIsEditing] = useState(false); + + return ( +
+ {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค */} + + + {/* ๋ ˆ์ด์–ด ์ •๋ณด */} +
+
+ {/* ๋ ˆ์ด์–ด ํƒ€์ž… ์•„์ด์ฝ˜ */} + + {getLayerTypeIcon(layer.type)} + + + {/* ๋ ˆ์ด์–ด ์ด๋ฆ„ */} + {isEditing ? ( + onUpdateName(e.target.value)} + onBlur={() => setIsEditing(false)} + onKeyDown={(e) => { + if (e.key === "Enter") setIsEditing(false); + }} + className="flex-1 bg-transparent outline-none border-b border-primary text-sm" + autoFocus + onClick={(e) => e.stopPropagation()} + /> + ) : ( + { + e.stopPropagation(); + setIsEditing(true); + }} + > + {layer.name} + + )} +
+ + {/* ๋ ˆ์ด์–ด ๋ฉ”ํƒ€ ์ •๋ณด */} +
+ + {getLayerTypeLabel(layer.type)} + + + {componentCount}๊ฐœ ์ปดํฌ๋„ŒํŠธ + +
+
+ + {/* ์•ก์…˜ ๋ฒ„ํŠผ๋“ค */} +
+ + + + + {layer.type !== "base" && ( + + )} +
+
+ ); +}; + +interface LayerManagerPanelProps { + components?: ComponentData[]; // layout.components๋ฅผ ์ „๋‹ฌ๋ฐ›์Œ +} + +export const LayerManagerPanel: React.FC = ({ components = [] }) => { + const { + layers, + activeLayerId, + setActiveLayerId, + addLayer, + removeLayer, + toggleLayerVisibility, + toggleLayerLock, + updateLayer, + } = useLayer(); + + // ๐Ÿ†• ๊ฐ ๋ ˆ์ด์–ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ๊ณ„์‚ฐ (layout.components ๊ธฐ๋ฐ˜) + const componentCountByLayer = useMemo(() => { + const counts: Record = {}; + + // ๋ชจ๋“  ๋ ˆ์ด์–ด๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™” + layers.forEach(layer => { + counts[layer.id] = 0; + }); + + // layout.components์—์„œ layerId๋ณ„๋กœ ์นด์šดํŠธ + components.forEach(comp => { + const layerId = comp.layerId || "default-layer"; + if (counts[layerId] !== undefined) { + counts[layerId]++; + } else { + // layerId๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ ˆ์ด์–ด์ธ ๊ฒฝ์šฐ default-layer๋กœ ์นด์šดํŠธ + if (counts["default-layer"] !== undefined) { + counts["default-layer"]++; + } + } + }); + + return counts; + }, [components, layers]); + + return ( +
+ {/* ํ—ค๋” */} +
+
+ +

๋ ˆ์ด์–ด

+ + {layers.length} + +
+ + {/* ๋ ˆ์ด์–ด ์ถ”๊ฐ€ ๋“œ๋กญ๋‹ค์šด */} + + + + + + addLayer("conditional", "์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด")}> + + ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด + + + addLayer("modal", "๋ชจ๋‹ฌ ๋ ˆ์ด์–ด")}> + + ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด + + addLayer("drawer", "๋“œ๋กœ์–ด ๋ ˆ์ด์–ด")}> + + ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด + + + +
+ + {/* ๋ ˆ์ด์–ด ๋ชฉ๋ก */} + +
+ {layers.length === 0 ? ( +
+ ๋ ˆ์ด์–ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +
+ ์œ„์˜ + ๋ฒ„ํŠผ์œผ๋กœ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +
+ ) : ( + layers + .slice() + .reverse() // ์ƒ์œ„ ๋ ˆ์ด์–ด๊ฐ€ ์œ„์— ํ‘œ์‹œ + .map((layer) => ( + setActiveLayerId(layer.id)} + onToggleVisibility={() => toggleLayerVisibility(layer.id)} + onToggleLock={() => toggleLayerLock(layer.id)} + onRemove={() => removeLayer(layer.id)} + onUpdateName={(name) => updateLayer(layer.id, { name })} + /> + )) + )} +
+
+ + {/* ๋„์›€๋ง */} +
+

๋”๋ธ”ํด๋ฆญ: ์ด๋ฆ„ ํŽธ์ง‘ | ๋“œ๋ž˜๊ทธ: ์ˆœ์„œ ๋ณ€๊ฒฝ

+
+
+ ); +}; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index c6ad7437..b4255d00 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -123,9 +123,12 @@ interface ScreenDesignerProps { onScreenUpdate?: (updatedScreen: Partial) => void; } -// ํŒจ๋„ ์„ค์ • (ํ†ตํ•ฉ ํŒจ๋„ 1๊ฐœ) +import { useLayerOptional, LayerProvider, createDefaultLayer } from "@/contexts/LayerContext"; +import { LayerManagerPanel } from "./LayerManagerPanel"; +import { LayerType, LayerDefinition } from "@/types/screen-management"; + +// ํŒจ๋„ ์„ค์ • ์—…๋ฐ์ดํŠธ const panelConfigs: PanelConfig[] = [ - // ํ†ตํ•ฉ ํŒจ๋„ (์ปดํฌ๋„ŒํŠธ + ํŽธ์ง‘ ํƒญ) { id: "v2", title: "ํŒจ๋„", @@ -134,12 +137,17 @@ const panelConfigs: PanelConfig[] = [ defaultHeight: 700, shortcutKey: "p", }, + { + id: "layer", + title: "๋ ˆ์ด์–ด", + defaultPosition: "right", + defaultWidth: 240, + defaultHeight: 500, + shortcutKey: "l", + }, ]; export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenUpdate }: ScreenDesignerProps) { - // ํŒจ๋„ ์ƒํƒœ ๊ด€๋ฆฌ - const { panelStates, togglePanel, openPanel, closePanel } = usePanelState(panelConfigs); - const [layout, setLayout] = useState({ components: [], gridSettings: { @@ -171,6 +179,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU SCREEN_RESOLUTIONS[0], // ๊ธฐ๋ณธ๊ฐ’: Full HD ); + // ๐Ÿ†• ํŒจ๋„ ์ƒํƒœ ๊ด€๋ฆฌ (usePanelState ํ›…) + const { panelStates, togglePanel, openPanel, closePanel, closeAllPanels, updatePanelPosition, updatePanelSize } = + usePanelState(panelConfigs); + const [selectedComponent, setSelectedComponent] = useState(null); // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ƒํƒœ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) @@ -438,6 +450,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const [tables, setTables] = useState([]); const [searchTerm, setSearchTerm] = useState(""); + // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก + const filteredTables = useMemo(() => { + if (!searchTerm.trim()) return tables; + const term = searchTerm.toLowerCase(); + return tables.filter( + (table) => + table.tableName.toLowerCase().includes(term) || + table.columns?.some((col) => col.columnName.toLowerCase().includes(term)), + ); + }, [tables, searchTerm]); + // ๊ทธ๋ฃน ์ƒ์„ฑ ๋‹ค์ด์–ผ๋กœ๊ทธ const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false); @@ -462,15 +485,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return lines; }, [layout.gridSettings?.showGrid, screenResolution.width, screenResolution.height]); - // ํ•„ํ„ฐ๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก - const filteredTables = useMemo(() => { - if (!searchTerm) return tables; - return tables.filter( - (table) => - table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || - table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())), - ); - }, [tables, searchTerm]); + // ๐Ÿ†• ๋ ˆ์ด์–ด ํ™œ์„ฑ ์ƒํƒœ ๊ด€๋ฆฌ (LayerProvider ์™ธ๋ถ€์—์„œ ๊ด€๋ฆฌ) + const [activeLayerId, setActiveLayerIdLocal] = useState("default-layer"); + + // ์บ”๋ฒ„์Šค์— ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง (๋ ˆ์ด์–ด ๊ธฐ๋ฐ˜) + // ํ™œ์„ฑ ๋ ˆ์ด์–ด๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ‘œ์‹œ + // layerId๊ฐ€ ์—†๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์–ด("default-layer")์— ์†ํ•œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ + const visibleComponents = useMemo(() => { + // ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ™œ์„ฑ ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ํ‘œ์‹œ + if (!activeLayerId) { + return layout.components; + } + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ•„ํ„ฐ๋ง + return layout.components.filter((comp) => { + // layerId๊ฐ€ ์—†๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์–ด("default-layer")์— ์†ํ•œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ + const compLayerId = comp.layerId || "default-layer"; + return compLayerId === activeLayerId; + }); + }, [layout.components, activeLayerId]); // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก ๊ณ„์‚ฐ const placedColumns = useMemo(() => { @@ -1798,9 +1831,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์„ ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋กœ ์ €์žฅ const currentMainTableName = tables.length > 0 ? tables[0].tableName : null; + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ •๋ณด๋„ ํ•จ๊ป˜ ์ €์žฅ (๋ ˆ์ด์–ด๊ฐ€ ์žˆ์œผ๋ฉด ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์—…๋ฐ์ดํŠธ) + const updatedLayers = layout.layers?.map((layer) => ({ + ...layer, + components: layer.components.map((comp) => { + // ๋ถ„ํ•  ํŒจ๋„ ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ ์šฉ + const updatedComp = updatedComponents.find((uc) => uc.id === comp.id); + return updatedComp || comp; + }), + })); + const layoutWithResolution = { ...layout, components: updatedComponents, + layers: updatedLayers, // ๐Ÿ†• ๋ ˆ์ด์–ด ์ •๋ณด ํฌํ•จ screenResolution: screenResolution, mainTableName: currentMainTableName, // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” }; @@ -2339,23 +2383,29 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } }); + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ + const componentsWithLayerId = newComponents.map((comp) => ({ + ...comp, + layerId: activeLayerId || "default-layer", + })); + // ๋ ˆ์ด์•„์›ƒ์— ์ƒˆ ์ปดํฌ๋„ŒํŠธ๋“ค ์ถ”๊ฐ€ const newLayout = { ...layout, - components: [...layout.components, ...newComponents], + components: [...layout.components, ...componentsWithLayerId], }; setLayout(newLayout); saveToHistory(newLayout); // ์ฒซ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ - if (newComponents.length > 0) { - setSelectedComponent(newComponents[0]); + if (componentsWithLayerId.length > 0) { + setSelectedComponent(componentsWithLayerId[0]); } toast.success(`${template.name} ํ…œํ”Œ๋ฆฟ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, selectedScreen, saveToHistory], + [layout, selectedScreen, saveToHistory, activeLayerId], ); // ๋ ˆ์ด์•„์›ƒ ๋“œ๋ž˜๊ทธ ์ฒ˜๋ฆฌ @@ -2409,6 +2459,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU label: layoutData.label, allowedComponentTypes: layoutData.allowedComponentTypes, dropZoneConfig: layoutData.dropZoneConfig, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ } as ComponentData; // ๋ ˆ์ด์•„์›ƒ์— ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ @@ -2425,7 +2476,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU toast.success(`${layoutData.label} ๋ ˆ์ด์•„์›ƒ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, screenResolution, saveToHistory, zoomLevel], + [layout, screenResolution, saveToHistory, zoomLevel, activeLayerId], ); // handleZoneComponentDrop์€ handleComponentDrop์œผ๋กœ ๋Œ€์ฒด๋จ @@ -3016,6 +3067,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU position: snappedPosition, size: componentSize, gridColumns: gridColumns, // ์ปดํฌ๋„ŒํŠธ๋ณ„ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ์ ์šฉ + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ componentConfig: { type: component.id, // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์˜ ID ์‚ฌ์šฉ webType: component.webType, // ์›นํƒ€์ž… ์ •๋ณด ์ถ”๊ฐ€ @@ -3049,7 +3101,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU toast.success(`${component.name} ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, selectedScreen, saveToHistory], + [layout, selectedScreen, saveToHistory, activeLayerId], ); // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ฒ˜๋ฆฌ @@ -3421,6 +3473,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tableName: table.tableName, position: { x, y, z: 1 } as Position, size: { width: 300, height: 200 }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ style: { labelDisplay: true, labelFontSize: "14px", @@ -3671,6 +3724,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU componentType: v2Mapping.componentType, // v2-input, v2-select ๋“ฑ position: { x: relativeX, y: relativeY, z: 1 } as Position, size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -3737,6 +3791,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU componentType: v2Mapping.componentType, // v2-input, v2-select ๋“ฑ position: { x, y, z: 1 } as Position, size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -4388,7 +4443,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU bottom: Math.max(selectionDrag.startPoint.y, currentPoint.y), }; - const selectedIds = layout.components + // ๐Ÿ†• visibleComponents๋งŒ ์„ ํƒ ๋Œ€์ƒ์œผ๋กœ (ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ) + const selectedIds = visibleComponents .filter((comp) => { const compRect = { left: comp.position.x, @@ -4411,7 +4467,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU selectedComponents: selectedIds, })); }, - [selectionDrag.isSelecting, selectionDrag.startPoint, layout.components, zoomLevel], + [selectionDrag.isSelecting, selectionDrag.startPoint, visibleComponents, zoomLevel], ); // ๋“œ๋ž˜๊ทธ ์„ ํƒ ์ข…๋ฃŒ @@ -4558,6 +4614,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU z: clipComponent.position.z || 1, } as Position, parentId: undefined, // ๋ถ™์—ฌ๋„ฃ๊ธฐ ์‹œ ๋ถ€๋ชจ ๊ด€๊ณ„ ํ•ด์ œ + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ๋ถ™์—ฌ๋„ฃ๊ธฐ }; newComponents.push(newComponent); }); @@ -4578,7 +4635,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // console.log("์ปดํฌ๋„ŒํŠธ ๋ถ™์—ฌ๋„ฃ๊ธฐ ์™„๋ฃŒ:", newComponents.length, "๊ฐœ"); toast.success(`${newComponents.length}๊ฐœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ถ™์—ฌ๋„ฃ์–ด์กŒ์Šต๋‹ˆ๋‹ค.`); - }, [clipboard, layout, saveToHistory]); + }, [clipboard, layout, saveToHistory, activeLayerId]); // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ์ƒ์„ฑ (๋‹ค์ค‘ ์„ ํƒ๋œ ๋ฒ„ํŠผ๋“ค์„ ํ•œ ๋ฒˆ์— ๊ทธ๋ฃน์œผ๋กœ) // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒํƒœ @@ -5374,6 +5431,36 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; }, [layout, selectedComponent]); + // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ - ๋ ˆ์ด์–ด ์ปจํ…์ŠคํŠธ์—์„œ ๋ ˆ์ด์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด layout์—๋„ ๋ฐ˜์˜ + // ์ฃผ์˜: layout.components๋Š” layerId ์†์„ฑ์œผ๋กœ ๋ ˆ์ด์–ด๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ฏ€๋กœ, ์—ฌ๊ธฐ์„œ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ + const handleLayersChange = useCallback((newLayers: LayerDefinition[]) => { + setLayout((prevLayout) => ({ + ...prevLayout, + layers: newLayers, + // components๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ - layerId ์†์„ฑ์œผ๋กœ ๋ ˆ์ด์–ด ๊ตฌ๋ถ„ + // components: prevLayout.components (๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์œ ์ง€๋จ) + })); + }, []); + + // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleActiveLayerChange = useCallback((newActiveLayerId: string | null) => { + setActiveLayerIdLocal(newActiveLayerId); + }, []); + + // ๐Ÿ†• ์ดˆ๊ธฐ ๋ ˆ์ด์–ด ๊ณ„์‚ฐ - layout์—์„œ layers๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ + // ์ฃผ์˜: components๋Š” layout.components์— layerId ์†์„ฑ์œผ๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ, layer.components๋Š” ๋น„์›Œ๋‘  + const initialLayers = useMemo(() => { + if (layout.layers && layout.layers.length > 0) { + // ๊ธฐ์กด ๋ ˆ์ด์–ด ๊ตฌ์กฐ ์‚ฌ์šฉ (layer.components๋Š” ๋ฌด์‹œํ•˜๊ณ  ๋นˆ ๋ฐฐ์—ด๋กœ ์„ค์ •) + return layout.layers.map(layer => ({ + ...layer, + components: [], // layout.components + layerId ๋ฐฉ์‹ ์‚ฌ์šฉ + })); + } + // layers๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ (components๋Š” ๋นˆ ๋ฐฐ์—ด) + return [createDefaultLayer()]; + }, [layout.layers]); + if (!selectedScreen) { return (
@@ -5393,7 +5480,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return ( - + +
{/* ์ƒ๋‹จ ์Šฌ๋ฆผ ํˆด๋ฐ” */}
- + ์ปดํฌ๋„ŒํŠธ + + ๋ ˆ์ด์–ด + ํŽธ์ง‘ @@ -5457,6 +5552,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU /> + {/* ๐Ÿ†• ๋ ˆ์ด์–ด ๊ด€๋ฆฌ ํƒญ */} + + + + {/* ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ์—๋„ V2PropertiesPanel ์‚ฌ์šฉ */} {selectedTabComponentInfo ? ( @@ -6088,7 +6188,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU {/* ์ปดํฌ๋„ŒํŠธ๋“ค */} {(() => { // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ๊ฐ์ง€ ๋ฐ ์ฒ˜๋ฆฌ - const topLevelComponents = layout.components.filter((component) => !component.parentId); + // visibleComponents๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ‘œ์‹œ + const topLevelComponents = visibleComponents.filter((component) => !component.parentId); // auto-compact ๋ชจ๋“œ์˜ ๋ฒ„ํŠผ๋“ค์„ ๊ทธ๋ฃน๋ณ„๋กœ ๋ฌถ๊ธฐ const buttonGroups: Record = {}; @@ -6740,6 +6841,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU />
+ ); } diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index 9c949514..cb6547c5 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -208,17 +208,14 @@ export const V2PropertiesPanel: React.FC = ({ if (componentId?.startsWith("v2-")) { const v2ConfigPanels: Record void }>> = { "v2-input": require("@/components/v2/config-panels/V2InputConfigPanel").V2InputConfigPanel, - "v2-select": require("@/components/v2/config-panels/V2SelectConfigPanel") - .V2SelectConfigPanel, + "v2-select": require("@/components/v2/config-panels/V2SelectConfigPanel").V2SelectConfigPanel, "v2-date": require("@/components/v2/config-panels/V2DateConfigPanel").V2DateConfigPanel, "v2-list": require("@/components/v2/config-panels/V2ListConfigPanel").V2ListConfigPanel, - "v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel") - .V2LayoutConfigPanel, + "v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel").V2LayoutConfigPanel, "v2-group": require("@/components/v2/config-panels/V2GroupConfigPanel").V2GroupConfigPanel, "v2-media": require("@/components/v2/config-panels/V2MediaConfigPanel").V2MediaConfigPanel, "v2-biz": require("@/components/v2/config-panels/V2BizConfigPanel").V2BizConfigPanel, - "v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel") - .V2HierarchyConfigPanel, + "v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel").V2HierarchyConfigPanel, }; const V2ConfigPanel = v2ConfigPanels[componentId]; @@ -823,7 +820,11 @@ export const V2PropertiesPanel: React.FC = ({
{ handleUpdate("style.labelText", e.target.value); handleUpdate("label", e.target.value); // label๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ @@ -870,10 +871,7 @@ export const V2PropertiesPanel: React.FC = ({ handleUpdate("labelDisplay", boolValue); // labelText๋„ ์„ค์ • (์ฒ˜์Œ ์ผค ๋•Œ ๋ผ๋ฒจ ํ…์ŠคํŠธ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ) if (boolValue && !selectedComponent.style?.labelText) { - const labelValue = - selectedComponent.label || - selectedComponent.componentConfig?.label || - ""; + const labelValue = selectedComponent.label || selectedComponent.componentConfig?.label || ""; if (labelValue) { handleUpdate("style.labelText", labelValue); } @@ -963,8 +961,7 @@ export const V2PropertiesPanel: React.FC = ({ } // ๐Ÿ†• 3.5. V2 ์ปดํฌ๋„ŒํŠธ - ๋ฐ˜๋“œ์‹œ ๋‹ค๋ฅธ ์ฒดํฌ๋ณด๋‹ค ๋จผ์ € ์ฒ˜๋ฆฌ - const v2ComponentType = - (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || ""; + const v2ComponentType = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || ""; if (v2ComponentType.startsWith("v2-")) { const configPanel = renderComponentConfigPanel(); if (configPanel) { diff --git a/frontend/contexts/LayerContext.tsx b/frontend/contexts/LayerContext.tsx new file mode 100644 index 00000000..6e0f67cd --- /dev/null +++ b/frontend/contexts/LayerContext.tsx @@ -0,0 +1,337 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo } from "react"; +import { LayerDefinition, LayerType, ComponentData } from "@/types/screen-management"; +import { v4 as uuidv4 } from "uuid"; + +interface LayerContextType { + // ๋ ˆ์ด์–ด ์ƒํƒœ + layers: LayerDefinition[]; + activeLayerId: string | null; + activeLayer: LayerDefinition | null; + + // ๋ ˆ์ด์–ด ๊ด€๋ฆฌ + setLayers: (layers: LayerDefinition[]) => void; + setActiveLayerId: (id: string | null) => void; + addLayer: (type: LayerType, name?: string) => void; + removeLayer: (id: string) => void; + updateLayer: (id: string, updates: Partial) => void; + moveLayer: (dragIndex: number, hoverIndex: number) => void; + toggleLayerVisibility: (id: string) => void; + toggleLayerLock: (id: string) => void; + getLayerById: (id: string) => LayerDefinition | undefined; + + // ์ปดํฌ๋„ŒํŠธ ๊ด€๋ฆฌ (๋ ˆ์ด์–ด๋ณ„) + addComponentToLayer: (layerId: string, component: ComponentData) => void; + removeComponentFromLayer: (layerId: string, componentId: string) => void; + updateComponentInLayer: (layerId: string, componentId: string, updates: Partial) => void; + moveComponentToLayer: (componentId: string, fromLayerId: string, toLayerId: string) => void; + + // ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ + getAllComponents: () => ComponentData[]; + getComponentById: (componentId: string) => { component: ComponentData; layerId: string } | null; + getComponentsInActiveLayer: () => ComponentData[]; + + // ๋ ˆ์ด์–ด ๊ฐ€์‹œ์„ฑ (๋Ÿฐํƒ€์ž„์šฉ) + runtimeVisibleLayers: string[]; + setRuntimeVisibleLayers: React.Dispatch>; + showLayer: (layerId: string) => void; + hideLayer: (layerId: string) => void; + toggleLayerRuntime: (layerId: string) => void; +} + +const LayerContext = createContext(undefined); + +export const useLayer = () => { + const context = useContext(LayerContext); + if (!context) { + throw new Error("useLayer must be used within a LayerProvider"); + } + return context; +}; + +// LayerProvider๊ฐ€ ์—†์„ ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ์ปจํ…์ŠคํŠธ (์„ ํƒ์  ์‚ฌ์šฉ) +export const useLayerOptional = () => { + return useContext(LayerContext); +}; + +interface LayerProviderProps { + children: ReactNode; + initialLayers?: LayerDefinition[]; + onLayersChange?: (layers: LayerDefinition[]) => void; + onActiveLayerChange?: (activeLayerId: string | null) => void; // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ +} + +// ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ ํ—ฌํผ +export const createDefaultLayer = (components?: ComponentData[]): LayerDefinition => ({ + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: components || [], +}); + +export const LayerProvider: React.FC = ({ + children, + initialLayers = [], + onLayersChange, + onActiveLayerChange, +}) => { + // ์ดˆ๊ธฐ ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ + const effectiveInitialLayers = initialLayers.length > 0 + ? initialLayers + : [createDefaultLayer()]; + + const [layers, setLayersState] = useState(effectiveInitialLayers); + const [activeLayerIdState, setActiveLayerIdState] = useState( + effectiveInitialLayers.length > 0 ? effectiveInitialLayers[0].id : null, + ); + + // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์‹œ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + const setActiveLayerId = useCallback((id: string | null) => { + setActiveLayerIdState(id); + onActiveLayerChange?.(id); + }, [onActiveLayerChange]); + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด ID (๋‚ด๋ถ€ ์ƒํƒœ ์‚ฌ์šฉ) + const activeLayerId = activeLayerIdState; + + // ๋Ÿฐํƒ€์ž„ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ (ํŽธ์ง‘๊ธฐ์—์„œ์˜ isVisible๊ณผ ๋ณ„๊ฐœ) + const [runtimeVisibleLayers, setRuntimeVisibleLayers] = useState( + effectiveInitialLayers.filter(l => l.isVisible).map(l => l.id) + ); + + // ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์‹œ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + const setLayers = useCallback((newLayers: LayerDefinition[]) => { + setLayersState(newLayers); + onLayersChange?.(newLayers); + }, [onLayersChange]); + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด ๊ณ„์‚ฐ + const activeLayer = useMemo(() => { + return layers.find(l => l.id === activeLayerId) || null; + }, [layers, activeLayerId]); + + const addLayer = useCallback( + (type: LayerType, name?: string) => { + const newLayer: LayerDefinition = { + id: uuidv4(), + name: name || `์ƒˆ ๋ ˆ์ด์–ด ${layers.length + 1}`, + type, + zIndex: layers.length, + isVisible: true, + isLocked: false, + components: [], + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ๊ธฐ๋ณธ ์„ค์ • + ...(type === "modal" || type === "drawer" ? { + overlayConfig: { + backdrop: true, + closeOnBackdropClick: true, + width: type === "drawer" ? "320px" : "600px", + height: type === "drawer" ? "100%" : "auto", + }, + } : {}), + }; + + setLayers([...layers, newLayer]); + setActiveLayerId(newLayer.id); + // ์ƒˆ ๋ ˆ์ด์–ด๋Š” ๋Ÿฐํƒ€์ž„์—์„œ๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ‘œ์‹œ + setRuntimeVisibleLayers(prev => [...prev, newLayer.id]); + }, + [layers, setLayers], + ); + + const removeLayer = useCallback( + (id: string) => { + // ๊ธฐ๋ณธ ๋ ˆ์ด์–ด๋Š” ์‚ญ์ œ ๋ถˆ๊ฐ€ + const layer = layers.find(l => l.id === id); + if (layer?.type === "base") { + console.warn("๊ธฐ๋ณธ ๋ ˆ์ด์–ด๋Š” ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + const filtered = layers.filter((layer) => layer.id !== id); + setLayers(filtered); + + if (activeLayerId === id) { + setActiveLayerId(filtered.length > 0 ? filtered[0].id : null); + } + + setRuntimeVisibleLayers(prev => prev.filter(lid => lid !== id)); + }, + [layers, activeLayerId, setLayers], + ); + + const updateLayer = useCallback((id: string, updates: Partial) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, ...updates } : layer))); + }, [layers, setLayers]); + + const moveLayer = useCallback((dragIndex: number, hoverIndex: number) => { + const newLayers = [...layers]; + const [removed] = newLayers.splice(dragIndex, 1); + newLayers.splice(hoverIndex, 0, removed); + // Update zIndex based on new order + setLayers(newLayers.map((layer, index) => ({ ...layer, zIndex: index }))); + }, [layers, setLayers]); + + const toggleLayerVisibility = useCallback((id: string) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isVisible: !layer.isVisible } : layer))); + }, [layers, setLayers]); + + const toggleLayerLock = useCallback((id: string) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isLocked: !layer.isLocked } : layer))); + }, [layers, setLayers]); + + const getLayerById = useCallback( + (id: string) => { + return layers.find((layer) => layer.id === id); + }, + [layers], + ); + + // ===== ์ปดํฌ๋„ŒํŠธ ๊ด€๋ฆฌ ํ•จ์ˆ˜ ===== + + const addComponentToLayer = useCallback((layerId: string, component: ComponentData) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: [...layer.components, component], + }; + } + return layer; + })); + }, [layers, setLayers]); + + const removeComponentFromLayer = useCallback((layerId: string, componentId: string) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: layer.components.filter(c => c.id !== componentId), + }; + } + return layer; + })); + }, [layers, setLayers]); + + const updateComponentInLayer = useCallback((layerId: string, componentId: string, updates: Partial) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: layer.components.map(c => + c.id === componentId ? { ...c, ...updates } as ComponentData : c + ), + }; + } + return layer; + })); + }, [layers, setLayers]); + + const moveComponentToLayer = useCallback((componentId: string, fromLayerId: string, toLayerId: string) => { + if (fromLayerId === toLayerId) return; + + const fromLayer = layers.find(l => l.id === fromLayerId); + const component = fromLayer?.components.find(c => c.id === componentId); + + if (!component) return; + + setLayers(layers.map(layer => { + if (layer.id === fromLayerId) { + return { + ...layer, + components: layer.components.filter(c => c.id !== componentId), + }; + } + if (layer.id === toLayerId) { + return { + ...layer, + components: [...layer.components, component], + }; + } + return layer; + })); + }, [layers, setLayers]); + + // ===== ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ ํ•จ์ˆ˜ ===== + + const getAllComponents = useCallback((): ComponentData[] => { + return layers.flatMap(layer => layer.components); + }, [layers]); + + const getComponentById = useCallback((componentId: string): { component: ComponentData; layerId: string } | null => { + for (const layer of layers) { + const component = layer.components.find(c => c.id === componentId); + if (component) { + return { component, layerId: layer.id }; + } + } + return null; + }, [layers]); + + const getComponentsInActiveLayer = useCallback((): ComponentData[] => { + const layer = layers.find(l => l.id === activeLayerId); + return layer?.components || []; + }, [layers, activeLayerId]); + + // ===== ๋Ÿฐํƒ€์ž„ ๋ ˆ์ด์–ด ๊ฐ€์‹œ์„ฑ ๊ด€๋ฆฌ ===== + + const showLayer = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => [...new Set([...prev, layerId])]); + }, []); + + const hideLayer = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => prev.filter(id => id !== layerId)); + }, []); + + const toggleLayerRuntime = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => + prev.includes(layerId) + ? prev.filter(id => id !== layerId) + : [...prev, layerId] + ); + }, []); + + return ( + + {children} + + ); +}; diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index 977830ca..8f14a250 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -10,7 +10,19 @@ import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; -import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2 } from "lucide-react"; +import { + Plus, + Trash2, + ArrowUp, + ArrowDown, + ChevronsUpDown, + Check, + Lock, + Unlock, + Database, + Table2, + Link2, +} from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; @@ -35,7 +47,7 @@ export const TableListConfigPanel: React.FC = ({ }) => { // config๊ฐ€ undefined์ธ ๊ฒฝ์šฐ ๋นˆ ๊ฐ์ฒด๋กœ ์ดˆ๊ธฐํ™” const config = configProp || {}; - + // console.log("๐Ÿ” TableListConfigPanel props:", { // config, // configType: typeof config, @@ -202,12 +214,12 @@ export const TableListConfigPanel: React.FC = ({ try { const result = await tableManagementApi.getColumnList(targetTableName); console.log("๐Ÿ”ง tableManagementApi ์‘๋‹ต:", result); - - if (result.success && result.data) { + + if (result.success && result.data) { // API ์‘๋‹ต ๊ตฌ์กฐ: { columns: [...], total, page, ... } const columns = Array.isArray(result.data) ? result.data : result.data.columns; console.log("๐Ÿ”ง ์ปฌ๋Ÿผ ๋ฐฐ์—ด:", columns); - + if (columns && Array.isArray(columns)) { setAvailableColumns( columns.map((col: any) => ({ @@ -779,7 +791,9 @@ export const TableListConfigPanel: React.FC = ({ checked={config.toolbar?.showEditMode ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showEditMode", checked)} /> - +
= ({ checked={config.toolbar?.showExcel ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showExcel", checked)} /> - +
= ({ checked={config.toolbar?.showPdf ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showPdf", checked)} /> - +
= ({ checked={config.toolbar?.showCopy ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showCopy", checked)} /> - +
= ({ checked={config.toolbar?.showSearch ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showSearch", checked)} /> - +
= ({ checked={config.toolbar?.showFilter ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showFilter", checked)} /> - +
= ({ checked={config.toolbar?.showRefresh ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showRefresh", checked)} /> - +
= ({ checked={config.toolbar?.showPaginationRefresh ?? true} onCheckedChange={(checked) => handleNestedChange("toolbar", "showPaginationRefresh", checked)} /> - +
@@ -1159,7 +1187,7 @@ export const TableListConfigPanel: React.FC = ({

์ปฌ๋Ÿผ ์„ ํƒ

-

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”

+

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”


{availableColumns.length > 0 ? ( @@ -1176,7 +1204,10 @@ export const TableListConfigPanel: React.FC = ({ onClick={() => { if (isAdded) { // ์ปฌ๋Ÿผ ์ œ๊ฑฐ - handleChange("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== column.columnName) || [], + ); } else { // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ addColumn(column.columnName); @@ -1187,7 +1218,10 @@ export const TableListConfigPanel: React.FC = ({ checked={isAdded} onCheckedChange={() => { if (isAdded) { - handleChange("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== column.columnName) || [], + ); } else { addColumn(column.columnName); } @@ -1196,7 +1230,9 @@ export const TableListConfigPanel: React.FC = ({ /> {column.label || column.columnName} - {column.input_type || column.dataType} + + {column.input_type || column.dataType} +
); })} @@ -1211,13 +1247,13 @@ export const TableListConfigPanel: React.FC = ({

Entity ์กฐ์ธ ์ปฌ๋Ÿผ

-

์—ฐ๊ด€ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”

+

์—ฐ๊ด€ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”


{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
-
+
{joinTable.tableName} @@ -1225,56 +1261,65 @@ export const TableListConfigPanel: React.FC = ({
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = entityJoinColumns.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); + {joinTable.availableColumns.map((column, colIndex) => { + const matchingJoinColumn = entityJoinColumns.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); - const isAlreadyAdded = config.columns?.some( - (col) => col.columnName === matchingJoinColumn?.joinAlias, - ); + const isAlreadyAdded = config.columns?.some( + (col) => col.columnName === matchingJoinColumn?.joinAlias, + ); if (!matchingJoinColumn) return null; - return ( -
{ + onClick={() => { if (isAlreadyAdded) { // ์ปฌ๋Ÿผ ์ œ๊ฑฐ - handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || [], + ); } else { // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ addEntityColumn(matchingJoinColumn); } }} > - { if (isAlreadyAdded) { - handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); - } else { + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || + [], + ); + } else { addEntityColumn(matchingJoinColumn); - } - }} + } + }} className="pointer-events-none h-3.5 w-3.5" /> - + {column.columnLabel} - {column.inputType || column.dataType} -
- ); - })} -
-
+ + {column.inputType || column.dataType} + +
+ ); + })} +
+
))} -
- + + )} )} @@ -1301,7 +1346,6 @@ export const TableListConfigPanel: React.FC = ({ onConfigChange={(dataFilter) => handleChange("dataFilter", dataFilter)} /> - ); diff --git a/frontend/lib/schemas/componentConfig.ts b/frontend/lib/schemas/componentConfig.ts index 8353ac05..82037cd0 100644 --- a/frontend/lib/schemas/componentConfig.ts +++ b/frontend/lib/schemas/componentConfig.ts @@ -148,9 +148,53 @@ export const componentV2Schema = z.object({ overrides: z.record(z.string(), z.any()).default({}), }); -export const layoutV2Schema = z.object({ - version: z.string().default("2.0"), +// ============================================ +// ๋ ˆ์ด์–ด ์Šคํ‚ค๋งˆ ์ •์˜ +// ============================================ +export const layerTypeSchema = z.enum(["base", "conditional", "modal", "drawer"]); + +export const layerSchema = z.object({ + id: z.string(), + name: z.string(), + type: layerTypeSchema, + zIndex: z.number().default(0), + isVisible: z.boolean().default(true), // ์ดˆ๊ธฐ ํ‘œ์‹œ ์—ฌ๋ถ€ + isLocked: z.boolean().default(false), // ํŽธ์ง‘ ์ž ๊ธˆ ์—ฌ๋ถ€ + + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ๋กœ์ง + condition: z + .object({ + targetComponentId: z.string(), + operator: z.enum(["eq", "neq", "in"]), + value: z.any(), + }) + .optional(), + + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์ „์šฉ ์„ค์ • + overlayConfig: z + .object({ + backdrop: z.boolean().default(true), + closeOnBackdropClick: z.boolean().default(true), + width: z.union([z.string(), z.number()]).optional(), + height: z.union([z.string(), z.number()]).optional(), + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์Šคํƒ€์ผ๋ง + backgroundColor: z.string().optional(), + backdropBlur: z.number().optional(), + // ๋“œ๋กœ์–ด ์ „์šฉ + position: z.enum(["left", "right", "top", "bottom"]).optional(), + }) + .optional(), + + // ํ•ด๋‹น ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค components: z.array(componentV2Schema).default([]), +}); + +export type Layer = z.infer; + +export const layoutV2Schema = z.object({ + version: z.string().default("2.1"), + layers: z.array(layerSchema).default([]), // ์‹ ๊ทœ ํ•„๋“œ + components: z.array(componentV2Schema).default([]), // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ updatedAt: z.string().optional(), screenResolution: z .object({ @@ -952,23 +996,78 @@ export function saveComponentV2(component: ComponentV2 & { config?: Record }> } { - const parsed = layoutV2Schema.parse(layoutData || { version: "2.0", components: [] }); +export function loadLayoutV2(layoutData: any): LayoutV2 & { + components: Array }>; + layers: Array }> }>; +} { + const parsed = layoutV2Schema.parse(layoutData || { version: "2.1", components: [], layers: [] }); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: components๋งŒ ์žˆ๊ณ  layers๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ Default Layer ์ƒ์„ฑ + if ((!parsed.layers || parsed.layers.length === 0) && parsed.components && parsed.components.length > 0) { + const defaultLayer: Layer = { + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: parsed.components, + }; + parsed.layers = [defaultLayer]; + } + + // ๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ + const loadedLayers = parsed.layers.map((layer) => ({ + ...layer, + components: layer.components.map(loadComponentV2), + })); + + // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ components ๋ฐฐ์—ด (๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ํ•ฉ์นจ) + const allComponents = loadedLayers.flatMap((layer) => layer.components); return { ...parsed, - components: parsed.components.map(loadComponentV2), + layers: loadedLayers, + components: allComponents, }; } // ============================================ // V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (์ „์ฒด ์ปดํฌ๋„ŒํŠธ ์ฐจ์ด๊ฐ’ ์ถ”์ถœ) // ============================================ -export function saveLayoutV2(components: Array }>): LayoutV2 { +export function saveLayoutV2( + components: Array }>, + layers?: Array }> }>, +): LayoutV2 { + // ๋ ˆ์ด์–ด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ ˆ์ด์–ด ๊ตฌ์กฐ ์ €์žฅ + if (layers && layers.length > 0) { + const savedLayers = layers.map((layer) => ({ + ...layer, + components: layer.components.map(saveComponentV2), + })); + + return { + version: "2.1", + layers: savedLayers, + components: savedLayers.flatMap((l) => l.components), // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ + }; + } + + // ๋ ˆ์ด์–ด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๊ธฐ์กด ๋ฐฉ์‹) - Default Layer๋กœ ๊ฐ์‹ธ์„œ ์ €์žฅ + const savedComponents = components.map(saveComponentV2); + const defaultLayer: Layer = { + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: savedComponents, + }; + return { - version: "2.0", - components: components.map(saveComponentV2), + version: "2.1", + layers: [defaultLayer], + components: savedComponents, }; } diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 67e8a934..4fa22259 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -38,6 +38,9 @@ export interface BaseComponent { gridColumnStart?: number; // ์‹œ์ž‘ ์ปฌ๋Ÿผ (1-12) gridRowIndex?: number; // ํ–‰ ์ธ๋ฑ์Šค + // ๐Ÿ†• ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ + layerId?: string; // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†ํ•œ ๋ ˆ์ด์–ด ID + parentId?: string; label?: string; required?: boolean; @@ -102,13 +105,13 @@ export interface WidgetComponent extends BaseComponent { entityConfig?: EntityTypeConfig; buttonConfig?: ButtonTypeConfig; arrayConfig?: ArrayTypeConfig; - + // ๐Ÿ†• ์ž๋™ ์ž…๋ ฅ ์„ค์ • (ํ…Œ์ด๋ธ” ์กฐํšŒ ๊ธฐ๋ฐ˜) autoFill?: { enabled: boolean; // ์ž๋™ ์ž…๋ ฅ ํ™œ์„ฑํ™” sourceTable: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” (์˜ˆ: company_mng) filterColumn: string; // ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ (์˜ˆ: company_code) - userField: 'companyCode' | 'userId' | 'deptCode'; // ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ + userField: "companyCode" | "userId" | "deptCode"; // ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ displayColumn: string; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ (์˜ˆ: company_name) }; } @@ -148,12 +151,12 @@ export interface DataTableComponent extends BaseComponent { searchable?: boolean; sortable?: boolean; filters?: DataTableFilter[]; - + // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ์ž๋™ ํ•„ํ„ฐ๋ง autoFilter?: { enabled: boolean; // ์ž๋™ ํ•„ํ„ฐ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ filterColumn: string; // ํ•„ํ„ฐ๋งํ•  ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: company_code, dept_code) - userField: 'companyCode' | 'userId' | 'deptCode'; // ์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ + userField: "companyCode" | "userId" | "deptCode"; // ์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ }; // ๐Ÿ†• ์ปฌ๋Ÿผ ๊ฐ’ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง @@ -307,13 +310,13 @@ export interface SelectTypeConfig { required?: boolean; readonly?: boolean; emptyMessage?: string; - + /** ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๊ด€๊ณ„ ์ฝ”๋“œ (๊ด€๊ณ„ ๊ด€๋ฆฌ์—์„œ ์ •์˜ํ•œ ์ฝ”๋“œ) */ cascadingRelationCode?: string; - + /** ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๋ถ€๋ชจ ํ•„๋“œ๋ช… (ํ™”๋ฉด ๋‚ด ๋‹ค๋ฅธ ํ•„๋“œ์˜ columnName) */ cascadingParentField?: string; - + /** @deprecated ์ง์ ‘ ์„ค์ • ๋ฐฉ์‹ - cascadingRelationCode ์‚ฌ์šฉ ๊ถŒ์žฅ */ cascading?: CascadingDropdownConfig; } @@ -402,10 +405,10 @@ export interface EntityTypeConfig { /** * ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด(Cascading Dropdown) ์„ค์ • - * + * * ๋ถ€๋ชจ ํ•„๋“œ์˜ ๊ฐ’์— ๋”ฐ๋ผ ์ž์‹ ๋“œ๋กญ๋‹ค์šด์˜ ์˜ต์…˜์ด ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. * ์˜ˆ: ์ฐฝ๊ณ  ์„ ํƒ โ†’ ํ•ด๋‹น ์ฐฝ๊ณ ์˜ ์œ„์น˜๋งŒ ํ‘œ์‹œ - * + * * @example * // ์ฐฝ๊ณ  โ†’ ์œ„์น˜ ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด * { @@ -420,34 +423,34 @@ export interface EntityTypeConfig { export interface CascadingDropdownConfig { /** ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ํ™œ์„ฑํ™” ์—ฌ๋ถ€ */ enabled: boolean; - + /** ๋ถ€๋ชจ ํ•„๋“œ๋ช… (์ด ํ•„๋“œ์˜ ๊ฐ’์— ๋”ฐ๋ผ ์˜ต์…˜์ด ํ•„ํ„ฐ๋ง๋จ) */ parentField: string; - + /** ์˜ต์…˜์„ ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… */ sourceTable: string; - + /** ๋ถ€๋ชจ ๊ฐ’๊ณผ ๋งค์นญํ•  ์ปฌ๋Ÿผ๋ช… (sourceTable์˜ ์ปฌ๋Ÿผ) */ parentKeyColumn: string; - + /** ๋“œ๋กญ๋‹ค์šด value๋กœ ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ๋ช… */ valueColumn: string; - + /** ๋“œ๋กญ๋‹ค์šด label๋กœ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋ช… */ labelColumn: string; - + /** ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด (์„ ํƒ์‚ฌํ•ญ) */ additionalFilters?: Record; - + /** ๋ถ€๋ชจ ๊ฐ’์ด ์—†์„ ๋•Œ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ emptyParentMessage?: string; - + /** ์˜ต์…˜์ด ์—†์„ ๋•Œ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ noOptionsMessage?: string; - + /** ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ loadingMessage?: string; - + /** ๋ถ€๋ชจ ๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ๊ฐ’ ์ดˆ๊ธฐํ™” */ clearOnParentChange?: boolean; } @@ -472,23 +475,23 @@ export interface ButtonTypeConfig { export interface QuickInsertColumnMapping { /** ์ €์žฅํ•  ํ…Œ์ด๋ธ”์˜ ๋Œ€์ƒ ์ปฌ๋Ÿผ๋ช… */ targetColumn: string; - + /** ๊ฐ’ ์†Œ์Šค ํƒ€์ž… */ sourceType: "component" | "leftPanel" | "fixed" | "currentUser"; - + // sourceType๋ณ„ ์ถ”๊ฐ€ ์„ค์ • /** component: ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ ID */ sourceComponentId?: string; - + /** component: ์ปดํฌ๋„ŒํŠธ์˜ columnName (formData ์ ‘๊ทผ์šฉ) */ sourceColumnName?: string; - + /** leftPanel: ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ์˜ ์ปฌ๋Ÿผ๋ช… */ sourceColumn?: string; - + /** fixed: ๊ณ ์ •๊ฐ’ */ fixedValue?: any; - + /** currentUser: ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ */ userField?: "userId" | "userName" | "companyCode" | "deptCode"; } @@ -499,13 +502,13 @@ export interface QuickInsertColumnMapping { export interface QuickInsertAfterAction { /** ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ, ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด ์ปดํฌ๋„ŒํŠธ) */ refreshData?: boolean; - + /** ์ดˆ๊ธฐํ™”ํ•  ์ปดํฌ๋„ŒํŠธ ID ๋ชฉ๋ก */ clearComponents?: string[]; - + /** ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์—ฌ๋ถ€ */ showSuccessMessage?: boolean; - + /** ์ปค์Šคํ…€ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ */ successMessage?: string; } @@ -516,20 +519,20 @@ export interface QuickInsertAfterAction { export interface QuickInsertDuplicateCheck { /** ์ค‘๋ณต ์ฒดํฌ ํ™œ์„ฑํ™” */ enabled: boolean; - + /** ์ค‘๋ณต ์ฒดํฌํ•  ์ปฌ๋Ÿผ๋“ค */ columns: string[]; - + /** ์ค‘๋ณต ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */ errorMessage?: string; } /** * ์ฆ‰์‹œ ์ €์žฅ(quickInsert) ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • - * + * * ํ™”๋ฉด์—์„œ entity ํƒ€์ž… ์„ ํƒ๋ฐ•์Šค๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•œ ํ›„, * ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ INSERTํ•˜๋Š” ๊ธฐ๋Šฅ - * + * * @example * ```typescript * const config: QuickInsertConfig = { @@ -557,13 +560,13 @@ export interface QuickInsertDuplicateCheck { export interface QuickInsertConfig { /** ์ €์žฅํ•  ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… */ targetTable: string; - + /** ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • */ columnMappings: QuickInsertColumnMapping[]; - + /** ์ €์žฅ ํ›„ ๋™์ž‘ ์„ค์ • */ afterInsert?: QuickInsertAfterAction; - + /** ์ค‘๋ณต ์ฒดํฌ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) */ duplicateCheck?: QuickInsertDuplicateCheck; } @@ -678,15 +681,15 @@ export interface DataTableFilter { export interface ColumnFilter { id: string; columnName: string; // ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ๋ช… - operator: - | "equals" - | "not_equals" - | "in" - | "not_in" - | "contains" - | "starts_with" - | "ends_with" - | "is_null" + operator: + | "equals" + | "not_equals" + | "in" + | "not_in" + | "contains" + | "starts_with" + | "ends_with" + | "is_null" | "is_not_null" | "greater_than" | "less_than" @@ -836,12 +839,71 @@ export interface GroupState { groupTitle?: string; } +// ============================================ +// ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ํƒ€์ž… ์ •์˜ +// ============================================ + +/** + * ๋ ˆ์ด์–ด ํƒ€์ž… + * - base: ๊ธฐ๋ณธ ๋ ˆ์ด์–ด (ํ•ญ์ƒ ํ‘œ์‹œ) + * - conditional: ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด (ํŠน์ • ์กฐ๊ฑด ๋งŒ์กฑ ์‹œ ํ‘œ์‹œ) + * - modal: ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด (ํŒ์—… ํ˜•ํƒœ) + * - drawer: ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด (์‚ฌ์ด๋“œ ํŒจ๋„ ํ˜•ํƒœ) + */ +export type LayerType = "base" | "conditional" | "modal" | "drawer"; + +/** + * ๋ ˆ์ด์–ด ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ์„ค์ • + */ +export interface LayerCondition { + targetComponentId: string; // ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ID + operator: "eq" | "neq" | "in"; // ๋น„๊ต ์—ฐ์‚ฐ์ž + value: any; // ๋น„๊ตํ•  ๊ฐ’ +} + +/** + * ๋ ˆ์ด์–ด ์˜ค๋ฒ„๋ ˆ์ด ์„ค์ • (๋ชจ๋‹ฌ/๋“œ๋กœ์–ด์šฉ) + */ +export interface LayerOverlayConfig { + backdrop: boolean; // ๋ฐฐ๊ฒฝ ์–ด๋‘ก๊ฒŒ ์ฒ˜๋ฆฌ ์—ฌ๋ถ€ + closeOnBackdropClick: boolean; // ๋ฐฐ๊ฒฝ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ ์—ฌ๋ถ€ + width?: string | number; // ๋„ˆ๋น„ + height?: string | number; // ๋†’์ด + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์Šคํƒ€์ผ๋ง + backgroundColor?: string; // ์ปจํ…์ธ  ๋ฐฐ๊ฒฝ์ƒ‰ + backdropBlur?: number; // ๋ฐฐ๊ฒฝ ๋ธ”๋Ÿฌ (px) + // ๋“œ๋กœ์–ด ์ „์šฉ + position?: "left" | "right" | "top" | "bottom"; // ๋“œ๋กœ์–ด ์œ„์น˜ +} + +/** + * ๋ ˆ์ด์–ด ์ •์˜ + */ +export interface LayerDefinition { + id: string; + name: string; + type: LayerType; + zIndex: number; + isVisible: boolean; // ์ดˆ๊ธฐ ํ‘œ์‹œ ์—ฌ๋ถ€ + isLocked: boolean; // ํŽธ์ง‘ ์ž ๊ธˆ ์—ฌ๋ถ€ + + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ๋กœ์ง + condition?: LayerCondition; + + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์ „์šฉ ์„ค์ • + overlayConfig?: LayerOverlayConfig; + + // ํ•ด๋‹น ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค + components: ComponentData[]; +} + /** * ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ */ export interface LayoutData { screenId: number; - components: ComponentData[]; + components: ComponentData[]; // @deprecated - use layers instead (kept for backward compatibility) + layers?: LayerDefinition[]; // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ชฉ๋ก gridSettings?: GridSettings; metadata?: LayoutMetadata; screenResolution?: ScreenResolution;