diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 4bebdcda..8cac05b4 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -11,6 +11,7 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere import { DynamicWebTypeRenderer } from "@/lib/registry"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; +import { initializeComponents } from "@/lib/registry/components"; export default function ScreenViewPage() { const params = useParams(); @@ -23,6 +24,20 @@ export default function ScreenViewPage() { const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); + useEffect(() => { + const initComponents = async () => { + try { + console.log("๐Ÿš€ ํ• ๋‹น๋œ ํ™”๋ฉด์—์„œ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘..."); + await initializeComponents(); + console.log("โœ… ํ• ๋‹น๋œ ํ™”๋ฉด์—์„œ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ"); + } catch (error) { + console.error("โŒ ํ• ๋‹น๋œ ํ™”๋ฉด์—์„œ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์‹คํŒจ:", error); + } + }; + + initComponents(); + }, []); + useEffect(() => { const loadScreen = async () => { try { @@ -150,10 +165,19 @@ export default function ScreenViewPage() { allComponents={layout.components} formData={formData} onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); + console.log("๐Ÿ“ ํผ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ:", { fieldName, value }); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("๐Ÿ“Š ์ „์ฒด ํผ ๋ฐ์ดํ„ฐ:", newFormData); + return newFormData; + }); + }} + screenInfo={{ + id: screenId, + tableName: screen?.tableName, }} /> @@ -234,6 +258,14 @@ export default function ScreenViewPage() { [fieldName]: value, })); }} + screenId={screenId} + tableName={screen?.tableName} + onRefresh={() => { + console.log("ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ"); + }} + onClose={() => { + console.log("ํ™”๋ฉด ๋‹ซ๊ธฐ ์š”์ฒญ"); + }} /> ) : ( {children} + + diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx new file mode 100644 index 00000000..c9ca3af1 --- /dev/null +++ b/frontend/components/common/ScreenModal.tsx @@ -0,0 +1,224 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; +import { screenApi } from "@/lib/api/screen"; +import { ComponentData } from "@/types/screen"; +import { toast } from "sonner"; + +interface ScreenModalState { + isOpen: boolean; + screenId: number | null; + title: string; + size: "sm" | "md" | "lg" | "xl"; +} + +interface ScreenModalProps { + className?: string; +} + +export const ScreenModal: React.FC = ({ className }) => { + const [modalState, setModalState] = useState({ + isOpen: false, + screenId: null, + title: "", + size: "md", + }); + + const [screenData, setScreenData] = useState<{ + components: ComponentData[]; + screenInfo: any; + } | null>(null); + + const [loading, setLoading] = useState(false); + const [screenDimensions, setScreenDimensions] = useState<{ + width: number; + height: number; + } | null>(null); + + // ํ™”๋ฉด์˜ ์‹ค์ œ ํฌ๊ธฐ ๊ณ„์‚ฐ ํ•จ์ˆ˜ + const calculateScreenDimensions = (components: ComponentData[]) => { + let maxWidth = 800; // ์ตœ์†Œ ๋„ˆ๋น„ + let maxHeight = 600; // ์ตœ์†Œ ๋†’์ด + + components.forEach((component) => { + const x = parseFloat(component.style?.positionX || "0"); + const y = parseFloat(component.style?.positionY || "0"); + const width = parseFloat(component.style?.width || "100"); + const height = parseFloat(component.style?.height || "40"); + + // ์ปดํฌ๋„ŒํŠธ์˜ ์˜ค๋ฅธ์ชฝ ๋๊ณผ ์•„๋ž˜์ชฝ ๋ ๊ณ„์‚ฐ + const rightEdge = x + width; + const bottomEdge = y + height; + + maxWidth = Math.max(maxWidth, rightEdge + 50); // ์—ฌ๋ฐฑ ์ถ”๊ฐ€ + maxHeight = Math.max(maxHeight, bottomEdge + 50); // ์—ฌ๋ฐฑ ์ถ”๊ฐ€ + }); + + return { + width: Math.min(maxWidth, window.innerWidth * 0.9), // ํ™”๋ฉด์˜ 90%๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก + height: Math.min(maxHeight, window.innerHeight * 0.8), // ํ™”๋ฉด์˜ 80%๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก + }; + }; + + // ์ „์—ญ ๋ชจ๋‹ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + useEffect(() => { + const handleOpenModal = (event: CustomEvent) => { + const { screenId, title, size } = event.detail; + setModalState({ + isOpen: true, + screenId, + title, + size, + }); + }; + + window.addEventListener("openScreenModal", handleOpenModal as EventListener); + + return () => { + window.removeEventListener("openScreenModal", handleOpenModal as EventListener); + }; + }, []); + + // ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ + useEffect(() => { + if (modalState.isOpen && modalState.screenId) { + loadScreenData(modalState.screenId); + } + }, [modalState.isOpen, modalState.screenId]); + + const loadScreenData = async (screenId: number) => { + try { + setLoading(true); + + console.log("ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ž‘:", screenId); + + // ํ™”๋ฉด ์ •๋ณด์™€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ + const [screenInfo, layoutData] = await Promise.all([ + screenApi.getScreen(screenId), + screenApi.getLayout(screenId), + ]); + + console.log("API ์‘๋‹ต:", { screenInfo, layoutData }); + + // screenApi๋Š” ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ .success ์ฒดํฌ ๋ถˆํ•„์š” + if (screenInfo && layoutData) { + const components = layoutData.components || []; + + // ํ™”๋ฉด์˜ ์‹ค์ œ ํฌ๊ธฐ ๊ณ„์‚ฐ + const dimensions = calculateScreenDimensions(components); + setScreenDimensions(dimensions); + + setScreenData({ + components, + screenInfo: screenInfo, + }); + console.log("ํ™”๋ฉด ๋ฐ์ดํ„ฐ ์„ค์ • ์™„๋ฃŒ:", { + componentsCount: components.length, + dimensions, + screenInfo, + }); + } else { + throw new Error("ํ™”๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + } + } catch (error) { + console.error("ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error); + toast.error("ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + handleClose(); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setModalState({ + isOpen: false, + screenId: null, + title: "", + size: "md", + }); + setScreenData(null); + }; + + // ๋ชจ๋‹ฌ ํฌ๊ธฐ ์„ค์ • - ํ™”๋ฉด ๋‚ด์šฉ์— ๋งž๊ฒŒ ๋™์  ์กฐ์ • + const getModalStyle = () => { + if (!screenDimensions) { + return { + className: "w-fit min-w-[400px] max-w-4xl max-h-[80vh] overflow-hidden", + style: {} + }; + } + + // ํ—ค๋” ๋†’์ด์™€ ํŒจ๋”ฉ์„ ๊ณ ๋ คํ•œ ์ „์ฒด ๋†’์ด ๊ณ„์‚ฐ + const headerHeight = 60; // DialogHeader + ํŒจ๋”ฉ + const totalHeight = screenDimensions.height + headerHeight; + + return { + className: "overflow-hidden p-0", + style: { + width: `${screenDimensions.width + 48}px`, // ํ—ค๋” ํŒจ๋”ฉ๊ณผ ์—ฌ๋ฐฑ ๊ณ ๋ ค + height: `${Math.min(totalHeight, window.innerHeight * 0.8)}px`, + maxWidth: '90vw', + maxHeight: '80vh' + } + }; + }; + + const modalStyle = getModalStyle(); + + return ( + + + + {modalState.title} + + +
+ {loading ? ( +
+
+
+

ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ) : screenData ? ( +
+ {screenData.components.map((component) => ( + + ))} +
+ ) : ( +
+

ํ™”๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

+
+ )} +
+
+
+ ); +}; + +export default ScreenModal; diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 4cd6c3d4..33d0a2c7 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -165,6 +165,9 @@ export const InteractiveScreenViewerDynamic: React.FC { + // ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ ๋กœ์ง (ํ•„์š”์‹œ ๊ตฌํ˜„) + console.log("ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ"); + }} + onClose={() => { + // ํ™”๋ฉด ๋‹ซ๊ธฐ ๋กœ์ง (ํ•„์š”์‹œ ๊ตฌํ˜„) + console.log("ํ™”๋ฉด ๋‹ซ๊ธฐ ์š”์ฒญ"); + }} + style={{ + width: "100%", + height: "100%", + ...comp.style, + }} /> ); } diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index ed8d872c..087335ac 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -23,6 +23,7 @@ import "@/lib/registry/components"; interface RealtimePreviewProps { component: ComponentData; isSelected?: boolean; + isDesignMode?: boolean; // ํŽธ์ง‘ ๋ชจ๋“œ ์—ฌ๋ถ€ onClick?: (e?: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; @@ -63,6 +64,7 @@ const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => { export const RealtimePreviewDynamic: React.FC = ({ component, isSelected = false, + isDesignMode = true, // ๊ธฐ๋ณธ๊ฐ’์€ ํŽธ์ง‘ ๋ชจ๋“œ onClick, onDragStart, onDragEnd, @@ -122,6 +124,8 @@ export const RealtimePreviewDynamic: React.FC = ({ { + const initComponents = async () => { + try { + console.log("๐Ÿš€ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘..."); + await initializeComponents(); + console.log("โœ… ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ"); + } catch (error) { + console.error("โŒ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์‹คํŒจ:", error); + } + }; + + initComponents(); + }, []); + // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ (์„ฑ๋Šฅ ์ตœ์ ํ™”: ์„ ํƒ๋œ ํ…Œ์ด๋ธ”๋งŒ ์กฐํšŒ) useEffect(() => { if (selectedScreen?.tableName && selectedScreen.tableName.trim()) { @@ -1499,7 +1516,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const newComponent: ComponentData = { id: generateComponentId(), - type: "widget", // ์ƒˆ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ชจ๋‘ widget ํƒ€์ž… + type: "component", // โœ… ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ label: component.name, widgetType: component.webType, componentType: component.id, // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์˜ ID (DynamicComponentRenderer์šฉ) @@ -1507,11 +1524,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD size: component.defaultSize, componentConfig: { type: component.id, // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์˜ ID ์‚ฌ์šฉ + webType: component.webType, // ์›นํƒ€์ž… ์ •๋ณด ์ถ”๊ฐ€ ...component.defaultConfig, }, webTypeConfig: getDefaultWebTypeConfig(component.webType), style: { - labelDisplay: true, + labelDisplay: component.id === "text-display" ? false : true, // ํ…์ŠคํŠธ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ผ๋ฒจ ์ˆจ๊น€ labelFontSize: "14px", labelColor: "#374151", labelFontWeight: "500", @@ -1547,10 +1565,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD e.preventDefault(); const dragData = e.dataTransfer.getData("application/json"); - if (!dragData) return; + console.log("๐ŸŽฏ ๋“œ๋กญ ์ด๋ฒคํŠธ:", { dragData }); + if (!dragData) { + console.log("โŒ ๋“œ๋ž˜๊ทธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + return; + } try { const parsedData = JSON.parse(dragData); + console.log("๐Ÿ“‹ ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ:", parsedData); // ํ…œํ”Œ๋ฆฟ ๋“œ๋ž˜๊ทธ์ธ ๊ฒฝ์šฐ if (parsedData.type === "template") { @@ -1603,6 +1626,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }, }; } else if (type === "column") { + console.log("๐Ÿ”„ ์ปฌ๋Ÿผ ๋“œ๋กญ ์ฒ˜๋ฆฌ:", { webType: column.widgetType, columnName: column.columnName }); // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด๋กœ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ const currentGridInfo = layout.gridSettings ? calculateGridInfo(screenResolution.width, screenResolution.height, { @@ -1767,18 +1791,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const relativeX = e.clientX - containerRect.left; const relativeY = e.clientY - containerRect.top; + // ์›นํƒ€์ž…์„ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ID๋กœ ๋งคํ•‘ + const componentId = getComponentIdFromWebType(column.widgetType); + console.log(`๐Ÿ”„ ํผ ์ปจํ…Œ์ด๋„ˆ ๋“œ๋กญ: ${column.widgetType} โ†’ ${componentId}`); + newComponent = { id: generateComponentId(), - type: "widget", - label: column.columnName, + type: "component", // โœ… ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ + label: column.columnLabel || column.columnName, tableName: table.tableName, columnName: column.columnName, - widgetType: column.widgetType, required: column.required, readonly: false, parentId: formContainerId, // ํผ ์ปจํ…Œ์ด๋„ˆ์˜ ์ž์‹์œผ๋กœ ์„ค์ • position: { x: relativeX, y: relativeY, z: 1 } as Position, size: { width: defaultWidth, height: 40 }, + gridColumns: 1, style: { labelDisplay: true, labelFontSize: "12px", @@ -1786,20 +1814,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD labelFontWeight: "500", labelMarginBottom: "6px", }, - webTypeConfig: getDefaultWebTypeConfig(column.widgetType), + componentConfig: { + type: componentId, // text-input, number-input ๋“ฑ + webType: column.widgetType, // ์›๋ณธ ์›นํƒ€์ž… ๋ณด์กด + ...getDefaultWebTypeConfig(column.widgetType), + }, }; } else { return; // ํผ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด ๋“œ๋กญ ์ทจ์†Œ } } else { - // ์ผ๋ฐ˜ ์บ”๋ฒ„์Šค์— ๋“œ๋กญํ•œ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) + // ์ผ๋ฐ˜ ์บ”๋ฒ„์Šค์— ๋“œ๋กญํ•œ ๊ฒฝ์šฐ - ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ + const componentId = getComponentIdFromWebType(column.widgetType); + console.log(`๐Ÿ”„ ์บ”๋ฒ„์Šค ๋“œ๋กญ: ${column.widgetType} โ†’ ${componentId}`); + newComponent = { id: generateComponentId(), - type: "widget", + type: "component", // โœ… ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ label: column.columnLabel || column.columnName, // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์šฐ์„ , ์—†์œผ๋ฉด ์ปฌ๋Ÿผ๋ช… tableName: table.tableName, columnName: column.columnName, - widgetType: column.widgetType, required: column.required, readonly: false, position: { x, y, z: 1 } as Position, @@ -1812,7 +1846,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD labelFontWeight: "500", labelMarginBottom: "6px", }, - webTypeConfig: getDefaultWebTypeConfig(column.widgetType), + componentConfig: { + type: componentId, // text-input, number-input ๋“ฑ + webType: column.widgetType, // ์›๋ณธ ์›นํƒ€์ž… ๋ณด์กด + ...getDefaultWebTypeConfig(column.widgetType), + }, }; } } else { @@ -3093,7 +3131,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD onDrop={(e) => { e.preventDefault(); console.log("๐ŸŽฏ ์บ”๋ฒ„์Šค ๋“œ๋กญ ์ด๋ฒคํŠธ ๋ฐœ์ƒ"); - handleComponentDrop(e); + handleDrop(e); }} > {/* ๊ฒฉ์ž ๋ผ์ธ */} @@ -3176,6 +3214,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD isSelected={ selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id) } + isDesignMode={true} // ํŽธ์ง‘ ๋ชจ๋“œ๋กœ ์„ค์ • onClick={(e) => handleComponentClick(component, e)} onDragStart={(e) => startComponentDrag(component, e)} onDragEnd={endDrag} @@ -3255,6 +3294,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD isSelected={ selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id) } + isDesignMode={true} // ํŽธ์ง‘ ๋ชจ๋“œ๋กœ ์„ค์ • onClick={(e) => handleComponentClick(child, e)} onDragStart={(e) => startComponentDrag(child, e)} onDragEnd={endDrag} @@ -3323,11 +3363,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD searchTerm={searchTerm} onSearchChange={setSearchTerm} onDragStart={(e, table, column) => { + console.log("๐Ÿš€ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘:", { table: table.tableName, column: column?.columnName }); const dragData = { type: column ? "column" : "table", table, column, }; + console.log("๐Ÿ“ฆ ๋“œ๋ž˜๊ทธ ๋ฐ์ดํ„ฐ:", dragData); e.dataTransfer.setData("application/json", JSON.stringify(dragData)); }} selectedTableName={selectedScreen.tableName} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 002de5de..35362caa 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -1,19 +1,82 @@ "use client"; -import React from "react"; +import React, { useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Check, ChevronsUpDown, Search } from "lucide-react"; +import { cn } from "@/lib/utils"; import { ComponentData } from "@/types/screen"; +import { apiClient } from "@/lib/api/client"; interface ButtonConfigPanelProps { component: ComponentData; onUpdateProperty: (path: string, value: any) => void; } +interface ScreenOption { + id: number; + name: string; + description?: string; +} + export const ButtonConfigPanel: React.FC = ({ component, onUpdateProperty }) => { const config = component.componentConfig || {}; + const [screens, setScreens] = useState([]); + const [screensLoading, setScreensLoading] = useState(false); + const [modalScreenOpen, setModalScreenOpen] = useState(false); + const [navScreenOpen, setNavScreenOpen] = useState(false); + const [modalSearchTerm, setModalSearchTerm] = useState(""); + const [navSearchTerm, setNavSearchTerm] = useState(""); + + // ํ™”๋ฉด ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ + useEffect(() => { + const fetchScreens = async () => { + try { + setScreensLoading(true); + console.log("๐Ÿ” ํ™”๋ฉด ๋ชฉ๋ก API ํ˜ธ์ถœ ์‹œ์ž‘"); + const response = await apiClient.get("/screen-management/screens"); + console.log("โœ… ํ™”๋ฉด ๋ชฉ๋ก API ์‘๋‹ต:", response.data); + + if (response.data.success && Array.isArray(response.data.data)) { + const screenList = response.data.data.map((screen: any) => ({ + id: screen.screenId, + name: screen.screenName, + description: screen.description, + })); + setScreens(screenList); + console.log("โœ… ํ™”๋ฉด ๋ชฉ๋ก ์„ค์ • ์™„๋ฃŒ:", screenList.length, "๊ฐœ"); + } + } catch (error) { + console.error("โŒ ํ™”๋ฉด ๋ชฉ๋ก ๋กœ๋”ฉ ์‹คํŒจ:", error); + } finally { + setScreensLoading(false); + } + }; + + fetchScreens(); + }, []); + + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง ํ•จ์ˆ˜ + const filterScreens = (searchTerm: string) => { + if (!searchTerm.trim()) return screens; + return screens.filter( + (screen) => + screen.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase())) + ); + }; + + console.log("๐Ÿ”ง config-panels/ButtonConfigPanel ๋ Œ๋”๋ง:", { + component, + config, + action: config.action, + actionType: config.action?.type, + screensCount: screens.length, + }); return (
@@ -68,8 +131,9 @@ export const ButtonConfigPanel: React.FC = ({ component,
+ + {/* ๋ชจ๋‹ฌ ์—ด๊ธฐ ์•ก์…˜ ์„ค์ • */} + {config.action?.type === "modal" && ( +
+

๋ชจ๋‹ฌ ์„ค์ •

+ +
+ + + onUpdateProperty("componentConfig.action", { + ...config.action, + modalTitle: e.target.value, + }) + } + /> +
+ +
+ + +
+ +
+ + + + + + +
+ {/* ๊ฒ€์ƒ‰ ์ž…๋ ฅ */} +
+ + setModalSearchTerm(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+ {/* ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ */} +
+ {(() => { + const filteredScreens = filterScreens(modalSearchTerm); + if (screensLoading) { + return
ํ™”๋ฉด ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...
; + } + if (filteredScreens.length === 0) { + return
๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
; + } + return filteredScreens.map((screen, index) => ( +
{ + onUpdateProperty("componentConfig.action", { + ...config.action, + targetScreenId: screen.id, + }); + setModalScreenOpen(false); + setModalSearchTerm(""); + }} + > + +
+ {screen.name} + {screen.description && {screen.description}} +
+
+ )); + })()} +
+
+
+
+
+
+ )} + + {/* ํŽ˜์ด์ง€ ์ด๋™ ์•ก์…˜ ์„ค์ • */} + {config.action?.type === "navigate" && ( +
+

ํŽ˜์ด์ง€ ์ด๋™ ์„ค์ •

+ +
+ + + + + + +
+ {/* ๊ฒ€์ƒ‰ ์ž…๋ ฅ */} +
+ + setNavSearchTerm(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+ {/* ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ */} +
+ {(() => { + const filteredScreens = filterScreens(navSearchTerm); + if (screensLoading) { + return
ํ™”๋ฉด ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...
; + } + if (filteredScreens.length === 0) { + return
๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
; + } + return filteredScreens.map((screen, index) => ( +
{ + onUpdateProperty("componentConfig.action", { + ...config.action, + targetScreenId: screen.id, + }); + setNavScreenOpen(false); + setNavSearchTerm(""); + }} + > + +
+ {screen.name} + {screen.description && {screen.description}} +
+
+ )); + })()} +
+
+
+
+

+ ์„ ํƒํ•œ ํ™”๋ฉด์œผ๋กœ /screens/{"{"}ํ™”๋ฉดID{"}"} ํ˜•ํƒœ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค +

+
+ +
+ + + onUpdateProperty("componentConfig.action", { + ...config.action, + targetUrl: e.target.value, + }) + } + /> +

URL์„ ์ž…๋ ฅํ•˜๋ฉด ํ™”๋ฉด ์„ ํƒ๋ณด๋‹ค ์šฐ์„  ์ ์šฉ๋ฉ๋‹ˆ๋‹ค

+
+
+ )} + + {/* ํ™•์ธ ๋ฉ”์‹œ์ง€ ์„ค์ • (๋ชจ๋“  ์•ก์…˜ ๊ณตํ†ต) */} + {config.action?.type && config.action.type !== "cancel" && config.action.type !== "close" && ( +
+

ํ™•์ธ ๋ฉ”์‹œ์ง€ ์„ค์ •

+ +
+ + + onUpdateProperty("componentConfig.action", { + ...config.action, + confirmMessage: e.target.value, + }) + } + /> +
+ +
+ + + onUpdateProperty("componentConfig.action", { + ...config.action, + successMessage: e.target.value, + }) + } + /> +
+ +
+ + + onUpdateProperty("componentConfig.action", { + ...config.action, + errorMessage: e.target.value, + }) + } + /> +
+
+ )}
); }; diff --git a/frontend/components/screen/panels/ButtonConfigPanel.tsx b/frontend/components/screen/panels/ButtonConfigPanel.tsx deleted file mode 100644 index 82436b98..00000000 --- a/frontend/components/screen/panels/ButtonConfigPanel.tsx +++ /dev/null @@ -1,537 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { Badge } from "@/components/ui/badge"; -import { - Save, - X, - Trash2, - Edit, - Plus, - RotateCcw, - Send, - ExternalLink, - MousePointer, - Settings, - AlertTriangle, -} from "lucide-react"; -import { ButtonActionType, ButtonTypeConfig, WidgetComponent, ScreenDefinition } from "@/types/screen"; -import { screenApi } from "@/lib/api/screen"; - -interface ButtonConfigPanelProps { - component: WidgetComponent; - onUpdateComponent: (updates: Partial) => void; -} - -const actionTypeOptions: { value: ButtonActionType; label: string; icon: React.ReactNode; color: string }[] = [ - { value: "save", label: "์ €์žฅ", icon: , color: "#3b82f6" }, - { value: "delete", label: "์‚ญ์ œ", icon: , color: "#ef4444" }, - { value: "edit", label: "์ˆ˜์ •", icon: , color: "#f59e0b" }, - { value: "add", label: "์ถ”๊ฐ€", icon: , color: "#10b981" }, - { value: "search", label: "๊ฒ€์ƒ‰", icon: , color: "#8b5cf6" }, - { value: "reset", label: "์ดˆ๊ธฐํ™”", icon: , color: "#6b7280" }, - { value: "submit", label: "์ œ์ถœ", icon: , color: "#059669" }, - { value: "close", label: "๋‹ซ๊ธฐ", icon: , color: "#6b7280" }, - { value: "popup", label: "๋ชจ๋‹ฌ ์—ด๊ธฐ", icon: , color: "#8b5cf6" }, - { value: "navigate", label: "ํŽ˜์ด์ง€ ์ด๋™", icon: , color: "#0ea5e9" }, - { value: "custom", label: "์‚ฌ์šฉ์ž ์ •์˜", icon: , color: "#64748b" }, -]; - -export const ButtonConfigPanel: React.FC = ({ component, onUpdateComponent }) => { - const config = (component.webTypeConfig as ButtonTypeConfig) || {}; - - // ๋กœ์ปฌ ์ƒํƒœ ๊ด€๋ฆฌ - const [localConfig, setLocalConfig] = useState(() => { - const defaultConfig = { - actionType: "custom" as ButtonActionType, - variant: "default" as ButtonVariant, - }; - - return { - ...defaultConfig, - ...config, // ์ €์žฅ๋œ ๊ฐ’์ด ๊ธฐ๋ณธ๊ฐ’์„ ๋ฎ์–ด์”€ - }; - }); - - // ํ™”๋ฉด ๋ชฉ๋ก ์ƒํƒœ - const [screens, setScreens] = useState([]); - const [screensLoading, setScreensLoading] = useState(false); - - // ํ™”๋ฉด ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜ - const loadScreens = async () => { - try { - setScreensLoading(true); - const response = await screenApi.getScreens({ size: 1000 }); // ๋ชจ๋“  ํ™”๋ฉด ๊ฐ€์ ธ์˜ค๊ธฐ - setScreens(response.data); - } catch (error) { - console.error("ํ™”๋ฉด ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); - } finally { - setScreensLoading(false); - } - }; - - // ๋ชจ๋‹ฌ ๋˜๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ ์•ก์…˜ ํƒ€์ž…์ผ ๋•Œ ํ™”๋ฉด ๋ชฉ๋ก ๋กœ๋“œ - useEffect(() => { - if (localConfig.actionType === "popup" || localConfig.actionType === "navigate") { - loadScreens(); - } - }, [localConfig.actionType]); - - // ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” - useEffect(() => { - const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {}; - - // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (์‹ค์ œ ๊ฐ’์ด ์žˆ์œผ๋ฉด ๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ) - const defaultConfig = { - actionType: "custom" as ButtonActionType, - variant: "default" as ButtonVariant, - }; - - // ์‹ค์ œ ์ €์žฅ๋œ ๊ฐ’์ด ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋„๋ก ์„ค์ • - setLocalConfig({ - ...defaultConfig, - ...newConfig, // ์ €์žฅ๋œ ๊ฐ’์ด ๊ธฐ๋ณธ๊ฐ’์„ ๋ฎ์–ด์”€ - }); - - console.log("๐Ÿ”„ ButtonConfigPanel ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™”:", { - componentId: component.id, - savedConfig: newConfig, - finalConfig: { ...defaultConfig, ...newConfig }, - }); - }, [component.webTypeConfig, component.id]); - - // ์„ค์ • ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - const updateConfig = (updates: Partial) => { - const newConfig = { ...localConfig, ...updates }; - setLocalConfig(newConfig); - - // ์Šคํƒ€์ผ ์—…๋ฐ์ดํŠธ๋„ ํ•จ๊ป˜ ์ ์šฉ - const styleUpdates: any = {}; - if (updates.backgroundColor) styleUpdates.backgroundColor = updates.backgroundColor; - if (updates.textColor) styleUpdates.color = updates.textColor; - if (updates.borderColor) styleUpdates.borderColor = updates.borderColor; - - onUpdateComponent({ - webTypeConfig: newConfig, - ...(Object.keys(styleUpdates).length > 0 && { - style: { ...component.style, ...styleUpdates }, - }), - }); - }; - - // ์•ก์…˜ ํƒ€์ž… ๋ณ€๊ฒฝ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - const handleActionTypeChange = (actionType: ButtonActionType) => { - const actionOption = actionTypeOptions.find((opt) => opt.value === actionType); - const updates: Partial = { actionType }; - - // ์•ก์…˜ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๊ธฐ๋ณธ ์„ค์ • - switch (actionType) { - case "save": - updates.variant = "default"; - updates.backgroundColor = "#3b82f6"; - updates.textColor = "#ffffff"; - // ๋ฒ„ํŠผ ๋ผ๋ฒจ๊ณผ ์Šคํƒ€์ผ๋„ ์—…๋ฐ์ดํŠธ - onUpdateComponent({ - label: "์ €์žฅ", - style: { ...component.style, backgroundColor: "#3b82f6", color: "#ffffff" }, - }); - break; - case "close": - updates.variant = "outline"; - updates.backgroundColor = "transparent"; - updates.textColor = "#6b7280"; - onUpdateComponent({ - label: "๋‹ซ๊ธฐ", - style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" }, - }); - break; - case "delete": - updates.variant = "destructive"; - updates.backgroundColor = "#ef4444"; - updates.textColor = "#ffffff"; - updates.confirmMessage = "์ •๋ง๋กœ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?"; - onUpdateComponent({ - label: "์‚ญ์ œ", - style: { ...component.style, backgroundColor: "#ef4444", color: "#ffffff" }, - }); - break; - case "edit": - updates.backgroundColor = "#f59e0b"; - updates.textColor = "#ffffff"; - onUpdateComponent({ - label: "์ˆ˜์ •", - style: { ...component.style, backgroundColor: "#f59e0b", color: "#ffffff" }, - }); - break; - case "add": - updates.backgroundColor = "#10b981"; - updates.textColor = "#ffffff"; - onUpdateComponent({ - label: "์ถ”๊ฐ€", - style: { ...component.style, backgroundColor: "#10b981", color: "#ffffff" }, - }); - break; - case "search": - updates.backgroundColor = "#8b5cf6"; - updates.textColor = "#ffffff"; - onUpdateComponent({ - label: "๊ฒ€์ƒ‰", - style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" }, - }); - break; - case "reset": - updates.variant = "outline"; - updates.backgroundColor = "transparent"; - updates.textColor = "#6b7280"; - onUpdateComponent({ - label: "์ดˆ๊ธฐํ™”", - style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" }, - }); - break; - case "submit": - updates.backgroundColor = "#059669"; - updates.textColor = "#ffffff"; - onUpdateComponent({ - label: "์ œ์ถœ", - style: { ...component.style, backgroundColor: "#059669", color: "#ffffff" }, - }); - break; - case "popup": - updates.backgroundColor = "#8b5cf6"; - updates.textColor = "#ffffff"; - updates.popupTitle = "์ƒ์„ธ ์ •๋ณด"; - updates.popupContent = "์—ฌ๊ธฐ์— ๋ชจ๋‹ฌ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”."; - updates.popupSize = "md"; - onUpdateComponent({ - label: "์ƒ์„ธ๋ณด๊ธฐ", - style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" }, - }); - break; - case "navigate": - updates.backgroundColor = "#0ea5e9"; - updates.textColor = "#ffffff"; - updates.navigateType = "url"; - updates.navigateUrl = "/"; - updates.navigateTarget = "_self"; - onUpdateComponent({ - label: "์ด๋™", - style: { ...component.style, backgroundColor: "#0ea5e9", color: "#ffffff" }, - }); - break; - case "custom": - updates.backgroundColor = "#64748b"; - updates.textColor = "#ffffff"; - onUpdateComponent({ - label: "๋ฒ„ํŠผ", - style: { ...component.style, backgroundColor: "#64748b", color: "#ffffff" }, - }); - break; - } - - // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ›„ webTypeConfig๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ - const newConfig = { ...localConfig, ...updates }; - setLocalConfig(newConfig); - - // webTypeConfig๋ฅผ ๋งˆ์ง€๋ง‰์— ๋‹ค์‹œ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ํ™•์‹คํžˆ ์ €์žฅ๋˜๋„๋ก ํ•จ - setTimeout(() => { - onUpdateComponent({ - webTypeConfig: newConfig, - }); - - console.log("๐ŸŽฏ ButtonActionType webTypeConfig ์ตœ์ข… ์—…๋ฐ์ดํŠธ:", { - actionType, - newConfig, - componentId: component.id, - }); - }, 0); - }; - - const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType); - - return ( -
- - - - - ๋ฒ„ํŠผ ๊ธฐ๋Šฅ ์„ค์ • - - - - {/* ์•ก์…˜ ํƒ€์ž… ์„ ํƒ */} -
- - - {selectedActionOption && ( -
- {selectedActionOption.icon} - {selectedActionOption.label} - - {selectedActionOption.value} - -
- )} -
- - - - {/* ๊ธฐ๋ณธ ์„ค์ • */} -
- - - {/* ๋ฒ„ํŠผ ํ…์ŠคํŠธ */} -
- - { - const newValue = e.target.value; - onUpdateComponent({ label: newValue }); - }} - placeholder="๋ฒ„ํŠผ์— ํ‘œ์‹œ๋  ํ…์ŠคํŠธ" - className="h-8 text-xs" - /> -
- - {/* ๋ฒ„ํŠผ ์Šคํƒ€์ผ */} -
-
- - -
-
- - {/* ์•„์ด์ฝ˜ ์„ค์ • */} -
- - updateConfig({ icon: e.target.value })} - placeholder="์˜ˆ: Save, Edit, Trash2" - className="h-8 text-xs" - /> -
-
- - - - {/* ์•ก์…˜๋ณ„ ์„ธ๋ถ€ ์„ค์ • */} - {localConfig.actionType === "delete" && ( -
- -
- - updateConfig({ confirmMessage: e.target.value })} - placeholder="์ •๋ง๋กœ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" - className="h-8 text-xs" - /> -
-
- )} - - {localConfig.actionType === "popup" && ( -
- -
-
- - - {localConfig.popupScreenId &&

์„ ํƒ๋œ ํ™”๋ฉด์ด ๋ชจ๋‹ฌ๋กœ ์—ด๋ฆฝ๋‹ˆ๋‹ค

} -
-
- - updateConfig({ popupTitle: e.target.value })} - placeholder="์ƒ์„ธ ์ •๋ณด" - className="h-8 text-xs" - /> -
- {!localConfig.popupScreenId && ( -
- -