From 9d74baf60ab285ba737e304a85cdb96d4de2570c Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 16 Jan 2026 15:12:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=94=BC=ED=84=B0=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80:=20ScreenDesigner=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EB=A6=AC=ED=94=BC=ED=84=B0=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EB=82=B4=EB=B6=80=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC,=20=EB=93=9C=EB=A1=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=9E=90=EC=8B=9D=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EA=B3=A0=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=9D=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20=EB=98=90=ED=95=9C,=20TableListC?= =?UTF-8?q?omponent=EC=97=90=EC=84=9C=20=EB=A6=AC=ED=94=BC=ED=84=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=EC=99=80=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=9C=84=EC=A0=AF=20=EC=97=B0=EB=8F=99=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=EB=B0=9C=EC=83=9D=EC=8B=9C?= =?UTF-8?q?=EC=BC=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=EC=9D=84=20=EC=B2=98=EB=A6=AC=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 101 ++- frontend/lib/registry/components/index.ts | 3 + .../RepeatContainerComponent.tsx | 656 ++++++++++++++ .../RepeatContainerConfigPanel.tsx | 803 ++++++++++++++++++ .../RepeatContainerRenderer.tsx | 12 + .../components/repeat-container/index.ts | 60 ++ .../components/repeat-container/types.ts | 187 ++++ .../table-list/TableListComponent.tsx | 39 + 8 files changed, 1859 insertions(+), 2 deletions(-) create mode 100644 frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx create mode 100644 frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx create mode 100644 frontend/lib/registry/components/repeat-container/RepeatContainerRenderer.tsx create mode 100644 frontend/lib/registry/components/repeat-container/index.ts create mode 100644 frontend/lib/registry/components/repeat-container/types.ts diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index aa2b83e7..eb92a005 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -2222,6 +2222,56 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD return; } } + + // ๐ŸŽฏ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ๋“œ๋กญ ์ฒ˜๋ฆฌ + const dropTarget = e.target as HTMLElement; + const repeatContainer = dropTarget.closest('[data-repeat-container="true"]'); + if (repeatContainer) { + const containerId = repeatContainer.getAttribute("data-component-id"); + if (containerId) { + console.log("๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ๋“œ๋กญ ๊ฐ์ง€:", { containerId, component }); + + // ํ•ด๋‹น ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ฐพ๊ธฐ + const targetComponent = layout.components.find((c) => c.id === containerId); + if (targetComponent && (targetComponent as any).componentType === "repeat-container") { + const currentConfig = (targetComponent as any).componentConfig || {}; + const currentChildren = currentConfig.children || []; + + // ์ƒˆ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ + const newChild = { + id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: component.id || component.componentType || "text-display", + label: component.name || component.label || "์ƒˆ ์ปดํฌ๋„ŒํŠธ", + fieldName: "", + position: { x: 0, y: currentChildren.length * 40 }, + size: component.defaultSize || { width: 200, height: 32 }, + componentConfig: component.defaultConfig || {}, + }; + + // ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ + const updatedComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + children: [...currentChildren, newChild], + }, + }; + + const newLayout = { + ...layout, + components: layout.components.map((c) => + c.id === containerId ? updatedComponent : c + ), + }; + + setLayout(newLayout); + saveToHistory(newLayout); + console.log("๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ์™„๋ฃŒ:", newChild); + return; // ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ + } + } + } + const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; @@ -2562,6 +2612,52 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const dropTarget = e.target as HTMLElement; const formContainer = dropTarget.closest('[data-form-container="true"]'); + // ๐ŸŽฏ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ์ปฌ๋Ÿผ ๋“œ๋กญ ์‹œ ์ฒ˜๋ฆฌ + const repeatContainer = dropTarget.closest('[data-repeat-container="true"]'); + if (repeatContainer && type === "column" && column) { + const containerId = repeatContainer.getAttribute("data-component-id"); + if (containerId) { + console.log("๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ์ปฌ๋Ÿผ ๋“œ๋กญ:", { containerId, column }); + + const targetComponent = layout.components.find((c) => c.id === containerId); + if (targetComponent && (targetComponent as any).componentType === "repeat-container") { + const currentConfig = (targetComponent as any).componentConfig || {}; + const currentChildren = currentConfig.children || []; + + // ์ƒˆ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ (์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜) + const newChild = { + id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: column.widgetType || "text-display", + label: column.columnLabel || column.columnName, + fieldName: column.columnName, + position: { x: 0, y: currentChildren.length * 40 }, + size: { width: 200, height: 32 }, + componentConfig: {}, + }; + + const updatedComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + children: [...currentChildren, newChild], + }, + }; + + const newLayout = { + ...layout, + components: layout.components.map((c) => + c.id === containerId ? updatedComponent : c + ), + }; + + setLayout(newLayout); + saveToHistory(newLayout); + console.log("๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ์— ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ์™„๋ฃŒ:", newChild); + return; + } + } + } + const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; @@ -4687,9 +4783,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }} - onDrop={(e) => { + onDropCapture={(e) => { + // ์บก์ฒ˜ ๋‹จ๊ณ„์—์„œ ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ž์‹ ์š”์†Œ ๋“œ๋กญ๋„ ๊ฐ์ง€ e.preventDefault(); - // console.log("๐ŸŽฏ ์บ”๋ฒ„์Šค ๋“œ๋กญ ์ด๋ฒคํŠธ ๋ฐœ์ƒ"); + console.log("๐ŸŽฏ ์บ”๋ฒ„์Šค ๋“œ๋กญ ์ด๋ฒคํŠธ (์บก์ฒ˜), target:", (e.target as HTMLElement).tagName, (e.target as HTMLElement).getAttribute("data-repeat-container")); handleDrop(e); }} > diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 563c184f..156a3f01 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -97,6 +97,9 @@ import "./pivot-grid/PivotGridRenderer"; // ํ”ผ๋ฒ— ํ…Œ์ด๋ธ” (ํ–‰/์—ด ๊ทธ๋ฃนํ™”, // ๐Ÿ†• ์ง‘๊ณ„ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ import "./aggregation-widget/AggregationWidgetRenderer"; // ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ (ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ ๋“ฑ) +// ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ +import "./repeat-container/RepeatContainerRenderer"; // ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต ๋ Œ๋”๋ง + /** * ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ */ diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx new file mode 100644 index 00000000..bb689c98 --- /dev/null +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx @@ -0,0 +1,656 @@ +"use client"; + +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { ComponentRendererProps } from "@/types/component"; +import { RepeatContainerConfig, RepeatItemContext, SlotComponentConfig } from "./types"; +import { Repeat, Package, ChevronLeft, ChevronRight, Plus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import DynamicComponentRenderer from "@/lib/registry/DynamicComponentRenderer"; + +interface RepeatContainerComponentProps extends ComponentRendererProps { + config?: RepeatContainerConfig; + // ์™ธ๋ถ€์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ + externalData?: any[]; + // ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ์Šฌ๋กฏ (children ๋Œ€์šฉ) + renderItem?: (context: RepeatItemContext) => React.ReactNode; + // formData ์ ‘๊ทผ + formData?: Record; + // formData ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + onFormDataChange?: (key: string, value: any) => void; + // ์„ ํƒ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + onSelectionChange?: (selectedData: any[]) => void; + // ์‚ฌ์šฉ์ž ์ •๋ณด + userId?: string; + userName?: string; + companyCode?: string; + // ํ™”๋ฉด ์ •๋ณด + screenId?: number; + screenTableName?: string; + // ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ (๋””์ž์ธ ๋ชจ๋“œ์—์„œ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์šฉ) + onUpdateComponent?: (updates: Partial) => void; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ + * ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปจํ…์ธ ๋ฅผ ๋ฐ˜๋ณต ๋ Œ๋”๋งํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ + */ +export function RepeatContainerComponent({ + component, + isDesignMode = false, + config: propsConfig, + externalData, + renderItem, + formData = {}, + onFormDataChange, + onSelectionChange, + userId, + userName, + companyCode, + screenId, + screenTableName, + onUpdateComponent, +}: RepeatContainerComponentProps) { + const componentConfig: RepeatContainerConfig = { + dataSourceType: "manual", + layout: "vertical", + gridColumns: 2, + gap: "16px", + showBorder: true, + showShadow: false, + borderRadius: "8px", + backgroundColor: "#ffffff", + padding: "16px", + showItemTitle: false, + itemTitleTemplate: "", + titleFontSize: "14px", + titleColor: "#374151", + titleFontWeight: "600", + emptyMessage: "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", + usePaging: false, + pageSize: 10, + clickable: false, + showSelectedState: true, + selectionMode: "single", + ...propsConfig, + ...component?.config, + ...component?.componentConfig, + }; + + const { + dataSourceType, + dataSourceComponentId, + tableName, + customTableName, + useCustomTable, + layout, + gridColumns, + gap, + itemMinWidth, + itemMaxWidth, + itemHeight, + showBorder, + showShadow, + borderRadius, + backgroundColor, + padding, + showItemTitle, + itemTitleTemplate, + titleFontSize, + titleColor, + titleFontWeight, + filterField, + filterColumn, + useGrouping, + groupByField, + children: slotChildren, + emptyMessage, + usePaging, + pageSize, + clickable, + showSelectedState, + selectionMode, + } = componentConfig; + + // ๋ฐ์ดํ„ฐ ์ƒํƒœ + const [data, setData] = useState([]); + const [selectedIndices, setSelectedIndices] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + // ์‹ค์ œ ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ”๋ช… + const effectiveTableName = useCustomTable ? customTableName : tableName; + + // ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ + useEffect(() => { + if (externalData && Array.isArray(externalData)) { + setData(externalData); + } + }, [externalData]); + + // ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ (componentId ๋˜๋Š” tableName์œผ๋กœ ๋งค์นญ) + useEffect(() => { + if (isDesignMode) return; + + console.log("๐Ÿ”„ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก:", { + componentId: component?.id, + dataSourceType, + dataSourceComponentId, + effectiveTableName, + }); + + // dataSourceComponentId๊ฐ€ ์—†์–ด๋„ ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ ๋งค์นญ ๊ฐ€๋Šฅ + const handleDataChange = (event: CustomEvent) => { + const { componentId, tableName: eventTableName, data: eventData } = event.detail || {}; + + console.log("๐Ÿ“ฉ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { + eventType: event.type, + fromComponentId: componentId, + fromTableName: eventTableName, + dataCount: Array.isArray(eventData) ? eventData.length : 0, + myDataSourceComponentId: dataSourceComponentId, + myEffectiveTableName: effectiveTableName, + }); + + // 1. ๋ช…์‹œ์ ์œผ๋กœ dataSourceComponentId๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋งŒ ๋งค์นญ + if (dataSourceComponentId) { + if (componentId === dataSourceComponentId && Array.isArray(eventData)) { + console.log("โœ… ๋ฆฌํ”ผํ„ฐ: ์ปดํฌ๋„ŒํŠธ ID๋กœ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์„ฑ๊ณต", { componentId, count: eventData.length }); + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } else { + console.log("โš ๏ธ ๋ฆฌํ”ผํ„ฐ: ์ปดํฌ๋„ŒํŠธ ID ๋ถˆ์ผ์น˜๋กœ ๋ฌด์‹œ", { expected: dataSourceComponentId, received: componentId }); + } + return; + } + + // 2. dataSourceComponentId๊ฐ€ ์—†์œผ๋ฉด ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ ๋งค์นญ + if (effectiveTableName && eventTableName === effectiveTableName && Array.isArray(eventData)) { + console.log("โœ… ๋ฆฌํ”ผํ„ฐ: ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์„ฑ๊ณต", { tableName: eventTableName, count: eventData.length }); + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } else if (effectiveTableName) { + console.log("โš ๏ธ ๋ฆฌํ”ผํ„ฐ: ํ…Œ์ด๋ธ”๋ช… ๋ถˆ์ผ์น˜๋กœ ๋ฌด์‹œ", { expected: effectiveTableName, received: eventTableName }); + } + }; + + window.addEventListener("repeaterDataChange" as any, handleDataChange); + window.addEventListener("tableListDataChange" as any, handleDataChange); + + return () => { + window.removeEventListener("repeaterDataChange" as any, handleDataChange); + window.removeEventListener("tableListDataChange" as any, handleDataChange); + }; + }, [component?.id, dataSourceType, dataSourceComponentId, effectiveTableName, isDesignMode]); + + // ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ + const filteredData = useMemo(() => { + if (!filterField || !filterColumn) return data; + + const filterValue = formData[filterField]; + if (filterValue === undefined || filterValue === null) return data; + + if (Array.isArray(filterValue)) { + return data.filter((row) => filterValue.includes(row[filterColumn])); + } + + return data.filter((row) => row[filterColumn] === filterValue); + }, [data, filterField, filterColumn, formData]); + + // ๊ทธ๋ฃนํ•‘๋œ ๋ฐ์ดํ„ฐ + const groupedData = useMemo(() => { + if (!useGrouping || !groupByField) return null; + + const groups: Record = {}; + filteredData.forEach((row) => { + const key = String(row[groupByField] ?? "๊ธฐํƒ€"); + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(row); + }); + + return groups; + }, [filteredData, useGrouping, groupByField]); + + // ํŽ˜์ด์ง•๋œ ๋ฐ์ดํ„ฐ + const paginatedData = useMemo(() => { + if (!usePaging || !pageSize) return filteredData; + + const startIndex = (currentPage - 1) * pageSize; + return filteredData.slice(startIndex, startIndex + pageSize); + }, [filteredData, usePaging, pageSize, currentPage]); + + // ์ด ํŽ˜์ด์ง€ ์ˆ˜ + const totalPages = useMemo(() => { + if (!usePaging || !pageSize || filteredData.length === 0) return 1; + return Math.ceil(filteredData.length / pageSize); + }, [filteredData.length, usePaging, pageSize]); + + // ์•„์ดํ…œ ์ œ๋ชฉ ์ƒ์„ฑ + const generateTitle = useCallback( + (rowData: Record, index: number): string => { + if (!showItemTitle) return ""; + + if (!itemTitleTemplate) { + return `์•„์ดํ…œ ${index + 1}`; + } + + return itemTitleTemplate.replace(/\{([^}]+)\}/g, (match, field) => { + return String(rowData[field] ?? ""); + }); + }, + [showItemTitle, itemTitleTemplate] + ); + + // ์•„์ดํ…œ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleItemClick = useCallback( + (index: number, rowData: any) => { + if (!clickable) return; + + let newSelectedIndices: number[]; + + if (selectionMode === "multiple") { + if (selectedIndices.includes(index)) { + newSelectedIndices = selectedIndices.filter((i) => i !== index); + } else { + newSelectedIndices = [...selectedIndices, index]; + } + } else { + newSelectedIndices = selectedIndices.includes(index) ? [] : [index]; + } + + setSelectedIndices(newSelectedIndices); + + if (onSelectionChange) { + const selectedData = newSelectedIndices.map((i) => paginatedData[i]); + onSelectionChange(selectedData); + } + }, + [clickable, selectionMode, selectedIndices, paginatedData, onSelectionChange] + ); + + // ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ ๊ณ„์‚ฐ + const layoutStyle = useMemo(() => { + const baseStyle: React.CSSProperties = { + gap: gap || "16px", + }; + + switch (layout) { + case "horizontal": + return { + ...baseStyle, + display: "flex", + flexDirection: "row" as const, + flexWrap: "wrap" as const, + }; + case "grid": + return { + ...baseStyle, + display: "grid", + gridTemplateColumns: `repeat(${gridColumns || 2}, 1fr)`, + }; + case "vertical": + default: + return { + ...baseStyle, + display: "flex", + flexDirection: "column" as const, + }; + } + }, [layout, gap, gridColumns]); + + // ์•„์ดํ…œ ์Šคํƒ€์ผ ๊ณ„์‚ฐ + const itemStyle = useMemo((): React.CSSProperties => { + return { + minWidth: itemMinWidth, + maxWidth: itemMaxWidth, + height: itemHeight, + backgroundColor: backgroundColor || "#ffffff", + borderRadius: borderRadius || "8px", + padding: padding || "16px", + border: showBorder ? "1px solid #e5e7eb" : "none", + boxShadow: showShadow ? "0 1px 3px rgba(0,0,0,0.1)" : "none", + }; + }, [itemMinWidth, itemMaxWidth, itemHeight, backgroundColor, borderRadius, padding, showBorder, showShadow]); + + // ์Šฌ๋กฏ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋ Œ๋”๋ง + const renderSlotChildren = useCallback( + (context: RepeatItemContext) => { + // renderItem prop์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + if (renderItem) { + return renderItem(context); + } + + // ์Šฌ๋กฏ์— ๋ฐฐ์น˜๋œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ + if (!slotChildren || slotChildren.length === 0) { + return ( +
+ ๋ฐ˜๋ณต ์•„์ดํ…œ #{context.index + 1} +
+ ); + } + + // ํ˜„์žฌ ์•„์ดํ…œ ๋ฐ์ดํ„ฐ๋ฅผ formData๋กœ ์ „๋‹ฌ + const itemFormData = { + ...formData, + ...context.data, + _repeatIndex: context.index, + _repeatTotal: context.totalCount, + _isFirst: context.isFirst, + _isLast: context.isLast, + }; + + // ์Šฌ๋กฏ์— ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋ Œ๋”๋ง + return ( +
+ {slotChildren.map((childComp: SlotComponentConfig) => { + const { position = { x: 0, y: 0 }, size = { width: 200, height: 40 } } = childComp; + + // DynamicComponentRenderer๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const componentData = { + id: `${childComp.id}_${context.index}`, + componentType: childComp.componentType, + label: childComp.label, + columnName: childComp.fieldName, + position: { ...position, z: 1 }, + size, + componentConfig: childComp.componentConfig, + style: childComp.style, + }; + + return ( +
+ { + if (onFormDataChange) { + onFormDataChange(`_repeat_${context.index}_${key}`, value); + } + }} + /> +
+ ); + })} +
+ ); + }, + [ + renderItem, + slotChildren, + formData, + screenId, + screenTableName, + effectiveTableName, + userId, + userName, + companyCode, + onFormDataChange, + ] + ); + + // ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ƒํƒœ (์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ์šฉ) + const [isDragOver, setIsDragOver] = useState(false); + + // ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ ํ•ธ๋“ค๋Ÿฌ (์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ๋งŒ) + // ์ค‘์š”: preventDefault()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ๋“œ๋กญ ๊ฐ€๋Šฅ ์˜์—ญ์œผ๋กœ ์ธ์‹๋จ + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(true); + }, []); + + // ๋“œ๋ž˜๊ทธ ๋ฆฌ๋ธŒ ํ•ธ๋“ค๋Ÿฌ + const handleDragLeave = useCallback((e: React.DragEvent) => { + // ์ž์‹ ์š”์†Œ๋กœ ์ด๋™ํ•  ๋•Œ false๊ฐ€ ๋˜์ง€ ์•Š๋„๋ก ์ฒดํฌ + const relatedTarget = e.relatedTarget as HTMLElement; + if (relatedTarget && (e.currentTarget as HTMLElement).contains(relatedTarget)) { + return; + } + setIsDragOver(false); + }, []); + + // ๋””์ž์ธ ๋ชจ๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + if (isDesignMode) { + const previewData = [ + { id: 1, name: "์•„์ดํ…œ 1", value: 100 }, + { id: 2, name: "์•„์ดํ…œ 2", value: 200 }, + { id: 3, name: "์•„์ดํ…œ 3", value: 300 }, + ]; + + const hasChildren = slotChildren && slotChildren.length > 0; + + return ( +
{ + // ์‹œ๊ฐ์  ์ƒํƒœ๋งŒ ๋ฆฌ์…‹, ๋“œ๋กญ ๋กœ์ง์€ ScreenDesigner์—์„œ ์ฒ˜๋ฆฌ + setIsDragOver(false); + // ์ค‘์š”: preventDefault()๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„์•ผ ์ด๋ฒคํŠธ๊ฐ€ ๋ฒ„๋ธ”๋ง๋จ + // ํ•˜์ง€๋งŒ ํ•„์š”ํ•˜๋‹ค๋ฉด ํ˜ธ์ถœํ•ด๋„ ๋จ - ๋ฒ„๋ธ”๋ง๊ณผ ๋ฌด๊ด€ + }} + > +
+
+ + ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ + ({previewData.length}๊ฐœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ) +
+ {isDragOver ? ( +
+ + ์—ฌ๊ธฐ์— ๋†“์œผ์„ธ์š” +
+ ) : !hasChildren ? ( +
+ + ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์„ธ์š” +
+ ) : null} +
+ +
+ {previewData.map((row, index) => { + const context: RepeatItemContext = { + index, + data: row, + totalCount: previewData.length, + isFirst: index === 0, + isLast: index === previewData.length - 1, + }; + + return ( +
+ {showItemTitle && ( +
+ {generateTitle(row, index)} +
+ )} + + {hasChildren ? ( +
+ {/* ๋””์ž์ธ ๋ชจ๋“œ: ๋ฐฐ์น˜๋œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œ์‹œ */} + {slotChildren!.map((child: SlotComponentConfig, childIdx: number) => ( +
+
+ {childIdx + 1} +
+
+
+ {child.label || child.componentType} +
+ {child.fieldName && ( +
+ {child.fieldName} +
+ )} +
+
+ {child.componentType} +
+
+ ))} +
+ ์•„์ดํ…œ #{index + 1} - ์‹คํ–‰ ์‹œ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ +
+
+ ) : ( +
+
+ ๋ฐ˜๋ณต ์•„์ดํ…œ #{index + 1} +
+
+ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ฐฐ์น˜ํ•˜์„ธ์š” +
+
+ )} +
+ ); + })} +
+
+ ); + } + + // ๋นˆ ์ƒํƒœ + if (paginatedData.length === 0 && !isLoading) { + return ( +
+ +

{emptyMessage}

+
+ ); + } + + // ๋กœ๋”ฉ ์ƒํƒœ + if (isLoading) { + return ( +
+
+ ๋กœ๋”ฉ ์ค‘... +
+ ); + } + + // ์‹ค์ œ ๋ Œ๋”๋ง + return ( +
+
+ {paginatedData.map((row, index) => { + const context: RepeatItemContext = { + index, + data: row, + totalCount: filteredData.length, + isFirst: index === 0, + isLast: index === paginatedData.length - 1, + }; + + return ( +
handleItemClick(index, row)} + > + {showItemTitle && ( +
+ {generateTitle(row, index)} +
+ )} + + {renderSlotChildren(context)} +
+ ); + })} +
+ + {/* ํŽ˜์ด์ง• */} + {usePaging && totalPages > 1 && ( +
+ + + + {currentPage} / {totalPages} + + + +
+ )} +
+ ); +} + +export const RepeatContainerWrapper = RepeatContainerComponent; diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx new file mode 100644 index 00000000..ddedded8 --- /dev/null +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx @@ -0,0 +1,803 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, GripVertical, Trash2, Type } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { RepeatContainerConfig, SlotComponentConfig } from "./types"; +import { tableTypeApi } from "@/lib/api/screen"; +import { tableManagementApi } from "@/lib/api/tableManagement"; + +interface RepeatContainerConfigPanelProps { + config: RepeatContainerConfig; + onChange: (config: Partial) => void; + screenTableName?: string; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์„ค์ • ํŒจ๋„ + */ +export function RepeatContainerConfigPanel({ + config, + onChange, + screenTableName, +}: RepeatContainerConfigPanelProps) { + const [availableTables, setAvailableTables] = useState>([]); + const [loadingTables, setLoadingTables] = useState(false); + const [tableComboboxOpen, setTableComboboxOpen] = useState(false); + + // ์ปฌ๋Ÿผ ๊ด€๋ จ ์ƒํƒœ + const [availableColumns, setAvailableColumns] = useState>([]); + const [loadingColumns, setLoadingColumns] = useState(false); + + // ์‹ค์ œ ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ” ์ด๋ฆ„ ๊ณ„์‚ฐ + const targetTableName = useMemo(() => { + if (config.useCustomTable && config.customTableName) { + return config.customTableName; + } + return config.tableName || screenTableName; + }, [config.useCustomTable, config.customTableName, config.tableName, screenTableName]); + + // ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… ์ž๋™ ์„ค์ • (์ดˆ๊ธฐ ํ•œ ๋ฒˆ๋งŒ) + useEffect(() => { + if (screenTableName && !config.tableName && !config.customTableName) { + onChange({ tableName: screenTableName }); + } + }, [screenTableName, config.tableName, config.customTableName, onChange]); + + // ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const fetchTables = async () => { + setLoadingTables(true); + try { + const response = await tableTypeApi.getTables(); + setAvailableTables( + response.map((table: any) => ({ + tableName: table.tableName, + displayName: table.displayName || table.tableName, + })) + ); + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", error); + } finally { + setLoadingTables(false); + } + }; + fetchTables(); + }, []); + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (!targetTableName) { + setAvailableColumns([]); + return; + } + + const fetchColumns = async () => { + setLoadingColumns(true); + try { + const response = await tableManagementApi.getColumnList(targetTableName); + if (response.success && response.data && Array.isArray(response.data)) { + setAvailableColumns( + response.data.map((col: any) => ({ + columnName: col.columnName, + displayName: col.displayName || col.columnLabel || col.columnName, + })) + ); + } + } catch (error) { + console.error("์ปฌ๋Ÿผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", error); + setAvailableColumns([]); + } finally { + setLoadingColumns(false); + } + }; + fetchColumns(); + }, [targetTableName]); + + return ( +
+
๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์„ค์ •
+ + {/* ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ…Œ์ด๋ธ” ์„ค์ • */} +
+
+

๋ฐ์ดํ„ฐ ์†Œ์Šค ํ…Œ์ด๋ธ”

+

๋ฐ˜๋ณต ๋ Œ๋”๋งํ•  ๋ฐ์ดํ„ฐ์˜ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค

+
+
+ + {/* ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ํ‘œ์‹œ (์นด๋“œ ํ˜•ํƒœ) */} +
+ +
+
+ {config.customTableName || config.tableName || screenTableName || "ํ…Œ์ด๋ธ” ๋ฏธ์„ ํƒ"} +
+
+ {config.useCustomTable ? "์ปค์Šคํ…€ ํ…Œ์ด๋ธ”" : "ํ™”๋ฉด ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”"} +
+
+
+ + {/* ํ…Œ์ด๋ธ” ์„ ํƒ Combobox */} + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {/* ๊ทธ๋ฃน 1: ํ™”๋ฉด ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” */} + {screenTableName && ( + + { + onChange({ + useCustomTable: false, + customTableName: undefined, + tableName: screenTableName, + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {screenTableName} + + + )} + + {/* ๊ทธ๋ฃน 2: ์ „์ฒด ํ…Œ์ด๋ธ” */} + + {availableTables + .filter((table) => table.tableName !== screenTableName) + .map((table) => ( + { + onChange({ + useCustomTable: true, + customTableName: table.tableName, + tableName: table.tableName, + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {table.displayName || table.tableName} + + ))} + + + + + +
+ + {/* ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ์—ฐ๊ฒฐ */} +
+
+

๋ฐ์ดํ„ฐ ์†Œ์Šค ์—ฐ๊ฒฐ

+

+ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +

+
+
+ +
+ + +
+ + {config.dataSourceType === "table-list" && ( +
+ + onChange({ dataSourceComponentId: e.target.value })} + placeholder="๋น„์šฐ๋ฉด ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ ์ž๋™ ๋งค์นญ" + className="h-8 text-xs" + /> +

+ ๋น„์›Œ๋‘๋ฉด ์œ„์—์„œ ์„ค์ •ํ•œ ํ…Œ์ด๋ธ”๋ช…๊ณผ ๊ฐ™์€ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค +

+
+ )} +
+ + {/* ์Šฌ๋กฏ ์ปดํฌ๋„ŒํŠธ ์„ค์ • */} + + + {/* ๋ ˆ์ด์•„์›ƒ ์„ค์ • */} +
+
+

๋ ˆ์ด์•„์›ƒ

+
+
+ + {/* ๋ ˆ์ด์•„์›ƒ ํƒ€์ž… ์„ ํƒ */} +
+ +
+ + + +
+
+ + {/* ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ (grid ๋ ˆ์ด์•„์›ƒ์ผ ๋•Œ๋งŒ) */} + {config.layout === "grid" && ( +
+ + +
+ )} + + {/* ๊ฐ„๊ฒฉ */} +
+ + onChange({ gap: e.target.value })} + placeholder="16px" + className="h-8 text-xs" + /> +
+
+ + {/* ์•„์ดํ…œ ์นด๋“œ ์„ค์ • */} +
+
+

์•„์ดํ…œ ์นด๋“œ ์Šคํƒ€์ผ

+
+
+ +
+
+ + onChange({ backgroundColor: e.target.value })} + className="h-8" + /> +
+ +
+ + onChange({ borderRadius: e.target.value })} + placeholder="8px" + className="h-8 text-xs" + /> +
+ +
+ + onChange({ padding: e.target.value })} + placeholder="16px" + className="h-8 text-xs" + /> +
+ +
+ + onChange({ itemHeight: e.target.value })} + placeholder="auto" + className="h-8 text-xs" + /> +
+
+ +
+
+ onChange({ showBorder: checked as boolean })} + /> + +
+ +
+ onChange({ showShadow: checked as boolean })} + /> + +
+
+
+ + {/* ์•„์ดํ…œ ์ œ๋ชฉ ์„ค์ • */} +
+
+ onChange({ showItemTitle: checked as boolean })} + /> + +
+
+ + {config.showItemTitle && ( +
+
+ + onChange({ itemTitleTemplate: e.target.value })} + placeholder="{order_no} - {item_code}" + className="h-8 text-xs" + /> +

+ {"{ํ•„๋“œ๋ช…}"} ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅ +

+
+ +
+
+ + onChange({ titleFontSize: e.target.value })} + placeholder="14px" + className="h-7 text-xs" + /> +
+
+ + onChange({ titleColor: e.target.value })} + className="h-7" + /> +
+
+ + +
+
+
+ )} +
+ + {/* ํŽ˜์ด์ง• ์„ค์ • */} +
+
+ onChange({ usePaging: checked as boolean })} + /> + +
+
+ + {config.usePaging && ( +
+ + +
+ )} +
+ + {/* ์ƒํ˜ธ์ž‘์šฉ ์„ค์ • */} +
+
+

์ƒํ˜ธ์ž‘์šฉ

+
+
+ +
+
+ onChange({ clickable: checked as boolean })} + /> + +
+ + {config.clickable && ( + <> +
+ onChange({ showSelectedState: checked as boolean })} + /> + +
+ +
+ + +
+ + )} +
+
+ + {/* ๋นˆ ์ƒํƒœ ์„ค์ • */} +
+
+

๋นˆ ์ƒํƒœ ๋ฉ”์‹œ์ง€

+
+
+ +
+ + onChange({ emptyMessage: e.target.value })} + placeholder="๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" + className="h-8 text-xs" + /> +
+
+
+ ); +} + +// ============================================================ +// ์Šฌ๋กฏ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๊ด€๋ฆฌ ์„น์…˜ +// ============================================================ +interface SlotChildrenSectionProps { + config: RepeatContainerConfig; + onChange: (config: Partial) => void; + availableColumns: Array<{ columnName: string; displayName?: string }>; + loadingColumns: boolean; +} + +function SlotChildrenSection({ + config, + onChange, + availableColumns, + loadingColumns, +}: SlotChildrenSectionProps) { + const [selectedColumn, setSelectedColumn] = useState(""); + const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); + + const children = config.children || []; + + // ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ + const addComponent = (columnName: string, displayName: string) => { + const newChild: SlotComponentConfig = { + id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: "text-display", + label: displayName, + fieldName: columnName, + position: { x: 0, y: children.length * 40 }, + size: { width: 200, height: 32 }, + componentConfig: {}, + }; + + onChange({ + children: [...children, newChild], + }); + setSelectedColumn(""); + setColumnComboboxOpen(false); + }; + + // ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ + const removeComponent = (id: string) => { + onChange({ + children: children.filter((c) => c.id !== id), + }); + }; + + // ์ปดํฌ๋„ŒํŠธ ๋ผ๋ฒจ ๋ณ€๊ฒฝ + const updateComponentLabel = (id: string, label: string) => { + onChange({ + children: children.map((c) => (c.id === id ? { ...c, label } : c)), + }); + }; + + // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ๋ณ€๊ฒฝ + const updateComponentType = (id: string, componentType: string) => { + onChange({ + children: children.map((c) => (c.id === id ? { ...c, componentType } : c)), + }); + }; + + return ( +
+
+

๋ฐ˜๋ณต ํ‘œ์‹œ ํ•„๋“œ

+

+ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์—ฌ ๊ฐ ํ–‰์— ํ‘œ์‹œํ•  ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค +

+
+
+ + {/* ์ถ”๊ฐ€๋œ ํ•„๋“œ ๋ชฉ๋ก */} + {children.length > 0 ? ( +
+ {children.map((child, index) => ( +
+
+ {index + 1} +
+
+
+
+
+ {child.label || child.fieldName} +
+
+ ํ•„๋“œ: {child.fieldName} +
+
+ +
+
+ +
+ ))} +
+ ) : ( +
+ +
ํ‘œ์‹œํ•  ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
+
+ ์•„๋ž˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก์—์„œ ์„ ํƒํ•˜์„ธ์š” +
+
+ )} + + {/* ์ปฌ๋Ÿผ ์„ ํƒ Combobox */} +
+ + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {availableColumns.map((col) => { + const isAdded = children.some((c) => c.fieldName === col.columnName); + return ( + { + if (!isAdded) { + addComponent(col.columnName, col.displayName || col.columnName); + } + }} + disabled={isAdded} + className={cn( + "text-xs cursor-pointer", + isAdded && "opacity-50 cursor-not-allowed" + )} + > + +
+
{col.displayName || col.columnName}
+
+ {col.columnName} +
+
+ {isAdded && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ +
+

+ ์•ˆ๋‚ด: ์—ฌ๊ธฐ๋Š” ํ•„๋“œ ๋‹จ์œ„ ์ปดํฌ๋„ŒํŠธ๋งŒ ์ถ”๊ฐ€ํ•˜์„ธ์š”.
+ ์ง‘๊ณ„ ์œ„์ ฏ, ๋ถ„ํ•  ํŒจ๋„ ๋“ฑ์€ ๋ฆฌํ”ผํ„ฐ ์™ธ๋ถ€์— ๋ณ„๋„๋กœ ๋ฐฐ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +

+
+
+ ); +} + diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerRenderer.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerRenderer.tsx new file mode 100644 index 00000000..9de77a81 --- /dev/null +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerRenderer.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; +import { RepeatContainerDefinition } from "./index"; + +// ์ปดํฌ๋„ŒํŠธ ์ž๋™ ๋“ฑ๋ก +if (typeof window !== "undefined") { + ComponentRegistry.registerComponent(RepeatContainerDefinition); +} + +export {}; + diff --git a/frontend/lib/registry/components/repeat-container/index.ts b/frontend/lib/registry/components/repeat-container/index.ts new file mode 100644 index 00000000..11dab964 --- /dev/null +++ b/frontend/lib/registry/components/repeat-container/index.ts @@ -0,0 +1,60 @@ +"use client"; + +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import { RepeatContainerWrapper } from "./RepeatContainerComponent"; +import { RepeatContainerConfigPanel } from "./RepeatContainerConfigPanel"; +import type { RepeatContainerConfig } from "./types"; + +/** + * RepeatContainer ์ปดํฌ๋„ŒํŠธ ์ •์˜ + * ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปจํ…์ธ ๋ฅผ ๋ฐ˜๋ณต ๋ Œ๋”๋งํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ + */ +export const RepeatContainerDefinition = createComponentDefinition({ + id: "repeat-container", + name: "๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ", + nameEng: "Repeat Container", + description: "๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜๋ณต ๋ Œ๋”๋งํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ", + category: ComponentCategory.LAYOUT, + webType: "text", + component: RepeatContainerWrapper, + defaultConfig: { + dataSourceType: "manual", + layout: "vertical", + gridColumns: 2, + gap: "16px", + showBorder: true, + showShadow: false, + borderRadius: "8px", + backgroundColor: "#ffffff", + padding: "16px", + showItemTitle: false, + itemTitleTemplate: "", + titleFontSize: "14px", + titleColor: "#374151", + titleFontWeight: "600", + emptyMessage: "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", + usePaging: false, + pageSize: 10, + clickable: false, + showSelectedState: true, + selectionMode: "single", + } as Partial, + defaultSize: { width: 600, height: 300 }, + configPanel: RepeatContainerConfigPanel, + icon: "Repeat", + tags: ["๋ฆฌํ”ผํ„ฐ", "๋ฐ˜๋ณต", "์ปจํ…Œ์ด๋„ˆ", "๋ฐ์ดํ„ฐ", "๋ ˆ์ด์•„์›ƒ", "๊ทธ๋ฆฌ๋“œ"], + version: "1.0.0", + author: "๊ฐœ๋ฐœํŒ€", +}); + +// ํƒ€์ž… ๋‚ด๋ณด๋‚ด๊ธฐ +export type { + RepeatContainerConfig, + SlotComponentConfig, + RepeatItemContext, + RepeatContainerValue, + DataSourceType, + LayoutType, +} from "./types"; + diff --git a/frontend/lib/registry/components/repeat-container/types.ts b/frontend/lib/registry/components/repeat-container/types.ts new file mode 100644 index 00000000..642f5647 --- /dev/null +++ b/frontend/lib/registry/components/repeat-container/types.ts @@ -0,0 +1,187 @@ +import { ComponentConfig } from "@/types/component"; + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํƒ€์ž… + */ +export type DataSourceType = "table-list" | "unified-repeater" | "externalData" | "manual"; + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋ ˆ์ด์•„์›ƒ ํƒ€์ž… + */ +export type LayoutType = "vertical" | "horizontal" | "grid"; + +/** + * ์Šฌ๋กฏ์— ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ ์„ค์ • + * ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ๋ฐฐ์น˜ํ•œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด + */ +export interface SlotComponentConfig { + id: string; + /** ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… (์˜ˆ: "text-input", "text-display") */ + componentType: string; + /** ์ปดํฌ๋„ŒํŠธ ๋ผ๋ฒจ */ + label?: string; + /** ๋ฐ”์ธ๋”ฉํ•  ๋ฐ์ดํ„ฐ ํ•„๋“œ๋ช… */ + fieldName?: string; + /** ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ (์Šฌ๋กฏ ๋‚ด๋ถ€ ์ƒ๋Œ€ ์ขŒํ‘œ) */ + position?: { x: number; y: number }; + /** ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ */ + size?: { width: number; height: number }; + /** ์ปดํฌ๋„ŒํŠธ ์ƒ์„ธ ์„ค์ • */ + componentConfig?: Record; + /** ์Šคํƒ€์ผ ์„ค์ • */ + style?: Record; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์„ค์ • + * ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” ์ปจํ…์ธ ๋ฅผ ๋ฐ˜๋ณต ๋ Œ๋”๋งํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ + */ +export interface RepeatContainerConfig extends ComponentConfig { + // ======================== + // 1. ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + // ======================== + /** ๋ฐ์ดํ„ฐ ์†Œ์Šค ํƒ€์ž… */ + dataSourceType: DataSourceType; + /** ์—ฐ๊ฒฐํ•  ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ๋˜๋Š” ๋ฆฌํ”ผํ„ฐ ์ปดํฌ๋„ŒํŠธ ID */ + dataSourceComponentId?: string; + + // ์ปดํฌ๋„ŒํŠธ๋ณ„ ํ…Œ์ด๋ธ” ์„ค์ • (๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ ์ค€์ˆ˜) + /** ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ”๋ช… */ + tableName?: string; + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”๋ช… */ + customTableName?: string; + /** true: customTableName ์‚ฌ์šฉ */ + useCustomTable?: boolean; + /** true: ์กฐํšŒ๋งŒ, ์ €์žฅ ์•ˆ ํ•จ */ + isReadOnly?: boolean; + + // ======================== + // 2. ๋ ˆ์ด์•„์›ƒ ์„ค์ • + // ======================== + /** ๋ฐฐ์น˜ ๋ฐฉํ–ฅ */ + layout: LayoutType; + /** grid์ผ ๋•Œ ์ปฌ๋Ÿผ ์ˆ˜ */ + gridColumns?: number; + /** ์•„์ดํ…œ ๊ฐ„ ๊ฐ„๊ฒฉ */ + gap?: string; + /** ์•„์ดํ…œ ์ตœ์†Œ ๋„ˆ๋น„ */ + itemMinWidth?: string; + /** ์•„์ดํ…œ ์ตœ๋Œ€ ๋„ˆ๋น„ */ + itemMaxWidth?: string; + /** ์•„์ดํ…œ ๋†’์ด */ + itemHeight?: string; + + // ======================== + // 3. ์•„์ดํ…œ ์นด๋“œ ์„ค์ • + // ======================== + /** ์นด๋“œ ํ…Œ๋‘๋ฆฌ ํ‘œ์‹œ */ + showBorder?: boolean; + /** ์นด๋“œ ๊ทธ๋ฆผ์ž ํ‘œ์‹œ */ + showShadow?: boolean; + /** ์นด๋“œ ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ */ + borderRadius?: string; + /** ์นด๋“œ ๋ฐฐ๊ฒฝ์ƒ‰ */ + backgroundColor?: string; + /** ์นด๋“œ ๋‚ด๋ถ€ ํŒจ๋”ฉ */ + padding?: string; + + // ======================== + // 4. ์ œ๋ชฉ ์„ค์ • (๊ฐ ์•„์ดํ…œ) + // ======================== + /** ์•„์ดํ…œ ์ œ๋ชฉ ํ‘œ์‹œ */ + showItemTitle?: boolean; + /** ์•„์ดํ…œ ์ œ๋ชฉ ํ…œํ”Œ๋ฆฟ (์˜ˆ: "{order_no} - {item_code}") */ + itemTitleTemplate?: string; + /** ์ œ๋ชฉ ํฐํŠธ ํฌ๊ธฐ */ + titleFontSize?: string; + /** ์ œ๋ชฉ ์ƒ‰์ƒ */ + titleColor?: string; + /** ์ œ๋ชฉ ํฐํŠธ ๊ตต๊ธฐ */ + titleFontWeight?: string; + + // ======================== + // 5. ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง (์„ ํƒ์‚ฌํ•ญ) + // ======================== + /** ํ•„ํ„ฐ ํ•„๋“œ (formData์—์„œ ๊ฐ€์ ธ์˜ฌ ํ‚ค) */ + filterField?: string; + /** ํ•„ํ„ฐ ์ปฌ๋Ÿผ (ํ…Œ์ด๋ธ”์—์„œ ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ) */ + filterColumn?: string; + + // ======================== + // 6. ๊ทธ๋ฃนํ•‘ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) + // ======================== + /** ๊ทธ๋ฃนํ•‘ ์‚ฌ์šฉ ์—ฌ๋ถ€ */ + useGrouping?: boolean; + /** ๊ทธ๋ฃนํ•‘ ๊ธฐ์ค€ ํ•„๋“œ */ + groupByField?: string; + + // ======================== + // 7. ์Šฌ๋กฏ ์ปจํ…์ธ  ์„ค์ • (children ์ง์ ‘ ๋ฐฐ์น˜) + // ======================== + /** + * ์Šฌ๋กฏ์— ๋ฐฐ์น˜๋œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค + * ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ๋“ค + * ๊ฐ ๋ฐ์ดํ„ฐ ์•„์ดํ…œ๋งˆ๋‹ค ์ด ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๋ฐ˜๋ณต ๋ Œ๋”๋ง๋จ + */ + children?: SlotComponentConfig[]; + + // ======================== + // 8. ๋นˆ ์ƒํƒœ ์„ค์ • + // ======================== + /** ๋ฐ์ดํ„ฐ ์—†์„ ๋•Œ ํ‘œ์‹œ ๋ฉ”์‹œ์ง€ */ + emptyMessage?: string; + /** ๋นˆ ์ƒํƒœ ์•„์ด์ฝ˜ */ + emptyIcon?: string; + + // ======================== + // 9. ํŽ˜์ด์ง• ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) + // ======================== + /** ํŽ˜์ด์ง• ์‚ฌ์šฉ ์—ฌ๋ถ€ */ + usePaging?: boolean; + /** ํŽ˜์ด์ง€๋‹น ์•„์ดํ…œ ์ˆ˜ */ + pageSize?: number; + + // ======================== + // 10. ์ด๋ฒคํŠธ ์„ค์ • + // ======================== + /** ์•„์ดํ…œ ํด๋ฆญ ์ด๋ฒคํŠธ ํ™œ์„ฑํ™” */ + clickable?: boolean; + /** ํด๋ฆญ ์‹œ ์„ ํƒ ์ƒํƒœ ํ‘œ์‹œ */ + showSelectedState?: boolean; + /** ์„ ํƒ ๋ชจ๋“œ (๋‹จ์ผ/๋‹ค์ค‘) */ + selectionMode?: "single" | "multiple"; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ๊ฐ’ ํƒ€์ž… + * ํ˜„์žฌ ๋ Œ๋”๋ง ์ค‘์ธ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด + */ +export interface RepeatContainerValue { + /** ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด */ + data: Record[]; + /** ์„ ํƒ๋œ ์•„์ดํ…œ ์ธ๋ฑ์Šค๋“ค */ + selectedIndices?: number[]; + /** ํ˜„์žฌ ํŽ˜์ด์ง€ (ํŽ˜์ด์ง• ์‚ฌ์šฉ ์‹œ) */ + currentPage?: number; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ปจํ…์ŠคํŠธ (๊ฐ ๋ฐ˜๋ณต ์•„์ดํ…œ์—์„œ ์‚ฌ์šฉ) + */ +export interface RepeatItemContext { + /** ํ˜„์žฌ ์•„์ดํ…œ ์ธ๋ฑ์Šค */ + index: number; + /** ํ˜„์žฌ ์•„์ดํ…œ ๋ฐ์ดํ„ฐ */ + data: Record; + /** ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜ */ + totalCount: number; + /** ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ์ธ์ง€ */ + isFirst: boolean; + /** ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ์ธ์ง€ */ + isLast: boolean; + /** ๊ทธ๋ฃน ํ‚ค (๊ทธ๋ฃนํ•‘ ์‚ฌ์šฉ ์‹œ) */ + groupKey?: string; + /** ๊ทธ๋ฃน ๋‚ด ์ธ๋ฑ์Šค (๊ทธ๋ฃนํ•‘ ์‚ฌ์šฉ ์‹œ) */ + groupIndex?: number; +} + diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 9be0f4ee..910babc5 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2074,6 +2074,19 @@ export const TableListComponent: React.FC = ({ }); } + // ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ/์ง‘๊ณ„ ์œ„์ ฏ ์—ฐ๋™์šฉ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + if (typeof window !== "undefined") { + const event = new CustomEvent("tableListDataChange", { + detail: { + componentId: component.id, + tableName: tableConfig.selectedTable, + data: selectedRowsData, + selectedRows: Array.from(newSelectedRows), + }, + }); + window.dispatchEvent(event); + } + // ๐Ÿ†• modalDataStore์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ž๋™ ์ €์žฅ (ํ…Œ์ด๋ธ”๋ช… ๊ธฐ๋ฐ˜ dataSourceId) if (tableConfig.selectedTable && selectedRowsData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { @@ -2112,6 +2125,19 @@ export const TableListComponent: React.FC = ({ }); } + // ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ/์ง‘๊ณ„ ์œ„์ ฏ ์—ฐ๋™์šฉ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + if (typeof window !== "undefined") { + const event = new CustomEvent("tableListDataChange", { + detail: { + componentId: component.id, + tableName: tableConfig.selectedTable, + data: filteredData, + selectedRows: Array.from(newSelectedRows), + }, + }); + window.dispatchEvent(event); + } + // ๐Ÿ†• modalDataStore์— ์ „์ฒด ๋ฐ์ดํ„ฐ ์ €์žฅ if (tableConfig.selectedTable && filteredData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { @@ -2135,6 +2161,19 @@ export const TableListComponent: React.FC = ({ onFormDataChange({ selectedRows: [], selectedRowsData: [] }); } + // ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ/์ง‘๊ณ„ ์œ„์ ฏ ์—ฐ๋™์šฉ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (์„ ํƒ ํ•ด์ œ) + if (typeof window !== "undefined") { + const event = new CustomEvent("tableListDataChange", { + detail: { + componentId: component.id, + tableName: tableConfig.selectedTable, + data: [], + selectedRows: [], + }, + }); + window.dispatchEvent(event); + } + // ๐Ÿ†• modalDataStore ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ if (tableConfig.selectedTable) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => {