diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 6f412de5..74506a39 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -5083,8 +5083,8 @@ export class ScreenManagementService { let layout: { layout_data: any } | null = null; // ๐Ÿ†• ๊ธฐ๋ณธ ๋ ˆ์ด์–ด(layer_id=1)๋ฅผ ์šฐ์„  ๋กœ๋“œ - // SUPER_ADMIN์ธ ๊ฒฝ์šฐ: ํ™”๋ฉด์˜ ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ - if (isSuperAdmin) { + // SUPER_ADMIN์ด๊ฑฐ๋‚˜ companyCode๊ฐ€ "*"์ธ ๊ฒฝ์šฐ: ํ™”๋ฉด์˜ ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + if (isSuperAdmin || companyCode === "*") { // 1. ํ™”๋ฉด ์ •์˜์˜ ํšŒ์‚ฌ ์ฝ”๋“œ + ๊ธฐ๋ณธ ๋ ˆ์ด์–ด layout = await queryOne<{ layout_data: any }>( `SELECT layout_data FROM screen_layouts_v2 diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 160883ad..d1e07abe 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -179,7 +179,25 @@ function ScreenViewPage() { } else { // V1 ๋ ˆ์ด์•„์›ƒ ๋˜๋Š” ๋นˆ ๋ ˆ์ด์•„์›ƒ const layoutData = await screenApi.getLayout(screenId); - setLayout(layoutData); + if (layoutData?.components?.length > 0) { + setLayout(layoutData); + } else { + console.warn("[ScreenViewPage] getLayout ์‹คํŒจ, getLayerLayout(1) fallback:", screenId); + const baseLayerData = await screenApi.getLayerLayout(screenId, 1); + if (baseLayerData && isValidV2Layout(baseLayerData)) { + const converted = convertV2ToLegacy(baseLayerData); + if (converted) { + setLayout({ + ...converted, + screenResolution: baseLayerData.screenResolution || converted.screenResolution, + } as LayoutData); + } else { + setLayout(layoutData); + } + } else { + setLayout(layoutData); + } + } } } catch (layoutError) { console.warn("๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์‹คํŒจ, ๋นˆ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ:", layoutError); diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index fe6ba4fa..ec36096d 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -413,9 +413,28 @@ export const EditModal: React.FC = ({ className }) => { // V2 ์—†์œผ๋ฉด ๊ธฐ์กด API fallback if (!layoutData) { + console.warn("[EditModal] V2 ๋ ˆ์ด์•„์›ƒ ์—†์Œ, getLayout fallback ์‹œ๋„:", screenId); layoutData = await screenApi.getLayout(screenId); } + // getLayout๋„ ์‹คํŒจํ•˜๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด(layer_id=1) ์ง์ ‘ ๋กœ๋“œ + if (!layoutData || !layoutData.components || layoutData.components.length === 0) { + console.warn("[EditModal] getLayout๋„ ์‹คํŒจ, getLayerLayout(1) ์ตœ์ข… fallback:", screenId); + try { + const baseLayerData = await screenApi.getLayerLayout(screenId, 1); + if (baseLayerData && isValidV2Layout(baseLayerData)) { + layoutData = convertV2ToLegacy(baseLayerData); + if (layoutData) { + layoutData.screenResolution = baseLayerData.screenResolution || layoutData.screenResolution; + } + } else if (baseLayerData?.components) { + layoutData = baseLayerData; + } + } catch (fallbackErr) { + console.error("[EditModal] getLayerLayout(1) fallback ์‹คํŒจ:", fallbackErr); + } + } + if (screenInfo && layoutData) { const components = layoutData.components || []; @@ -1440,7 +1459,7 @@ export const EditModal: React.FC = ({ className }) => { -
+
{loading ? (
@@ -1455,7 +1474,7 @@ export const EditModal: React.FC = ({ className }) => { >
= ( // ๋ผ๋ฒจ ํ‘œ์‹œ ์—ฌ๋ถ€ ๊ณ„์‚ฐ const shouldShowLabel = - !hideLabel && // hideLabel์ด true๋ฉด ๋ผ๋ฒจ ์ˆจ๊น€ - (component.style?.labelDisplay ?? true) && + !hideLabel && + (component.style?.labelDisplay ?? true) !== false && + component.style?.labelDisplay !== "false" && (component.label || component.style?.labelText) && - !templateTypes.includes(component.type); // ํ…œํ”Œ๋ฆฟ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ผ๋ฒจ ํ‘œ์‹œ ์•ˆํ•จ + !templateTypes.includes(component.type); const labelText = component.style?.labelText || component.label || ""; @@ -2232,8 +2233,17 @@ export const InteractiveScreenViewer: React.FC = ( ...component, style: { ...component.style, - labelDisplay: false, // ์ƒ์œ„์—์„œ ๋ผ๋ฒจ์„ ํ‘œ์‹œํ–ˆ์œผ๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ๋Š” ์ˆจ๊น€ + labelDisplay: false, + labelPosition: "top" as const, + ...(isHorizontalLabel ? { width: "100%", height: "100%" } : {}), }, + ...(isHorizontalLabel ? { + size: { + ...component.size, + width: undefined as unknown as number, + height: undefined as unknown as number, + }, + } : {}), } : component; diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index a35c5ed2..1bb04e97 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -1109,7 +1109,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const compType = (component as any).componentType || ""; const isSplitLine = type === "component" && compType === "v2-split-line"; @@ -1194,9 +1200,17 @@ export const InteractiveScreenViewerDynamic: React.FC { + const { borderWidth: _bw, borderColor: _bc, borderStyle: _bs, border: _b, borderRadius: _br, ...rest } = safeStyleWithoutSize; + return rest; + })() + : safeStyleWithoutSize; + const componentStyle = { position: "absolute" as const, - ...safeStyleWithoutSize, + ...cleanedStyle, // left/top์€ ๋ฐ˜๋“œ์‹œ ๋งˆ์ง€๋ง‰์— (styleWithoutSize๊ฐ€ ๋ฎ์–ด์“ฐ์ง€ ๋ชปํ•˜๊ฒŒ) left: adjustedX, top: position?.y || 0, @@ -1267,11 +1281,7 @@ export const InteractiveScreenViewerDynamic: React.FC
{needsExternalLabel ? ( -
- {externalLabelComponent} -
- {renderInteractiveWidget(componentToRender)} + isHorizLabel ? ( +
+ +
+ {renderInteractiveWidget(componentToRender)} +
-
+ ) : ( +
+ {externalLabelComponent} +
+ {renderInteractiveWidget(componentToRender)} +
+
+ ) ) : ( renderInteractiveWidget(componentToRender) )} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index b95506d9..dcca4d0d 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -548,10 +548,23 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const origWidth = size?.width || 100; const isSplitShrunk = splitAdjustedWidth !== null && splitAdjustedWidth < origWidth; + // v2 ์ˆ˜ํ‰ ๋ผ๋ฒจ ์ปดํฌ๋„ŒํŠธ: position wrapper์—์„œ border ์ œ๊ฑฐ (DynamicComponentRenderer๊ฐ€ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ) + const isV2HorizLabel = !!( + componentStyle && + (componentStyle.labelDisplay === true || componentStyle.labelDisplay === "true") && + (componentStyle.labelPosition === "left" || componentStyle.labelPosition === "right") + ); + const safeComponentStyle = isV2HorizLabel + ? (() => { + const { borderWidth, borderColor, borderStyle, border, borderRadius, ...rest } = componentStyle as any; + return rest; + })() + : componentStyle; + const baseStyle = { left: `${adjustedPositionX}px`, top: `${position.y}px`, - ...componentStyle, + ...safeComponentStyle, width: splitAdjustedWidth !== null ? `${splitAdjustedWidth}px` : displayWidth, height: displayHeight, zIndex: component.type === "layout" ? 1 : position.z || 2, diff --git a/frontend/components/v2/V2Date.tsx b/frontend/components/v2/V2Date.tsx index 1f8b6b99..c6bbff8b 100644 --- a/frontend/components/v2/V2Date.tsx +++ b/frontend/components/v2/V2Date.tsx @@ -700,7 +700,7 @@ export const V2Date = forwardRef((props, ref) => { } }; - const showLabel = label && style?.labelDisplay !== false; + const showLabel = label && style?.labelDisplay !== false && style?.labelDisplay !== "false"; const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index d76802e8..219fa275 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -962,7 +962,7 @@ export const V2Input = forwardRef((props, ref) => }; const actualLabel = label || style?.labelText; - const showLabel = actualLabel && style?.labelDisplay === true; + const showLabel = actualLabel && style?.labelDisplay !== false && style?.labelDisplay !== "false"; const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 52b6614d..538d33be 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -1145,7 +1145,7 @@ export const V2Select = forwardRef( } }; - const showLabel = label && style?.labelDisplay !== false; + const showLabel = label && style?.labelDisplay !== false && style?.labelDisplay !== "false"; const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 85532c36..50c4bee4 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -371,15 +371,18 @@ export const DynamicComponentRenderer: React.FC = try { const { V2SelectRenderer } = require("@/lib/registry/components/v2-select/V2SelectRenderer"); const fieldName = columnName || component.id; - const currentValue = props.formData?.[fieldName] || ""; - const handleChange = (value: any) => { - if (props.onFormDataChange) { - props.onFormDataChange(fieldName, value); - } - }; - - // V2SelectRenderer์šฉ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + // ์ˆ˜ํ‰ ๋ผ๋ฒจ ๊ฐ์ง€ + const catLabelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; + const catLabelPosition = component.style?.labelPosition; + const catLabelText = (catLabelDisplay === true || catLabelDisplay === "true") + ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) + : undefined; + const catNeedsExternalHorizLabel = !!( + catLabelText && + (catLabelPosition === "left" || catLabelPosition === "right") + ); + const selectComponent = { ...component, componentConfig: { @@ -395,6 +398,24 @@ export const DynamicComponentRenderer: React.FC = webType: "category", }; + const catStyle = catNeedsExternalHorizLabel + ? { + ...(component as any).style, + labelDisplay: false, + labelPosition: "top" as const, + width: "100%", + height: "100%", + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + border: undefined, + borderRadius: undefined, + } + : (component as any).style; + const catSize = catNeedsExternalHorizLabel + ? { ...(component as any).size, width: undefined, height: undefined } + : (component as any).size; + const rendererProps = { component: selectComponent, formData: props.formData, @@ -402,12 +423,47 @@ export const DynamicComponentRenderer: React.FC = isDesignMode: props.isDesignMode, isInteractive: props.isInteractive ?? !props.isDesignMode, tableName, - style: (component as any).style, - size: (component as any).size, + style: catStyle, + size: catSize, }; const rendererInstance = new V2SelectRenderer(rendererProps); - return rendererInstance.render(); + const renderedCatSelect = rendererInstance.render(); + + if (catNeedsExternalHorizLabel) { + const labelGap = component.style?.labelGap || "8px"; + const labelFontSize = component.style?.labelFontSize || "14px"; + const labelColor = component.style?.labelColor || "#64748b"; + const labelFontWeight = component.style?.labelFontWeight || "500"; + const isRequired = component.required || (component as any).required; + const isLeft = catLabelPosition === "left"; + return ( +
+ +
+ {renderedCatSelect} +
+
+ ); + } + return renderedCatSelect; } catch (error) { console.error("โŒ V2SelectRenderer ๋กœ๋“œ ์‹คํŒจ:", error); } @@ -619,18 +675,39 @@ export const DynamicComponentRenderer: React.FC = componentType === "modal-repeater-table" || componentType === "v2-input"; - // ๐Ÿ†• v2-input ๋“ฑ์˜ ๋ผ๋ฒจ ํ‘œ์‹œ ๋กœ์ง (labelDisplay๊ฐ€ true์ผ ๋•Œ๋งŒ ๋ผ๋ฒจ ํ‘œ์‹œ) + // ๐Ÿ†• v2-input ๋“ฑ์˜ ๋ผ๋ฒจ ํ‘œ์‹œ ๋กœ์ง (labelDisplay๊ฐ€ true/"true"์ผ ๋•Œ๋งŒ ๋ผ๋ฒจ ํ‘œ์‹œ) const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; - const effectiveLabel = labelDisplay === true + const effectiveLabel = (labelDisplay === true || labelDisplay === "true") ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) : undefined; + // ๐Ÿ”ง ์ˆ˜ํ‰ ๋ผ๋ฒจ(left/right) ๊ฐ์ง€ โ†’ ์™ธ๋ถ€ flex ์ปจํ…Œ์ด๋„ˆ์—์„œ ๋ผ๋ฒจ ์ฒ˜๋ฆฌ + const labelPosition = component.style?.labelPosition; + const isV2Component = componentType?.startsWith("v2-"); + const needsExternalHorizLabel = !!( + isV2Component && + effectiveLabel && + (labelPosition === "left" || labelPosition === "right") + ); + // ๐Ÿ”ง ์ˆœ์„œ ์ค‘์š”! component.style ๋จผ์ €, CSS ํฌ๊ธฐ ์†์„ฑ์€ size ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฎ์–ด์”€ const mergedStyle = { ...component.style, // ์›๋ณธ style (labelDisplay, labelText ๋“ฑ) - ๋จผ์ €! // CSS ํฌ๊ธฐ ์†์„ฑ์€ size์—์„œ ๊ณ„์‚ฐํ•œ ๊ฐ’์œผ๋กœ ๋ช…์‹œ์  ๋ฎ์–ด์“ฐ๊ธฐ (์šฐ์„ ์ˆœ์œ„ ์ตœ๊ณ ) width: finalStyle.width, height: finalStyle.height, + // ์ˆ˜ํ‰ ๋ผ๋ฒจ โ†’ V2 ์ปดํฌ๋„ŒํŠธ์—๋Š” ๋ผ๋ฒจ ๋น„ํ™œ์„ฑํ™” (์™ธ๋ถ€์—์„œ ์ฒ˜๋ฆฌ) + ...(needsExternalHorizLabel ? { + labelDisplay: false, + labelPosition: "top" as const, + width: "100%", + height: "100%", + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + border: undefined, + borderRadius: undefined, + } : {}), }; // ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ componentConfig ๋ณ‘ํ•ฉ (DB ์ตœ์‹  ์„ค์ • ์šฐ์„ ) @@ -649,7 +726,9 @@ export const DynamicComponentRenderer: React.FC = onClick, onDragStart, onDragEnd, - size: component.size || newComponent.defaultSize, + size: needsExternalHorizLabel + ? { ...(component.size || newComponent.defaultSize), width: undefined, height: undefined } + : (component.size || newComponent.defaultSize), position: component.position, config: mergedComponentConfig, componentConfig: mergedComponentConfig, @@ -657,8 +736,8 @@ export const DynamicComponentRenderer: React.FC = ...(mergedComponentConfig || {}), // ๐Ÿ”ง style์€ ๋งจ ๋งˆ์ง€๋ง‰์—! (componentConfig.style์ด ์žˆ์–ด๋„ mergedStyle์ด ์šฐ์„ ) style: mergedStyle, - // ๐Ÿ†• ๋ผ๋ฒจ ํ‘œ์‹œ (labelDisplay๊ฐ€ true์ผ ๋•Œ๋งŒ) - label: effectiveLabel, + // ์ˆ˜ํ‰ ๋ผ๋ฒจ โ†’ ์™ธ๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ label ์ „๋‹ฌ ์•ˆ ํ•จ + label: needsExternalHorizLabel ? undefined : effectiveLabel, // ๐Ÿ†• V2 ๋ ˆ์ด์•„์›ƒ์—์„œ overrides์—์„œ ๋ณต์›๋œ ์ƒ์œ„ ๋ ˆ๋ฒจ ์†์„ฑ๋“ค๋„ ์ „๋‹ฌ (DB ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์šฐ์„ ) inputType: (baseColumnName && columnMetaCache[screenTableName || ""]?.[baseColumnName]?.input_type) || (component as any).inputType || mergedComponentConfig?.inputType, columnName: (component as any).columnName || component.componentConfig?.columnName, @@ -759,16 +838,51 @@ export const DynamicComponentRenderer: React.FC = NewComponentRenderer.prototype && NewComponentRenderer.prototype.render; + let renderedElement: React.ReactElement; if (isClass) { - // ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ๋ Œ๋”๋Ÿฌ (AutoRegisteringComponentRenderer ์ƒ์†) const rendererInstance = new NewComponentRenderer(rendererProps); - return rendererInstance.render(); + renderedElement = rendererInstance.render(); } else { - // ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ - // refreshKey๋ฅผ React key๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋งˆ์šดํŠธ ๊ฐ•์ œ - - return ; + renderedElement = ; } + + // ์ˆ˜ํ‰ ๋ผ๋ฒจ โ†’ ๋ผ๋ฒจ์„ ์ปดํฌ๋„ŒํŠธ ์˜์—ญ ๋ฐ”๊นฅ์— absolute ๋ฐฐ์น˜, ์ž…๋ ฅ์€ 100% ์ฑ„์›€ + if (needsExternalHorizLabel) { + const labelGap = component.style?.labelGap || "8px"; + const labelFontSize = component.style?.labelFontSize || "14px"; + const labelColor = component.style?.labelColor || "#64748b"; + const labelFontWeight = component.style?.labelFontWeight || "500"; + const isRequired = component.required || (component as any).required; + const isLeft = labelPosition === "left"; + + return ( +
+ +
+ {renderedElement} +
+
+ ); + } + + return renderedElement; } } catch (error) { console.error(`โŒ ์ƒˆ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ์‹คํŒจ (${componentType}):`, error); diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 8cd8b0c5..8526b0c9 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -10,11 +10,74 @@ 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, GripVertical, X } 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"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; +import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +/** + * ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์„ ํƒ๋œ ์ปฌ๋Ÿผ ํ–‰ (v2-split-panel-layout์˜ SortableColumnRow ๋™์ผ ํŒจํ„ด) + */ +function SortableColumnRow({ + id, + col, + index, + isEntityJoin, + onLabelChange, + onWidthChange, + onRemove, +}: { + id: string; + col: ColumnConfig; + index: number; + isEntityJoin?: boolean; + onLabelChange: (value: string) => void; + onWidthChange: (value: number) => void; + onRemove: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const style = { transform: CSS.Transform.toString(transform), transition }; + + return ( +
+
+ +
+ {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} + onLabelChange(e.target.value)} + placeholder="ํ‘œ์‹œ๋ช…" + className="h-6 min-w-0 flex-1 text-xs" + /> + onWidthChange(parseInt(e.target.value) || 100)} + placeholder="๋„ˆ๋น„" + className="h-6 w-14 shrink-0 text-xs" + /> + +
+ ); +} export interface TableListConfigPanelProps { config: TableListConfig; @@ -348,11 +411,11 @@ export const TableListConfigPanel: React.FC = ({ const existingColumn = config.columns?.find((col) => col.columnName === columnName); if (existingColumn) return; - // tableColumns์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ผ๋ฒจ ์ •๋ณด ์ฐพ๊ธฐ + // tableColumns โ†’ availableColumns ์ˆœ์„œ๋กœ ํ•œ๊ตญ์–ด ๋ผ๋ฒจ ์ฐพ๊ธฐ const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName); + const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName); - // ๋ผ๋ฒจ๋ช… ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ - const displayName = columnInfo?.label || columnInfo?.displayName || columnName; + const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName; const newColumn: ColumnConfig = { columnName, @@ -1213,6 +1276,62 @@ export const TableListConfigPanel: React.FC = ({ )} + {/* ์„ ํƒ๋œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ (DnD) */} + {config.columns && config.columns.length > 0 && ( +
+
+

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ({config.columns.length}๊ฐœ ์„ ํƒ)

+

+ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํ‘œ์‹œ๋ช…/๋„ˆ๋น„๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +

+
+
+ { + const { active, over } = event; + if (!over || active.id === over.id) return; + const columns = [...(config.columns || [])]; + const oldIndex = columns.findIndex((c) => c.columnName === active.id); + const newIndex = columns.findIndex((c) => c.columnName === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + const reordered = arrayMove(columns, oldIndex, newIndex); + reordered.forEach((col, idx) => { col.order = idx; }); + handleChange("columns", reordered); + } + }} + > + c.columnName)} + strategy={verticalListSortingStrategy} + > +
+ {(config.columns || []).map((column, idx) => { + const resolvedLabel = + column.displayName && column.displayName !== column.columnName + ? column.displayName + : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName; + + const colWithLabel = { ...column, displayName: resolvedLabel }; + return ( + updateColumn(column.columnName, { displayName: value })} + onWidthChange={(value) => updateColumn(column.columnName, { width: value })} + onRemove={() => removeColumn(column.columnName)} + /> + ); + })} +
+
+
+
+ )} + {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง ์„ค์ • */}
@@ -1240,3 +1359,4 @@ export const TableListConfigPanel: React.FC = ({
); }; + diff --git a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx index fcb7b710..bd5f3d92 100644 --- a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx @@ -937,19 +937,38 @@ export function BomItemEditorComponent({ setItemSearchOpen(true); }, []); - // ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ’ˆ๋ชฉ ID ๋ชฉ๋ก (์ค‘๋ณต ๋ฐฉ์ง€์šฉ) + // ๊ฐ™์€ ๋ ˆ๋ฒจ(ํ˜•์ œ) ํ’ˆ๋ชฉ ID ๋ชฉ๋ก (๋™์ผ ๋ ˆ๋ฒจ ์ค‘๋ณต ๋ฐฉ์ง€, ํ•˜์œ„ ๋ ˆ๋ฒจ์€ ํ—ˆ์šฉ) const existingItemIds = useMemo(() => { const ids = new Set(); - const collect = (nodes: BomItemNode[]) => { - for (const n of nodes) { - const fk = n.data[cfg.dataSource?.foreignKey || "child_item_id"]; + const fkField = cfg.dataSource?.foreignKey || "child_item_id"; + + if (addTargetParentId === null) { + // ๋ฃจํŠธ ๋ ˆ๋ฒจ ์ถ”๊ฐ€: ๋ฃจํŠธ ๋…ธ๋“œ์˜ ํ˜•์ œ๋“ค๋งŒ ์ฒดํฌ + for (const n of treeData) { + const fk = n.data[fkField]; if (fk) ids.add(fk); - collect(n.children); } - }; - collect(treeData); + } else { + // ํ•˜์œ„ ์ถ”๊ฐ€: ํ•ด๋‹น ๋ถ€๋ชจ์˜ ์ง์† ์ž์‹๋“ค๋งŒ ์ฒดํฌ + const findParent = (nodes: BomItemNode[]): BomItemNode | null => { + for (const n of nodes) { + if (n.tempId === addTargetParentId) return n; + const found = findParent(n.children); + if (found) return found; + } + return null; + }; + const parent = findParent(treeData); + if (parent) { + for (const child of parent.children) { + const fk = child.data[fkField]; + if (fk) ids.add(fk); + } + } + } + return ids; - }, [treeData, cfg]); + }, [treeData, cfg, addTargetParentId]); // ๋ฃจํŠธ ํ’ˆ๋ชฉ ์ถ”๊ฐ€ ์‹œ์ž‘ const handleAddRoot = useCallback(() => { diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index 35f15596..7de8a533 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -22,11 +22,76 @@ import { Database, Table2, Link2, + GripVertical, + X, } 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"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; +import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +/** + * ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์„ ํƒ๋œ ์ปฌ๋Ÿผ ํ–‰ (v2-split-panel-layout์˜ SortableColumnRow ๋™์ผ ํŒจํ„ด) + */ +function SortableColumnRow({ + id, + col, + index, + isEntityJoin, + onLabelChange, + onWidthChange, + onRemove, +}: { + id: string; + col: ColumnConfig; + index: number; + isEntityJoin?: boolean; + onLabelChange: (value: string) => void; + onWidthChange: (value: number) => void; + onRemove: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const style = { transform: CSS.Transform.toString(transform), transition }; + + return ( +
+
+ +
+ {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} + onLabelChange(e.target.value)} + placeholder="ํ‘œ์‹œ๋ช…" + className="h-6 min-w-0 flex-1 text-xs" + /> + onWidthChange(parseInt(e.target.value) || 100)} + placeholder="๋„ˆ๋น„" + className="h-6 w-14 shrink-0 text-xs" + /> + +
+ ); +} export interface TableListConfigPanelProps { config: TableListConfig; @@ -366,11 +431,11 @@ export const TableListConfigPanel: React.FC = ({ const existingColumn = config.columns?.find((col) => col.columnName === columnName); if (existingColumn) return; - // tableColumns์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ผ๋ฒจ ์ •๋ณด ์ฐพ๊ธฐ + // tableColumns โ†’ availableColumns ์ˆœ์„œ๋กœ ํ•œ๊ตญ์–ด ๋ผ๋ฒจ ์ฐพ๊ธฐ const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName); + const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName); - // ๋ผ๋ฒจ๋ช… ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ - const displayName = columnInfo?.label || columnInfo?.displayName || columnName; + const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName; const newColumn: ColumnConfig = { columnName, @@ -1458,6 +1523,63 @@ export const TableListConfigPanel: React.FC = ({ )} + {/* ์„ ํƒ๋œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ (DnD) */} + {config.columns && config.columns.length > 0 && ( +
+
+

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ({config.columns.length}๊ฐœ ์„ ํƒ)

+

+ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํ‘œ์‹œ๋ช…/๋„ˆ๋น„๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +

+
+
+ { + const { active, over } = event; + if (!over || active.id === over.id) return; + const columns = [...(config.columns || [])]; + const oldIndex = columns.findIndex((c) => c.columnName === active.id); + const newIndex = columns.findIndex((c) => c.columnName === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + const reordered = arrayMove(columns, oldIndex, newIndex); + reordered.forEach((col, idx) => { col.order = idx; }); + handleChange("columns", reordered); + } + }} + > + c.columnName)} + strategy={verticalListSortingStrategy} + > +
+ {(config.columns || []).map((column, idx) => { + // displayName์ด columnName๊ณผ ๊ฐ™์œผ๋ฉด ํ•œ๊ตญ์–ด ๋ผ๋ฒจ ๋ฏธ์„ค์ • โ†’ availableColumns์—์„œ ์ฐพ๊ธฐ + const resolvedLabel = + column.displayName && column.displayName !== column.columnName + ? column.displayName + : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName; + + const colWithLabel = { ...column, displayName: resolvedLabel }; + return ( + updateColumn(column.columnName, { displayName: value })} + onWidthChange={(value) => updateColumn(column.columnName, { width: value })} + onRemove={() => removeColumn(column.columnName)} + /> + ); + })} +
+
+
+
+ )} + {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง ์„ค์ • */}
@@ -1484,3 +1606,4 @@ export const TableListConfigPanel: React.FC = ({
); }; + diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 054b257f..2ed4db87 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -3173,16 +3173,16 @@ export class ButtonActionExecutor { return false; } - // 1. ํ™”๋ฉด ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ - let description = config.modalDescription || ""; - if (!description) { + // 1. ํ™”๋ฉด ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ œ๋ชฉ/์„ค๋ช…์ด ๋ฏธ์„ค์ • ์‹œ ํ™”๋ฉด๋ช…์—์„œ ๊ฐ€์ ธ์˜ด) + let screenInfo: any = null; + if (!config.modalTitle || !config.modalDescription) { try { - const screenInfo = await screenApi.getScreen(config.targetScreenId); - description = screenInfo?.description || ""; + screenInfo = await screenApi.getScreen(config.targetScreenId); } catch (error) { - console.warn("ํ™”๋ฉด ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); + console.warn("ํ™”๋ฉด ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); } } + let description = config.modalDescription || screenInfo?.description || ""; // 2. ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ฐ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ let selectedData: any[] = []; @@ -3288,7 +3288,7 @@ export class ButtonActionExecutor { } // 3. ๋™์  ๋ชจ๋‹ฌ ์ œ๋ชฉ ์ƒ์„ฑ - let finalTitle = config.modalTitle || "ํ™”๋ฉด"; + let finalTitle = config.modalTitle || screenInfo?.screenName || "๋ฐ์ดํ„ฐ ๋“ฑ๋ก"; // ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ ์ฒ˜๋ฆฌ if (config.modalTitleBlocks?.length) {