From d6f40f3cd342d63237d1128c60186e13ef42346a Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 16 Dec 2025 18:02:08 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=ED=8A=BC=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=95=84=ED=84=B0=EB=A7=81?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/InteractiveDataTable.tsx | 45 ++- frontend/components/screen/SaveModal.tsx | 47 +++ .../config-panels/ButtonConfigPanel.tsx | 1 + .../screen/panels/UnifiedPropertiesPanel.tsx | 12 + .../lib/registry/DynamicComponentRenderer.tsx | 5 + .../EntitySearchInputComponent.tsx | 161 +++++---- .../RelatedDataButtonsComponent.tsx | 160 ++++++++- .../RelatedDataButtonsConfigPanel.tsx | 336 +++++++++++++++++- .../components/related-data-buttons/types.ts | 19 + .../table-list/TableListComponent.tsx | 52 ++- .../textarea-basic/TextareaBasicComponent.tsx | 24 +- frontend/lib/utils/buttonActions.ts | 160 +++++++++ 12 files changed, 909 insertions(+), 113 deletions(-) diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index e44a356c..a1015ac6 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -311,6 +311,41 @@ export const InteractiveDataTable: React.FC = ({ }; }, [currentPage, searchValues, loadData, component.tableName]); + // ๐Ÿ†• RelatedDataButtons ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (๋ฒ„ํŠผ ์„ ํƒ ์‹œ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง) + const [relatedButtonFilter, setRelatedButtonFilter] = useState<{ + filterColumn: string; + filterValue: any; + } | null>(null); + + useEffect(() => { + const handleRelatedButtonSelect = (event: CustomEvent) => { + const { targetTable, filterColumn, filterValue } = event.detail || {}; + + // ์ด ํ…Œ์ด๋ธ”์ด ๋Œ€์ƒ ํ…Œ์ด๋ธ”์ธ์ง€ ํ™•์ธ + if (targetTable === component.tableName) { + console.log("๐Ÿ“Œ [InteractiveDataTable] RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ:", { + tableName: component.tableName, + filterColumn, + filterValue, + }); + setRelatedButtonFilter({ filterColumn, filterValue }); + } + }; + + window.addEventListener("related-button-select" as any, handleRelatedButtonSelect); + + return () => { + window.removeEventListener("related-button-select" as any, handleRelatedButtonSelect); + }; + }, [component.tableName]); + + // relatedButtonFilter ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ + useEffect(() => { + if (relatedButtonFilter) { + loadData(1, searchValues); + } + }, [relatedButtonFilter]); + // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ๊ฐ’ ๋งคํ•‘ ๋กœ๋“œ useEffect(() => { const loadCategoryMappings = async () => { @@ -705,10 +740,17 @@ export const InteractiveDataTable: React.FC = ({ return; } + // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ + let relatedButtonFilterValues: Record = {}; + if (relatedButtonFilter) { + relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue; + } + // ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ‘ํ•ฉ const mergedSearchParams = { ...searchParams, ...linkedFilterValues, + ...relatedButtonFilterValues, // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ }; console.log("๐Ÿ” ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘:", { @@ -716,6 +758,7 @@ export const InteractiveDataTable: React.FC = ({ page, pageSize, linkedFilterValues, + relatedButtonFilterValues, mergedSearchParams, }); @@ -822,7 +865,7 @@ export const InteractiveDataTable: React.FC = ({ setLoading(false); } }, - [component.tableName, pageSize, component.autoFilter, splitPanelContext?.selectedLeftData], // ๐Ÿ†• autoFilter, ์—ฐ๊ฒฐํ•„ํ„ฐ ์ถ”๊ฐ€ + [component.tableName, pageSize, component.autoFilter, splitPanelContext?.selectedLeftData, relatedButtonFilter], // ๐Ÿ†• autoFilter, ์—ฐ๊ฒฐํ•„ํ„ฐ, RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ ); // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋“œ diff --git a/frontend/components/screen/SaveModal.tsx b/frontend/components/screen/SaveModal.tsx index 4e158719..88ca2534 100644 --- a/frontend/components/screen/SaveModal.tsx +++ b/frontend/components/screen/SaveModal.tsx @@ -101,6 +101,46 @@ export const SaveModal: React.FC = ({ }; }, [onClose]); + // ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ + const validateRequiredFields = (): { isValid: boolean; missingFields: string[] } => { + const missingFields: string[] = []; + + components.forEach((component) => { + // ์ปดํฌ๋„ŒํŠธ์˜ required ์†์„ฑ ํ™•์ธ (์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ์ฒดํฌ) + const isRequired = + component.required === true || + component.style?.required === true || + component.componentConfig?.required === true; + + const columnName = component.columnName || component.style?.columnName; + const label = component.label || component.style?.label || columnName; + + console.log("๐Ÿ” ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ:", { + componentId: component.id, + columnName, + label, + isRequired, + "component.required": component.required, + "style.required": component.style?.required, + "componentConfig.required": component.componentConfig?.required, + value: formData[columnName || ""], + }); + + if (isRequired && columnName) { + const value = formData[columnName]; + // ๊ฐ’์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ (null, undefined, ๋นˆ ๋ฌธ์ž์—ด, ๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ๋ฌธ์ž์—ด) + if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) { + missingFields.push(label || columnName); + } + } + }); + + return { + isValid: missingFields.length === 0, + missingFields, + }; + }; + // ์ €์žฅ ํ•ธ๋“ค๋Ÿฌ const handleSave = async () => { if (!screenData || !screenId) return; @@ -111,6 +151,13 @@ export const SaveModal: React.FC = ({ return; } + // โœ… ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ + const validation = validateRequiredFields(); + if (!validation.isValid) { + toast.error(`ํ•„์ˆ˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”: ${validation.missingFields.join(", ")}`); + return; + } + try { setIsSaving(true); diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 5c61fb95..3a126c29 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -645,6 +645,7 @@ export const ButtonConfigPanel: React.FC = ({ ํŽ˜์ด์ง€ ์ด๋™ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์—ด๊ธฐ + ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ๋ชจ๋‹ฌ ์—ด๊ธฐ ๋ชจ๋‹ฌ ์—ด๊ธฐ ์ฆ‰์‹œ ์ €์žฅ ์ œ์–ด ํ๋ฆ„ diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index ecf2671c..ad34df9a 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -943,6 +943,18 @@ export const UnifiedPropertiesPanel: React.FC = ({ )} + {/* ์ˆจ๊น€ ์˜ต์…˜ */} +
+ { + handleUpdate("hidden", checked); + handleUpdate("componentConfig.hidden", checked); + }} + className="h-4 w-4" + /> + +
); diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index b3e77cd7..6ca5e68f 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -319,6 +319,11 @@ export const DynamicComponentRenderer: React.FC = // ์ˆจ๊น€ ๊ฐ’ ์ถ”์ถœ const hiddenValue = component.hidden || component.componentConfig?.hidden; + // ์ˆจ๊น€ ์ฒ˜๋ฆฌ: ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ชจ๋“œ(์‹ค์ œ ๋ทฐ)์—์„œ๋งŒ ์ˆจ๊น€, ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋Š” ํ‘œ์‹œ + if (hiddenValue && isInteractive) { + return null; + } + // size.width์™€ size.height๋ฅผ style.width์™€ style.height๋กœ ๋ณ€ํ™˜ const finalStyle: React.CSSProperties = { ...component.style, diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 6ee22a0c..49d96122 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -7,19 +7,8 @@ import { Search, X, Check, ChevronsUpDown } from "lucide-react"; import { EntitySearchModal } from "./EntitySearchModal"; import { EntitySearchInputProps, EntitySearchResult } from "./types"; import { cn } from "@/lib/utils"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { dynamicFormApi } from "@/lib/api/dynamicForm"; export function EntitySearchInputComponent({ @@ -44,7 +33,7 @@ export function EntitySearchInputComponent({ component, isInteractive, onFormDataChange, -}: EntitySearchInputProps & { +}: EntitySearchInputProps & { uiMode?: string; component?: any; isInteractive?: boolean; @@ -52,7 +41,7 @@ export function EntitySearchInputComponent({ }) { // uiMode๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด modeProp ์‚ฌ์šฉ, ๊ธฐ๋ณธ๊ฐ’ "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; - + const [modalOpen, setModalOpen] = useState(false); const [selectOpen, setSelectOpen] = useState(false); const [displayValue, setDisplayValue] = useState(""); @@ -74,7 +63,7 @@ export function EntitySearchInputComponent({ const loadOptions = async () => { if (!tableName) return; - + setIsLoadingOptions(true); try { const response = await dynamicFormApi.getTableData(tableName, { @@ -82,7 +71,7 @@ export function EntitySearchInputComponent({ pageSize: 100, // ์ตœ๋Œ€ 100๊ฐœ๊นŒ์ง€ ๋กœ๋“œ filters: filterCondition, }); - + if (response.success && response.data) { setOptions(response.data); } @@ -93,28 +82,73 @@ export function EntitySearchInputComponent({ } }; - // value๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํ‘œ์‹œ๊ฐ’ ์—…๋ฐ์ดํŠธ + // value๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํ‘œ์‹œ๊ฐ’ ์—…๋ฐ์ดํŠธ (์™ธ๋ž˜ํ‚ค ๊ฐ’์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ) useEffect(() => { - if (value && selectedData) { - setDisplayValue(selectedData[displayField] || ""); - } else if (value && mode === "select" && options.length > 0) { - // select ๋ชจ๋“œ์—์„œ value๊ฐ€ ์žˆ๊ณ  options๊ฐ€ ๋กœ๋“œ๋œ ๊ฒฝ์šฐ - const found = options.find(opt => opt[valueField] === value); - if (found) { - setSelectedData(found); - setDisplayValue(found[displayField] || ""); + const loadDisplayValue = async () => { + if (value && selectedData) { + // ์ด๋ฏธ selectedData๊ฐ€ ์žˆ์œผ๋ฉด ํ‘œ์‹œ๊ฐ’๋งŒ ์—…๋ฐ์ดํŠธ + setDisplayValue(selectedData[displayField] || ""); + } else if (value && mode === "select" && options.length > 0) { + // select ๋ชจ๋“œ์—์„œ value๊ฐ€ ์žˆ๊ณ  options๊ฐ€ ๋กœ๋“œ๋œ ๊ฒฝ์šฐ + const found = options.find((opt) => opt[valueField] === value); + if (found) { + setSelectedData(found); + setDisplayValue(found[displayField] || ""); + } + } else if (value && !selectedData && tableName) { + // value๋Š” ์žˆ์ง€๋งŒ selectedData๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ) + // API๋กœ ํ•ด๋‹น ๋ฐ์ดํ„ฐ ์กฐํšŒ + try { + console.log("๐Ÿ” [EntitySearchInput] ์ดˆ๊ธฐ๊ฐ’ ์กฐํšŒ:", { value, tableName, valueField }); + const response = await dynamicFormApi.getTableData(tableName, { + filters: { [valueField]: value }, + pageSize: 1, + }); + + if (response.success && response.data) { + // ๋ฐ์ดํ„ฐ ์ถ”์ถœ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ) + const responseData = response.data as any; + const dataArray = Array.isArray(responseData) + ? responseData + : responseData?.data + ? Array.isArray(responseData.data) + ? responseData.data + : [responseData.data] + : []; + + if (dataArray.length > 0) { + const foundData = dataArray[0]; + setSelectedData(foundData); + setDisplayValue(foundData[displayField] || ""); + console.log("โœ… [EntitySearchInput] ์ดˆ๊ธฐ๊ฐ’ ๋กœ๋“œ ์™„๋ฃŒ:", foundData); + } else { + // ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ value ์ž์ฒด๋ฅผ ํ‘œ์‹œ + console.log("โš ๏ธ [EntitySearchInput] ์ดˆ๊ธฐ๊ฐ’ ๋ฐ์ดํ„ฐ ์—†์Œ, value ํ‘œ์‹œ:", value); + setDisplayValue(String(value)); + } + } else { + console.log("โš ๏ธ [EntitySearchInput] API ์‘๋‹ต ์‹คํŒจ, value ํ‘œ์‹œ:", value); + setDisplayValue(String(value)); + } + } catch (error) { + console.error("โŒ [EntitySearchInput] ์ดˆ๊ธฐ๊ฐ’ ์กฐํšŒ ์‹คํŒจ:", error); + // ์—๋Ÿฌ ์‹œ value ์ž์ฒด๋ฅผ ํ‘œ์‹œ + setDisplayValue(String(value)); + } + } else if (!value) { + setDisplayValue(""); + setSelectedData(null); } - } else if (!value) { - setDisplayValue(""); - setSelectedData(null); - } - }, [value, displayField, options, mode, valueField]); + }; + + loadDisplayValue(); + }, [value, displayField, options, mode, valueField, tableName, selectedData]); const handleSelect = (newValue: any, fullData: EntitySearchResult) => { setSelectedData(fullData); setDisplayValue(fullData[displayField] || ""); onChange?.(newValue, fullData); - + // ๐Ÿ†• onFormDataChange ํ˜ธ์ถœ (formData์— ๊ฐ’ ์ €์žฅ) if (isInteractive && onFormDataChange && component?.columnName) { onFormDataChange(component.columnName, newValue); @@ -126,7 +160,7 @@ export function EntitySearchInputComponent({ setDisplayValue(""); setSelectedData(null); onChange?.(null, null); - + // ๐Ÿ†• onFormDataChange ํ˜ธ์ถœ (formData์—์„œ ๊ฐ’ ์ œ๊ฑฐ) if (isInteractive && onFormDataChange && component?.columnName) { onFormDataChange(component.columnName, null); @@ -147,14 +181,19 @@ export function EntitySearchInputComponent({ // ๋†’์ด ๊ณ„์‚ฐ (style์—์„œ height๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’) const componentHeight = style?.height; - const inputStyle: React.CSSProperties = componentHeight - ? { height: componentHeight } - : {}; + const inputStyle: React.CSSProperties = componentHeight ? { height: componentHeight } : {}; // select ๋ชจ๋“œ: ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด if (mode === "select") { return ( -
+
+ {/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} + {component?.label && component?.style?.labelDisplay !== false && ( + + )} - + - + - - ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - + ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. {options.map((option, index) => (
{option[displayField]} {valueField !== displayField && ( - - {option[valueField]} - + {option[valueField]} )}
@@ -221,7 +244,7 @@ export function EntitySearchInputComponent({ {/* ์ถ”๊ฐ€ ์ •๋ณด ํ‘œ์‹œ */} {showAdditionalInfo && selectedData && additionalFields.length > 0 && ( -
+
{additionalFields.map((field) => (
{field}: @@ -236,9 +259,16 @@ export function EntitySearchInputComponent({ // modal, combo, autocomplete ๋ชจ๋“œ return ( -
+
+ {/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} + {component?.label && component?.style?.labelDisplay !== false && ( + + )} {/* ์ž…๋ ฅ ํ•„๋“œ */} -
+
@@ -278,7 +308,7 @@ export function EntitySearchInputComponent({ {/* ์ถ”๊ฐ€ ์ •๋ณด ํ‘œ์‹œ */} {showAdditionalInfo && selectedData && additionalFields.length > 0 && ( -
+
{additionalFields.map((field) => (
{field}: @@ -306,4 +336,3 @@ export function EntitySearchInputComponent({
); } - diff --git a/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx index 768edfe9..cd535366 100644 --- a/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx +++ b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx @@ -1,13 +1,24 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { Plus, Star, Loader2 } from "lucide-react"; +import { Plus, Star, Loader2, ExternalLink } from "lucide-react"; import { cn } from "@/lib/utils"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { dataApi } from "@/lib/api/data"; import type { RelatedDataButtonsConfig, ButtonItem } from "./types"; +// ์ „์—ญ ์ƒํƒœ: ํ˜„์žฌ ์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ฒŒ +declare global { + interface Window { + __relatedButtonsSelectedData?: { + selectedItem: ButtonItem | null; + masterData: Record | null; + config: RelatedDataButtonsConfig | null; + }; + } +} + interface RelatedDataButtonsComponentProps { config: RelatedDataButtonsConfig; className?: string; @@ -21,12 +32,27 @@ export const RelatedDataButtonsComponent: React.FC { const [buttons, setButtons] = useState([]); const [selectedId, setSelectedId] = useState(null); + const [selectedItem, setSelectedItem] = useState(null); const [loading, setLoading] = useState(false); const [masterData, setMasterData] = useState | null>(null); // SplitPanel Context ์—ฐ๊ฒฐ const splitPanelContext = useSplitPanelContext(); + // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์—ญ ์ƒํƒœ์— ์ €์žฅ (์™ธ๋ถ€ ๋ฒ„ํŠผ์—์„œ ์ ‘๊ทผ์šฉ) + useEffect(() => { + window.__relatedButtonsSelectedData = { + selectedItem, + masterData, + config, + }; + console.log("๐Ÿ”„ [RelatedDataButtons] ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ:", { + selectedItem, + hasConfig: !!config, + modalLink: config?.modalLink, + }); + }, [selectedItem, masterData, config]); + // ์ขŒ์ธก ํŒจ๋„์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๊ฐ์ง€ useEffect(() => { if (!splitPanelContext?.selectedLeftData) { @@ -89,6 +115,7 @@ export const RelatedDataButtonsComponent: React.FC item.isDefault); const targetItem = defaultItem || items[0]; setSelectedId(targetItem.id); + setSelectedItem(targetItem); emitSelection(targetItem); } } @@ -104,6 +131,7 @@ export const RelatedDataButtonsComponent: React.FC { if (masterData) { setSelectedId(null); // ๋งˆ์Šคํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์„ ํƒ ์ดˆ๊ธฐํ™” + setSelectedItem(null); loadButtons(); } }, [masterData, loadButtons]); @@ -134,9 +162,82 @@ export const RelatedDataButtonsComponent: React.FC { setSelectedId(item.id); + setSelectedItem(item); emitSelection(item); }, [emitSelection]); + // ๋ชจ๋‹ฌ ์—ด๊ธฐ (์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ) + const openModalWithSelectedData = useCallback((targetScreenId: number) => { + if (!selectedItem) { + console.warn("์„ ํƒ๋œ ๋ฒ„ํŠผ์ด ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ ์šฉ + const initialData: Record = {}; + + if (config.modalLink?.dataMapping) { + config.modalLink.dataMapping.forEach(mapping => { + if (mapping.sourceField === "value") { + initialData[mapping.targetField] = selectedItem.value; + } else if (mapping.sourceField === "id") { + initialData[mapping.targetField] = selectedItem.id; + } else if (selectedItem.rawData[mapping.sourceField] !== undefined) { + initialData[mapping.targetField] = selectedItem.rawData[mapping.sourceField]; + } + }); + } else { + // ๊ธฐ๋ณธ ๋งคํ•‘: id๋ฅผ routing_version_id๋กœ ์ „๋‹ฌ + initialData["routing_version_id"] = selectedItem.value || selectedItem.id; + } + + console.log("๐Ÿ“ค RelatedDataButtons ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { + targetScreenId, + selectedItem, + initialData, + }); + + window.dispatchEvent(new CustomEvent("open-screen-modal", { + detail: { + screenId: targetScreenId, + initialData, + onSuccess: () => { + loadButtons(); // ๋ชจ๋‹ฌ ์„ฑ๊ณต ํ›„ ์ƒˆ๋กœ๊ณ ์นจ + }, + }, + })); + }, [selectedItem, config.modalLink, loadButtons]); + + // ์™ธ๋ถ€ ๋ฒ„ํŠผ์—์„œ ๋ชจ๋‹ฌ ์—ด๊ธฐ ์š”์ฒญ ์ˆ˜์‹  + useEffect(() => { + const handleExternalModalOpen = (event: CustomEvent) => { + const { targetScreenId, componentId } = event.detail || {}; + + // componentId๊ฐ€ ์ง€์ •๋˜์–ด ์žˆ๊ณ  ํ˜„์žฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ฌด์‹œ + if (componentId && componentId !== config.sourceMapping?.sourceTable) { + return; + } + + if (targetScreenId && selectedItem) { + openModalWithSelectedData(targetScreenId); + } + }; + + window.addEventListener("related-buttons-open-modal" as any, handleExternalModalOpen); + return () => { + window.removeEventListener("related-buttons-open-modal" as any, handleExternalModalOpen); + }; + }, [selectedItem, config.sourceMapping, openModalWithSelectedData]); + + // ๋‚ด๋ถ€ ๋ชจ๋‹ฌ ๋งํฌ ๋ฒ„ํŠผ ํด๋ฆญ + const handleModalLinkClick = useCallback(() => { + if (!config.modalLink?.targetScreenId) { + console.warn("๋ชจ๋‹ฌ ๋งํฌ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + openModalWithSelectedData(config.modalLink.targetScreenId); + }, [config.modalLink, openModalWithSelectedData]); + // ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ const handleAddClick = useCallback(() => { if (!config.addButton?.modalScreenId) return; @@ -177,6 +278,7 @@ export const RelatedDataButtonsComponent: React.FC @@ -198,18 +300,34 @@ export const RelatedDataButtonsComponent: React.FC - {/* ํ—ค๋” ์œ„์น˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} - {addButtonConfig?.show && addButtonConfig?.position === "header" && ( - - )} +
+ {/* ๋ชจ๋‹ฌ ๋งํฌ ๋ฒ„ํŠผ (ํ—ค๋” ์œ„์น˜) */} + {modalLinkConfig?.enabled && modalLinkConfig?.triggerType === "button" && modalLinkConfig?.buttonPosition === "header" && ( + + )} + + {/* ํ—ค๋” ์œ„์น˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {addButtonConfig?.show && addButtonConfig?.position === "header" && ( + + )} +
)} @@ -258,6 +376,20 @@ export const RelatedDataButtonsComponent: React.FC ))} + {/* ๋ชจ๋‹ฌ ๋งํฌ ๋ฒ„ํŠผ (์ธ๋ผ์ธ ์œ„์น˜) */} + {modalLinkConfig?.enabled && modalLinkConfig?.triggerType === "button" && modalLinkConfig?.buttonPosition !== "header" && ( + + )} + {/* ์ธ๋ผ์ธ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} {addButtonConfig?.show && addButtonConfig?.position !== "header" && ( + + + + + + ํ™”๋ฉด์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {screens.map((screen) => ( + { + onChange(screen.screenId, screen.tableName); + setOpen(false); + }} + className="text-xs" + > + + {screen.screenName} + ({screen.screenId}) + + ))} + + + + + + ); +}; + interface TableInfo { tableName: string; displayName?: string; @@ -37,6 +117,9 @@ export const RelatedDataButtonsConfigPanel: React.FC([]); const [sourceTableColumns, setSourceTableColumns] = useState([]); const [buttonTableColumns, setButtonTableColumns] = useState([]); + const [targetModalTableColumns, setTargetModalTableColumns] = useState([]); // ๋Œ€์ƒ ๋ชจ๋‹ฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + const [targetModalTableName, setTargetModalTableName] = useState(""); // ๋Œ€์ƒ ๋ชจ๋‹ฌ ํ…Œ์ด๋ธ”๋ช… + const [eventTargetTableColumns, setEventTargetTableColumns] = useState([]); // ํ•˜์œ„ ํ…Œ์ด๋ธ” ์—ฐ๋™ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ // Popover ์ƒํƒœ const [sourceTableOpen, setSourceTableOpen] = useState(false); @@ -104,6 +187,69 @@ export const RelatedDataButtonsConfigPanel: React.FC { + const loadTargetScreenTable = async () => { + if (!config.modalLink?.targetScreenId) { + setTargetModalTableName(""); + return; + } + try { + const screenInfo = await screenApi.getScreen(config.modalLink.targetScreenId); + if (screenInfo?.tableName) { + setTargetModalTableName(screenInfo.tableName); + } + } catch (error) { + console.error("๋Œ€์ƒ ๋ชจ๋‹ฌ ํ™”๋ฉด ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + loadTargetScreenTable(); + }, [config.modalLink?.targetScreenId]); + + // ๋Œ€์ƒ ๋ชจ๋‹ฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const loadColumns = async () => { + if (!targetModalTableName) { + setTargetModalTableColumns([]); + return; + } + try { + const response = await getTableColumns(targetModalTableName); + if (response.success && response.data?.columns) { + setTargetModalTableColumns(response.data.columns.map((c: any) => ({ + columnName: c.columnName || c.column_name, + columnLabel: c.columnLabel || c.column_label || c.displayName, + }))); + } + } catch (error) { + console.error("๋Œ€์ƒ ๋ชจ๋‹ฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + loadColumns(); + }, [targetModalTableName]); + + // ํ•˜์œ„ ํ…Œ์ด๋ธ” ์—ฐ๋™ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const loadColumns = async () => { + if (!config.events?.targetTable) { + setEventTargetTableColumns([]); + return; + } + try { + const response = await getTableColumns(config.events.targetTable); + if (response.success && response.data?.columns) { + setEventTargetTableColumns(response.data.columns.map((c: any) => ({ + columnName: c.columnName || c.column_name, + columnLabel: c.columnLabel || c.column_label || c.displayName, + }))); + } + } catch (error) { + console.error("ํ•˜์œ„ ํ…Œ์ด๋ธ” ์—ฐ๋™ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + loadColumns(); + }, [config.events?.targetTable]); + // ์„ค์ • ์—…๋ฐ์ดํŠธ ํ—ฌํผ const updateConfig = useCallback((updates: Partial) => { onChange({ ...config, ...updates }); @@ -151,6 +297,13 @@ export const RelatedDataButtonsConfigPanel: React.FC>) => { + onChange({ + ...config, + modalLink: { ...config.modalLink, ...updates }, + }); + }, [config, onChange]); + const tables = allTables.length > 0 ? allTables : propTables; return ( @@ -471,11 +624,27 @@ export const RelatedDataButtonsConfigPanel: React.FC - updateEvents({ targetFilterColumn: e.target.value })} - placeholder="์˜ˆ: routing_version_id" - /> + onValueChange={(value) => updateEvents({ targetFilterColumn: value })} + > + + + + + {eventTargetTableColumns.map((col) => ( + + {col.columnLabel || col.columnName} + + ))} + + + {eventTargetTableColumns.length === 0 && config.events?.targetTable && ( +

์ปฌ๋Ÿผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+ )} + {!config.events?.targetTable && ( +

๋จผ์ € ๋Œ€์ƒ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”

+ )}
@@ -517,18 +686,165 @@ export const RelatedDataButtonsConfigPanel: React.FC
- - updateAddButton({ modalScreenId: parseInt(e.target.value) || undefined })} - placeholder="ํ™”๋ฉด ID" + + updateAddButton({ modalScreenId: screenId })} + placeholder="ํ™”๋ฉด ์„ ํƒ" />
)}
+ {/* ๋ชจ๋‹ฌ ์—ฐ๋™ ์„ค์ • (์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ๋กœ ์ „๋‹ฌ) */} +
+
+ + updateModalLink({ enabled: checked })} + /> +
+ + {config.modalLink?.enabled && ( +
+
+ + +
+ + {config.modalLink?.triggerType === "button" && ( + <> +
+ + updateModalLink({ buttonLabel: e.target.value })} + placeholder="๊ณต์ • ์ถ”๊ฐ€" + /> +
+ +
+ + +
+ + )} + +
+ + { + updateModalLink({ targetScreenId: screenId }); + if (tableName) { + setTargetModalTableName(tableName); + } + }} + placeholder="ํ™”๋ฉด ์„ ํƒ" + /> + {targetModalTableName && ( +

+ ํ…Œ์ด๋ธ”: {targetModalTableName} +

+ )} +
+ +
+ +

+ ์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. +

+
+
+ + +
+ +
+ + + {targetModalTableColumns.length === 0 && targetModalTableName && ( +

์ปฌ๋Ÿผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+ )} + {!targetModalTableName && ( +

๋จผ์ € ๋Œ€์ƒ ๋ชจ๋‹ฌ ํ™”๋ฉด์„ ์„ ํƒํ•˜์„ธ์š”

+ )} +
+
+
+
+ )} +
+ {/* ๊ธฐํƒ€ ์„ค์ • */}
diff --git a/frontend/lib/registry/components/related-data-buttons/types.ts b/frontend/lib/registry/components/related-data-buttons/types.ts index 01585b6b..7f1849ce 100644 --- a/frontend/lib/registry/components/related-data-buttons/types.ts +++ b/frontend/lib/registry/components/related-data-buttons/types.ts @@ -65,6 +65,22 @@ export interface EventConfig { customEventName?: string; } +/** + * ๋ชจ๋‹ฌ ์—ฐ๋™ ์„ค์ • (์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ๋กœ ์ „๋‹ฌ) + */ +export interface ModalLinkConfig { + enabled?: boolean; // ๋ชจ๋‹ฌ ์—ฐ๋™ ํ™œ์„ฑํ™” + targetScreenId?: number; // ์—ด๋ฆด ๋ชจ๋‹ฌ ํ™”๋ฉด ID + triggerType?: "button" | "external"; // button: ๋ณ„๋„ ๋ฒ„ํŠผ, external: ์™ธ๋ถ€ ๋ฒ„ํŠผ์—์„œ ํ˜ธ์ถœ + buttonLabel?: string; // ๋ฒ„ํŠผ ํ…์ŠคํŠธ (triggerType์ด button์ผ ๋•Œ) + buttonPosition?: "header" | "inline"; // ๋ฒ„ํŠผ ์œ„์น˜ + // ๋ฐ์ดํ„ฐ ๋งคํ•‘: ์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ โ†’ ๋ชจ๋‹ฌ ์ดˆ๊ธฐ๊ฐ’ + dataMapping?: { + sourceField: string; // ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ์˜ ํ•„๋“œ๋ช… (์˜ˆ: "id", "value") + targetField: string; // ๋ชจ๋‹ฌ์— ์ „๋‹ฌํ•  ํ•„๋“œ๋ช… (์˜ˆ: "routing_version_id") + }[]; +} + /** * ๋ฉ”์ธ ์„ค์ • */ @@ -90,6 +106,9 @@ export interface RelatedDataButtonsConfig { // ์ด๋ฒคํŠธ ์„ค์ • events?: EventConfig; + // ๋ชจ๋‹ฌ ์—ฐ๋™ ์„ค์ • (์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ๋กœ ์ „๋‹ฌ) + modalLink?: ModalLinkConfig; + // ์ž๋™ ์„ ํƒ autoSelectFirst?: boolean; // ์ฒซ ๋ฒˆ์งธ (๋˜๋Š” ๊ธฐ๋ณธ) ํ•ญ๋ชฉ ์ž๋™ ์„ ํƒ diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 41a477ab..d1063049 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -304,6 +304,12 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์ƒํƒœ (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) const [linkedFilterValues, setLinkedFilterValues] = useState>({}); + // ๐Ÿ†• RelatedDataButtons ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ํ•„ํ„ฐ ์ƒํƒœ + const [relatedButtonFilter, setRelatedButtonFilter] = useState<{ + filterColumn: string; + filterValue: any; + } | null>(null); + // TableOptions Context const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions(); const [filters, setFilters] = useState([]); @@ -1548,10 +1554,21 @@ export const TableListComponent: React.FC = ({ return; } - // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ์™€ ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ‘ํ•ฉ + // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ๊ฐ’ ์ค€๋น„ + let relatedButtonFilterValues: Record = {}; + if (relatedButtonFilter) { + relatedButtonFilterValues[relatedButtonFilter.filterColumn] = { + value: relatedButtonFilter.filterValue, + operator: "equals", + }; + console.log("๐Ÿ”— [TableList] RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ:", relatedButtonFilterValues); + } + + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ, ์—ฐ๊ฒฐ ํ•„ํ„ฐ, RelatedDataButtons ํ•„ํ„ฐ ๋ณ‘ํ•ฉ const filters = { ...(Object.keys(searchValues).length > 0 ? searchValues : {}), ...linkedFilterValues, + ...relatedButtonFilterValues, // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ }; const hasFilters = Object.keys(filters).length > 0; @@ -1748,6 +1765,8 @@ export const TableListComponent: React.FC = ({ splitPanelPosition, currentSplitPosition, splitPanelContext?.selectedLeftData, + // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ + relatedButtonFilter, ]); const fetchTableDataDebounced = useCallback( @@ -4764,6 +4783,37 @@ export const TableListComponent: React.FC = ({ }; }, [tableConfig.selectedTable, isDesignMode]); + // ๐Ÿ†• RelatedDataButtons ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (๋ฒ„ํŠผ ์„ ํƒ ์‹œ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง) + useEffect(() => { + const handleRelatedButtonSelect = (event: CustomEvent) => { + const { targetTable, filterColumn, filterValue } = event.detail || {}; + + // ์ด ํ…Œ์ด๋ธ”์ด ๋Œ€์ƒ ํ…Œ์ด๋ธ”์ธ์ง€ ํ™•์ธ + if (targetTable === tableConfig.selectedTable) { + console.log("๐Ÿ“Œ [TableList] RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ:", { + tableName: tableConfig.selectedTable, + filterColumn, + filterValue, + }); + setRelatedButtonFilter({ filterColumn, filterValue }); + } + }; + + window.addEventListener("related-button-select" as any, handleRelatedButtonSelect); + + return () => { + window.removeEventListener("related-button-select" as any, handleRelatedButtonSelect); + }; + }, [tableConfig.selectedTable]); + + // ๐Ÿ†• relatedButtonFilter ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ + useEffect(() => { + if (relatedButtonFilter && !isDesignMode) { + console.log("๐Ÿ”„ [TableList] RelatedDataButtons ํ•„ํ„ฐ ๋ณ€๊ฒฝ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ:", relatedButtonFilter); + setRefreshTrigger((prev) => prev + 1); + } + }, [relatedButtonFilter, isDesignMode]); + // ๐ŸŽฏ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ๊ณ„์‚ฐ (๋‚ด์šฉ ๊ธฐ๋ฐ˜) const calculateOptimalColumnWidth = useCallback( (columnName: string, displayName: string): number => { diff --git a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx index eea2f113..a1e441a7 100644 --- a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx +++ b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx @@ -55,29 +55,11 @@ export const TextareaBasicComponent: React.FC = ({ onClick?.(); }; - // DOM์— ์ „๋‹ฌํ•˜๋ฉด ์•ˆ ๋˜๋Š” React-specific props ํ•„ํ„ฐ๋ง - const { - selectedScreen, - onZoneComponentDrop, - onZoneClick, - componentConfig: _componentConfig, - component: _component, - isSelected: _isSelected, - onClick: _onClick, - onDragStart: _onDragStart, - onDragEnd: _onDragEnd, - size: _size, - position: _position, - style: _style, - screenId: _screenId, - tableName: _tableName, - onRefresh: _onRefresh, - onClose: _onClose, - ...domProps - } = props; + // DOM์— ์ „๋‹ฌํ•˜๋ฉด ์•ˆ ๋˜๋Š” React-specific props ํ•„ํ„ฐ๋ง - ๋ชจ๋“  ์ปค์Šคํ…€ props ์ œ๊ฑฐ + // domProps๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ํ•„์š”ํ•œ props๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌ return ( -
+
{/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} {component.label && component.style?.labelDisplay !== false && (