"use client"; import React, { useState, useEffect, useMemo } 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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Check, ChevronsUpDown, Search, Plus, X, ChevronUp, ChevronDown, Type, Database } from "lucide-react"; import { cn } from "@/lib/utils"; import { ComponentData } from "@/types/screen"; import { apiClient } from "@/lib/api/client"; import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel"; import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel"; import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel"; // πŸ†• 제λͺ© 블둝 νƒ€μž… interface TitleBlock { id: string; type: "text" | "field"; value: string; // text: ν…μŠ€νŠΈ λ‚΄μš©, field: 컬럼λͺ… tableName?: string; // field일 λ•Œ ν…Œμ΄λΈ”λͺ… label?: string; // field일 λ•Œ ν‘œμ‹œμš© 라벨 } interface ButtonConfigPanelProps { component: ComponentData; onUpdateProperty: (path: string, value: any) => void; allComponents?: ComponentData[]; // πŸ†• ν”Œλ‘œμš° μœ„μ ― κ°μ§€μš© currentTableName?: string; // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… (μžλ™ κ°μ§€μš©) currentScreenCompanyCode?: string; // ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ } interface ScreenOption { id: number; name: string; description?: string; } export const ButtonConfigPanel: React.FC = ({ component, onUpdateProperty, allComponents = [], // πŸ†• κΈ°λ³Έκ°’ 빈 λ°°μ—΄ currentTableName, // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… currentScreenCompanyCode, // ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ }) => { // πŸ”§ componentμ—μ„œ 직접 읽기 (useMemo 제거) const config = component.componentConfig || {}; const currentAction = component.componentConfig?.action || {}; // 둜컬 μƒνƒœ 관리 (μ‹€μ‹œκ°„ μž…λ ₯ 반영) const [localInputs, setLocalInputs] = useState({ text: config.text !== undefined ? config.text : "λ²„νŠΌ", modalTitle: String(config.action?.modalTitle || ""), modalDescription: String(config.action?.modalDescription || ""), editModalTitle: String(config.action?.editModalTitle || ""), editModalDescription: String(config.action?.editModalDescription || ""), targetUrl: String(config.action?.targetUrl || ""), }); 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(""); // ν…Œμ΄λΈ” 컬럼 λͺ©λ‘ μƒνƒœ const [tableColumns, setTableColumns] = useState([]); const [columnsLoading, setColumnsLoading] = useState(false); const [displayColumnOpen, setDisplayColumnOpen] = useState(false); const [displayColumnSearch, setDisplayColumnSearch] = useState(""); // πŸ†• 제λͺ© 블둝 λΉŒλ” μƒνƒœ const [titleBlocks, setTitleBlocks] = useState([]); const [availableTables, setAvailableTables] = useState>([]); // μ‹œμŠ€ν…œμ˜ λͺ¨λ“  ν…Œμ΄λΈ” λͺ©λ‘ const [tableColumnsMap, setTableColumnsMap] = useState>>({}); const [blockTableSearches, setBlockTableSearches] = useState>({}); // 블둝별 ν…Œμ΄λΈ” 검색어 const [blockColumnSearches, setBlockColumnSearches] = useState>({}); // 블둝별 컬럼 검색어 const [blockTablePopoverOpen, setBlockTablePopoverOpen] = useState>({}); // 블둝별 ν…Œμ΄λΈ” Popover μ—΄λ¦Ό μƒνƒœ const [blockColumnPopoverOpen, setBlockColumnPopoverOpen] = useState>({}); // 블둝별 컬럼 Popover μ—΄λ¦Ό μƒνƒœ // πŸ†• 데이터 전달 ν•„λ“œ λ§€ν•‘μš© μƒνƒœ const [mappingSourceColumns, setMappingSourceColumns] = useState>([]); const [mappingTargetColumns, setMappingTargetColumns] = useState>([]); const [mappingSourcePopoverOpen, setMappingSourcePopoverOpen] = useState>({}); const [mappingTargetPopoverOpen, setMappingTargetPopoverOpen] = useState>({}); const [mappingSourceSearch, setMappingSourceSearch] = useState>({}); const [mappingTargetSearch, setMappingTargetSearch] = useState>({}); // 🎯 ν”Œλ‘œμš° μœ„μ ―μ΄ 화면에 μžˆλŠ”μ§€ 확인 const hasFlowWidget = useMemo(() => { const found = allComponents.some((comp: any) => { // ScreenDesignerμ—μ„œ μ €μž₯ν•˜λŠ” componentType 속성 확인! const compType = comp.componentType || comp.widgetType || ""; // "flow-widget" 체크 const isFlow = compType === "flow-widget" || compType?.toLowerCase().includes("flow"); if (isFlow) { console.log("βœ… ν”Œλ‘œμš° μœ„μ ― 발견!", { id: comp.id, componentType: comp.componentType }); } return isFlow; }); console.log("🎯 ν”Œλ‘œμš° μœ„μ ― 쑴재 μ—¬λΆ€:", found); return found; }, [allComponents]); // μ»΄ν¬λ„ŒνŠΈ prop λ³€κ²½ μ‹œ 둜컬 μƒνƒœ 동기화 (Input만) useEffect(() => { const latestConfig = component.componentConfig || {}; const latestAction = latestConfig.action || {}; setLocalInputs({ text: latestConfig.text !== undefined ? latestConfig.text : "λ²„νŠΌ", modalTitle: String(latestAction.modalTitle || ""), modalDescription: String(latestAction.modalDescription || ""), editModalTitle: String(latestAction.editModalTitle || ""), editModalDescription: String(latestAction.editModalDescription || ""), targetUrl: String(latestAction.targetUrl || ""), }); // πŸ†• 제λͺ© 블둝 μ΄ˆκΈ°ν™” if (latestAction.modalTitleBlocks && latestAction.modalTitleBlocks.length > 0) { setTitleBlocks(latestAction.modalTitleBlocks); } else { // κΈ°λ³Έκ°’: 빈 λ°°μ—΄ setTitleBlocks([]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [component.id]); // πŸ†• 제λͺ© 블둝 ν•Έλ“€λŸ¬ const addTextBlock = () => { const newBlock: TitleBlock = { id: `text-${Date.now()}`, type: "text", value: "", }; const updatedBlocks = [...titleBlocks, newBlock]; setTitleBlocks(updatedBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", updatedBlocks); }; const addFieldBlock = () => { const newBlock: TitleBlock = { id: `field-${Date.now()}`, type: "field", value: "", tableName: "", label: "", }; const updatedBlocks = [...titleBlocks, newBlock]; setTitleBlocks(updatedBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", updatedBlocks); }; const updateBlock = (id: string, updates: Partial) => { const updatedBlocks = titleBlocks.map((block) => block.id === id ? { ...block, ...updates } : block ); setTitleBlocks(updatedBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", updatedBlocks); }; const removeBlock = (id: string) => { const updatedBlocks = titleBlocks.filter((block) => block.id !== id); setTitleBlocks(updatedBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", updatedBlocks); }; const moveBlockUp = (id: string) => { const index = titleBlocks.findIndex((b) => b.id === id); if (index <= 0) return; const newBlocks = [...titleBlocks]; [newBlocks[index - 1], newBlocks[index]] = [newBlocks[index], newBlocks[index - 1]]; setTitleBlocks(newBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", newBlocks); }; const moveBlockDown = (id: string) => { const index = titleBlocks.findIndex((b) => b.id === id); if (index < 0 || index >= titleBlocks.length - 1) return; const newBlocks = [...titleBlocks]; [newBlocks[index], newBlocks[index + 1]] = [newBlocks[index + 1], newBlocks[index]]; setTitleBlocks(newBlocks); onUpdateProperty("componentConfig.action.modalTitleBlocks", newBlocks); }; // πŸ†• 제λͺ© 미리보기 생성 const generateTitlePreview = (): string => { if (titleBlocks.length === 0) return "(제λͺ© μ—†μŒ)"; return titleBlocks .map((block) => { if (block.type === "text") { return block.value || "(ν…μŠ€νŠΈ)"; } else { return block.label || block.value || "(ν•„λ“œ)"; } }) .join(""); }; // πŸ†• μ‹œμŠ€ν…œμ˜ λͺ¨λ“  ν…Œμ΄λΈ” λͺ©λ‘ λ‘œλ“œ useEffect(() => { const fetchAllTables = async () => { try { const response = await apiClient.get("/table-management/tables"); if (response.data.success && response.data.data) { const tables = response.data.data.map((table: any) => ({ name: table.tableName, label: table.displayName || table.tableName, })); setAvailableTables(tables); console.log(`βœ… 전체 ν…Œμ΄λΈ” λͺ©λ‘ λ‘œλ“œ 성곡:`, tables.length); } } catch (error) { console.error("ν…Œμ΄λΈ” λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); } }; fetchAllTables(); }, []); // πŸ†• νŠΉμ • ν…Œμ΄λΈ”μ˜ 컬럼 λ‘œλ“œ const loadTableColumns = async (tableName: string) => { if (!tableName || tableColumnsMap[tableName]) return; try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); console.log(`πŸ“₯ ν…Œμ΄λΈ” ${tableName} 컬럼 응닡:`, response.data); if (response.data.success) { // dataκ°€ 배열인지 확인 let columnData = response.data.data; // data.columns ν˜•νƒœμΌ μˆ˜λ„ 있음 if (!Array.isArray(columnData) && columnData?.columns) { columnData = columnData.columns; } // data.data ν˜•νƒœμΌ μˆ˜λ„ 있음 if (!Array.isArray(columnData) && columnData?.data) { columnData = columnData.data; } if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => { const name = col.name || col.columnName; const label = col.displayName || col.label || col.columnLabel || name; console.log(` - 컬럼: ${name} β†’ "${label}"`); return { name, label }; }); setTableColumnsMap((prev) => ({ ...prev, [tableName]: columns })); console.log(`βœ… ν…Œμ΄λΈ” ${tableName} 컬럼 λ‘œλ“œ 성곡:`, columns.length, "개"); } else { console.error("❌ 컬럼 데이터가 배열이 μ•„λ‹™λ‹ˆλ‹€:", columnData); } } } catch (error) { console.error("컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } }; // πŸ†• 데이터 전달 μ†ŒμŠ€/νƒ€κ²Ÿ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ useEffect(() => { const sourceTable = config.action?.dataTransfer?.sourceTable; const targetTable = config.action?.dataTransfer?.targetTable; const loadColumns = async () => { if (sourceTable) { try { const response = await apiClient.get(`/table-management/tables/${sourceTable}/columns`); if (response.data.success) { let columnData = response.data.data; if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ name: col.name || col.columnName, label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, })); setMappingSourceColumns(columns); } } } catch (error) { console.error("μ†ŒμŠ€ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } if (targetTable) { try { const response = await apiClient.get(`/table-management/tables/${targetTable}/columns`); if (response.data.success) { let columnData = response.data.data; if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ name: col.name || col.columnName, label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, })); setMappingTargetColumns(columns); } } } catch (error) { console.error("νƒ€κ²Ÿ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } }; loadColumns(); }, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]); // ν™”λ©΄ λͺ©λ‘ κ°€μ Έμ˜€κΈ° (ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ κΈ°μ€€) useEffect(() => { const fetchScreens = async () => { try { setScreensLoading(true); // ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ κΈ°μ€€μœΌλ‘œ ν™”λ©΄ λͺ©λ‘ 쑰회 const params: any = { page: 1, size: 9999, // 맀우 큰 κ°’μœΌλ‘œ μ„€μ •ν•˜μ—¬ 전체 λͺ©λ‘ κ°€μ Έμ˜€κΈ° }; // ν˜„μž¬ ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œκ°€ 있으면 필터링 νŒŒλΌλ―Έν„°λ‘œ 전달 if (currentScreenCompanyCode) { params.companyCode = currentScreenCompanyCode; } const response = await apiClient.get("/screen-management/screens", { params, }); 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); } } catch (error) { // console.error("❌ ν™”λ©΄ λͺ©λ‘ λ‘œλ”© μ‹€νŒ¨:", error); } finally { setScreensLoading(false); } }; fetchScreens(); }, [currentScreenCompanyCode]); // ν…Œμ΄λΈ” 컬럼 λͺ©λ‘ κ°€μ Έμ˜€κΈ° (ν…Œμ΄λΈ” 이λ ₯ 보기 μ•‘μ…˜μΌ λ•Œ) useEffect(() => { const fetchTableColumns = async () => { // ν…Œμ΄λΈ” 이λ ₯ 보기 μ•‘μ…˜μ΄ μ•„λ‹ˆλ©΄ μŠ€ν‚΅ if (config.action?.type !== "view_table_history") { return; } // 1. μˆ˜λ™ μž…λ ₯된 ν…Œμ΄λΈ”λͺ… μš°μ„  // 2. μ—†μœΌλ©΄ ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… μ‚¬μš© const tableName = config.action?.historyTableName || currentTableName; // ν…Œμ΄λΈ”λͺ…이 μ—†μœΌλ©΄ μŠ€ν‚΅ if (!tableName) { return; } try { setColumnsLoading(true); const response = await apiClient.get(`/table-management/tables/${tableName}/columns`, { params: { page: 1, size: 9999, // 전체 컬럼 κ°€μ Έμ˜€κΈ° }, }); // API 응닡 ꡬ쑰: { success, data: { columns: [...], total, page, totalPages } } const columnData = response.data.data?.columns; if (!columnData || !Array.isArray(columnData)) { console.error("❌ 컬럼 데이터가 배열이 μ•„λ‹™λ‹ˆλ‹€:", columnData); setTableColumns([]); return; } if (response.data.success) { // ID 컬럼과 λ‚ μ§œ κ΄€λ ¨ 컬럼 μ œμ™Έ const filteredColumns = columnData .filter((col: any) => { const colName = col.columnName.toLowerCase(); const dataType = col.dataType?.toLowerCase() || ""; // ID 컬럼 μ œμ™Έ (id, _id둜 λλ‚˜λŠ” 컬럼) if (colName === "id" || colName.endsWith("_id")) { return false; } // λ‚ μ§œ/μ‹œκ°„ νƒ€μž… μ œμ™Έ (데이터 νƒ€μž… κΈ°μ€€) if (dataType.includes("date") || dataType.includes("time") || dataType.includes("timestamp")) { return false; } // λ‚ μ§œ/μ‹œκ°„ κ΄€λ ¨ 컬럼λͺ… μ œμ™Έ (컬럼λͺ…에 date, time, at 포함) if ( colName.includes("date") || colName.includes("time") || colName.endsWith("_at") || colName.startsWith("created") || colName.startsWith("updated") ) { return false; } return true; }) .map((col: any) => col.columnName); setTableColumns(filteredColumns); } } catch (error) { console.error("❌ ν…Œμ΄λΈ” 컬럼 λ‘œλ”© μ‹€νŒ¨:", error); } finally { setColumnsLoading(false); } }; fetchTableColumns(); }, [config.action?.type, config.action?.historyTableName, currentTableName]); // 검색 필터링 ν•¨μˆ˜ 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 (
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, text: newValue })); onUpdateProperty("componentConfig.text", newValue); }} placeholder="λ²„νŠΌ ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•˜μ„Έμš”" />
{/* λͺ¨λ‹¬ μ—΄κΈ° μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "modal" && (

λͺ¨λ‹¬ μ„€μ •

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, modalTitle: newValue })); onUpdateProperty("componentConfig.action.modalTitle", newValue); }} />
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, modalDescription: newValue })); onUpdateProperty("componentConfig.action.modalDescription", newValue); }} />

λͺ¨λ‹¬ 제λͺ© μ•„λž˜μ— ν‘œμ‹œλ©λ‹ˆλ‹€

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.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}
)} {/* πŸ†• 데이터 전달 + λͺ¨λ‹¬ μ—΄κΈ° μ•‘μ…˜ μ„€μ • */} {component.componentConfig?.action?.type === "openModalWithData" && (

데이터 전달 + λͺ¨λ‹¬ μ„€μ •

TableListμ—μ„œ μ„ νƒλœ 데이터λ₯Ό λ‹€μŒ λͺ¨λ‹¬λ‘œ μ „λ‹¬ν•©λ‹ˆλ‹€

{ onUpdateProperty("componentConfig.action.dataSourceId", e.target.value); }} />

✨ λΉ„μ›Œλ‘λ©΄ ν˜„μž¬ ν™”λ©΄μ˜ TableListλ₯Ό μžλ™μœΌλ‘œ κ°μ§€ν•©λ‹ˆλ‹€

β€’ μžλ™ 감지: ν˜„μž¬ ν™”λ©΄μ˜ TableList 선택 데이터
β€’ λˆ„μ  전달: 이전 λͺ¨λ‹¬μ˜ λͺ¨λ“  데이터도 μžλ™μœΌλ‘œ ν•¨κ»˜ 전달
β€’ λ‹€μŒ ν™”λ©΄μ—μ„œ tableName으둜 λ°”λ‘œ μ‚¬μš© κ°€λŠ₯
β€’ μˆ˜λ™ μ„€μ •: ν•„μš”μ‹œ 직접 ν…Œμ΄λΈ”λͺ… μž…λ ₯ (예: item_info)

{/* πŸ†• 블둝 기반 제λͺ© λΉŒλ” */}
{/* 블둝 λͺ©λ‘ */}
{titleBlocks.length === 0 ? (
ν…μŠ€νŠΈλ‚˜ ν•„λ“œλ₯Ό μΆ”κ°€ν•˜μ—¬ 제λͺ©μ„ κ΅¬μ„±ν•˜μ„Έμš”
) : ( titleBlocks.map((block, index) => (
{/* μˆœμ„œ λ³€κ²½ λ²„νŠΌ */}
{/* 블둝 νƒ€μž… ν‘œμ‹œ */}
{block.type === "text" ? ( ) : ( )}
{/* 블둝 μ„€μ • */}
{block.type === "text" ? ( // ν…μŠ€νŠΈ 블둝 updateBlock(block.id, { value: e.target.value })} className="h-7 text-xs" /> ) : ( // ν•„λ“œ 블둝 <> {/* ν…Œμ΄λΈ” 선택 - Combobox */} { setBlockTablePopoverOpen((prev) => ({ ...prev, [block.id]: open })); }} > { setBlockTableSearches((prev) => ({ ...prev, [block.id]: value })); }} /> ν…Œμ΄λΈ”μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. {availableTables .filter((table) => { const search = (blockTableSearches[block.id] || "").toLowerCase(); if (!search) return true; return ( table.label.toLowerCase().includes(search) || table.name.toLowerCase().includes(search) ); }) .map((table) => ( { updateBlock(block.id, { tableName: table.name, value: "" }); loadTableColumns(table.name); setBlockTableSearches((prev) => ({ ...prev, [block.id]: "" })); setBlockTablePopoverOpen((prev) => ({ ...prev, [block.id]: false })); }} className="text-xs" > {table.label} ({table.name}) ))} {block.tableName && ( <> {/* 컬럼 선택 - Combobox (라벨λͺ… ν‘œμ‹œ) */} { setBlockColumnPopoverOpen((prev) => ({ ...prev, [block.id]: open })); }} > { setBlockColumnSearches((prev) => ({ ...prev, [block.id]: value })); }} /> μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. {(tableColumnsMap[block.tableName] || []) .filter((col) => { const search = (blockColumnSearches[block.id] || "").toLowerCase(); if (!search) return true; return ( col.label.toLowerCase().includes(search) || col.name.toLowerCase().includes(search) ); }) .map((col) => ( { updateBlock(block.id, { value: col.name, label: col.label, }); setBlockColumnSearches((prev) => ({ ...prev, [block.id]: "" })); setBlockColumnPopoverOpen((prev) => ({ ...prev, [block.id]: false })); }} className="text-xs" > {col.label} ({col.name}) ))} updateBlock(block.id, { label: e.target.value })} className="h-7 text-xs" /> )} )}
{/* μ‚­μ œ λ²„νŠΌ */}
)) )}
{/* 미리보기 */} {titleBlocks.length > 0 && (
미리보기: {generateTitlePreview()}
)}

β€’ ν…μŠ€νŠΈ: κ³ μ • ν…μŠ€νŠΈ μž…λ ₯ (예: "ν’ˆλͺ© 상세정보 - ")
β€’ ν•„λ“œ: 이전 ν™”λ©΄ λ°μ΄ν„°λ‘œ μžλ™ μ±„μ›Œμ§ (예: ν’ˆλͺ©λͺ…, 규격)
β€’ μˆœμ„œ λ³€κ²½: ↑↓ λ²„νŠΌμœΌλ‘œ 자유둭게 배치
β€’ 데이터가 μ—†μœΌλ©΄ "ν‘œμ‹œ 라벨"이 λŒ€μ‹  ν‘œμ‹œλ©λ‹ˆλ‹€

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.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

SelectedItemsDetailInput μ»΄ν¬λ„ŒνŠΈκ°€ μžˆλŠ” 화면을 μ„ νƒν•˜μ„Έμš”

)} {/* μˆ˜μ • μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "edit" && (

μˆ˜μ • μ„€μ •

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.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

μ„ νƒλœ 데이터가 이 폼 화면에 μžλ™μœΌλ‘œ λ‘œλ“œλ˜μ–΄ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€

{(component.componentConfig?.action?.editMode || "modal") === "modal" && ( <>
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue })); onUpdateProperty("componentConfig.action.editModalTitle", newValue); onUpdateProperty("webTypeConfig.editModalTitle", newValue); }} />

λΉ„μ›Œλ‘λ©΄ κΈ°λ³Έ 제λͺ©μ΄ ν‘œμ‹œλ©λ‹ˆλ‹€

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue })); onUpdateProperty("componentConfig.action.editModalDescription", newValue); onUpdateProperty("webTypeConfig.editModalDescription", newValue); }} />

λΉ„μ›Œλ‘λ©΄ μ„€λͺ…이 ν‘œμ‹œλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€

)}
)} {/* 볡사 μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "copy" && (

볡사 μ„€μ • (ν’ˆλͺ©μ½”λ“œ μžλ™ μ΄ˆκΈ°ν™”)

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.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

μ„ νƒλœ 데이터가 λ³΅μ‚¬λ˜λ©°, ν’ˆλͺ©μ½”λ“œλŠ” μžλ™μœΌλ‘œ μ΄ˆκΈ°ν™”λ©λ‹ˆλ‹€

{(component.componentConfig?.action?.editMode || "modal") === "modal" && ( <>
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue })); onUpdateProperty("componentConfig.action.editModalTitle", newValue); onUpdateProperty("webTypeConfig.editModalTitle", newValue); }} />

λΉ„μ›Œλ‘λ©΄ κΈ°λ³Έ 제λͺ©μ΄ ν‘œμ‹œλ©λ‹ˆλ‹€

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue })); onUpdateProperty("componentConfig.action.editModalDescription", newValue); onUpdateProperty("webTypeConfig.editModalDescription", newValue); }} />

λΉ„μ›Œλ‘λ©΄ μ„€λͺ…이 ν‘œμ‹œλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€

)}
)} {/* ν…Œμ΄λΈ” 이λ ₯ 보기 μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "view_table_history" && (
μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. {tableColumns.map((column) => ( { onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue); setDisplayColumnOpen(false); }} className="text-xs" > {column} ))}
)} {/* νŽ˜μ΄μ§€ 이동 μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "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.targetScreenId", screen.id); setNavScreenOpen(false); setNavSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

μ„ νƒν•œ ν™”λ©΄μœΌλ‘œ /screens/{"{"}ν™”λ©΄ID{"}"} ν˜•νƒœλ‘œ μ΄λ™ν•©λ‹ˆλ‹€

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, targetUrl: newValue })); onUpdateProperty("componentConfig.action.targetUrl", newValue); }} className="h-6 w-full px-2 py-0 text-xs" />

URL을 μž…λ ₯ν•˜λ©΄ ν™”λ©΄ 선택보닀 μš°μ„  μ μš©λ©λ‹ˆλ‹€

)} {/* μ—‘μ…€ λ‹€μš΄λ‘œλ“œ μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "excel_download" && (

μ—‘μ…€ λ‹€μš΄λ‘œλ“œ μ„€μ •

onUpdateProperty("componentConfig.action.excelFileName", e.target.value)} className="h-8 text-xs" />

ν™•μž₯자(.xlsx)λŠ” μžλ™μœΌλ‘œ μΆ”κ°€λ©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.excelSheetName", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.excelIncludeHeaders", checked)} />
)} {/* μ—‘μ…€ μ—…λ‘œλ“œ μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "excel_upload" && (

πŸ“€ μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ •

{(config.action?.excelUploadMode === "update" || config.action?.excelUploadMode === "upsert") && (
onUpdateProperty("componentConfig.action.excelKeyColumn", e.target.value)} className="h-8 text-xs" />

UPDATE/UPSERT μ‹œ 기쀀이 λ˜λŠ” 컬럼λͺ…

)}
)} {/* λ°”μ½”λ“œ μŠ€μΊ” μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "barcode_scan" && (

πŸ“· λ°”μ½”λ“œ μŠ€μΊ” μ„€μ •

onUpdateProperty("componentConfig.action.barcodeTargetField", e.target.value)} className="h-8 text-xs" />

μŠ€μΊ” κ²°κ³Όκ°€ μž…λ ₯될 폼 ν•„λ“œλͺ…

onUpdateProperty("componentConfig.action.barcodeAutoSubmit", checked)} />
)} {/* μ½”λ“œ 병합 μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "code_merge" && (

πŸ”€ μ½”λ“œ 병합 μ„€μ •

onUpdateProperty("componentConfig.action.mergeColumnName", e.target.value)} className="h-8 text-xs" />

병합할 컬럼λͺ… (예: item_code). 이 컬럼이 μžˆλŠ” λͺ¨λ“  ν…Œμ΄λΈ”μ— 병합이 μ μš©λ©λ‹ˆλ‹€.

영ν–₯받을 ν…Œμ΄λΈ”κ³Ό ν–‰ 수λ₯Ό 미리 ν™•μΈν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.mergeShowPreview", checked)} />

μ‚¬μš© 방법:
1. ν…Œμ΄λΈ”μ—μ„œ 병합할 두 개의 행을 μ„ νƒν•©λ‹ˆλ‹€
2. 이 λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ 병합 λ°©ν–₯을 선택할 수 μžˆμŠ΅λ‹ˆλ‹€
3. λ°μ΄ν„°λŠ” μ‚­μ œλ˜μ§€ μ•Šκ³ , 컬럼 κ°’λ§Œ λ³€κ²½λ©λ‹ˆλ‹€

)} {/* μœ„μΉ˜μ •λ³΄ κ°€μ Έμ˜€κΈ° μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "geolocation" && (

πŸ“ μœ„μΉ˜μ •λ³΄ μ„€μ •

{/* ν…Œμ΄λΈ” 선택 */}

μœ„μΉ˜ 정보λ₯Ό μ €μž₯ν•  ν…Œμ΄λΈ” (κΈ°λ³Έ: ν˜„μž¬ ν™”λ©΄ ν…Œμ΄λΈ”)

onUpdateProperty("componentConfig.action.geolocationLatField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationLngField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationAccuracyField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationTimestampField", e.target.value)} className="h-8 text-xs" />

GPSλ₯Ό μ‚¬μš©ν•˜μ—¬ 더 μ •ν™•ν•œ μœ„μΉ˜ (배터리 μ†Œλͺ¨ 증가)

onUpdateProperty("componentConfig.action.geolocationHighAccuracy", checked)} />

μœ„μΉ˜ 정보λ₯Ό κ°€μ Έμ˜¨ ν›„ μžλ™μœΌλ‘œ 폼을 μ €μž₯ν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.geolocationAutoSave", checked)} />
{/* 첫 번째 μΆ”κ°€ ν…Œμ΄λΈ” μ„€μ • (μœ„μΉ˜μ •λ³΄μ™€ ν•¨κ»˜ μƒνƒœ λ³€κ²½) */}

μœ„μΉ˜μ •λ³΄μ™€ ν•¨κ»˜ λ‹€λ₯Έ ν…Œμ΄λΈ”μ˜ ν•„λ“œ 값을 λ³€κ²½ν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.geolocationUpdateField", checked)} />
{config.action?.geolocationUpdateField && (
onUpdateProperty("componentConfig.action.geolocationExtraField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationExtraValue", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationExtraKeyField", e.target.value)} className="h-8 text-xs" />
)}
{/* 두 번째 μΆ”κ°€ ν…Œμ΄λΈ” μ„€μ • */}

두 번째 ν…Œμ΄λΈ”μ˜ ν•„λ“œ 값도 ν•¨κ»˜ λ³€κ²½ν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.geolocationSecondTableEnabled", checked)} />
{config.action?.geolocationSecondTableEnabled && (
onUpdateProperty("componentConfig.action.geolocationSecondField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationSecondValue", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.geolocationSecondKeyField", e.target.value)} className="h-8 text-xs" />
{config.action?.geolocationSecondMode === "insert" && (

μœ„λ„/경도λ₯Ό 이 ν…Œμ΄λΈ”μ—λ„ μ €μž₯

onUpdateProperty("componentConfig.action.geolocationSecondInsertFields", { ...config.action?.geolocationSecondInsertFields, includeLocation: checked })} />
)}

{config.action?.geolocationSecondMode === "insert" ? "μƒˆ λ ˆμ½”λ“œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. μ—°κ²° ν•„λ“œλ‘œ ν˜„μž¬ 폼 데이터와 μ—°κ²°λ©λ‹ˆλ‹€." : "κΈ°μ‘΄ λ ˆμ½”λ“œλ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€. ν‚€ ν•„λ“œλ‘œ λ ˆμ½”λ“œλ₯Ό μ°Ύμ•„ 값을 λ³€κ²½ν•©λ‹ˆλ‹€."}

)}

μ‚¬μš© 방법:
1. λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ λΈŒλΌμš°μ €κ°€ μœ„μΉ˜ κΆŒν•œμ„ μš”μ²­ν•©λ‹ˆλ‹€
2. μ‚¬μš©μžκ°€ ν—ˆμš©ν•˜λ©΄ ν˜„μž¬ GPS μ’Œν‘œλ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€
3. μœ„λ„/경도가 μ§€μ •λœ ν•„λ“œμ— μžλ™μœΌλ‘œ μž…λ ₯λ©λ‹ˆλ‹€
4. μΆ”κ°€ ν…Œμ΄λΈ” 섀정이 있으면 ν•΄λ‹Ή ν…Œμ΄λΈ”μ˜ ν•„λ“œλ„ ν•¨κ»˜ λ³€κ²½λ©λ‹ˆλ‹€

μ˜ˆμ‹œ: μœ„μΉ˜μ •λ³΄ μ €μž₯ + vehicles.statusλ₯Ό inactive둜 λ³€κ²½

μ°Έκ³ : HTTPS ν™˜κ²½μ—μ„œλ§Œ μœ„μΉ˜μ •λ³΄κ°€ μž‘λ™ν•©λ‹ˆλ‹€.

)} {/* ν•„λ“œ κ°’ λ³€κ²½ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "update_field" && (

πŸ“ ν•„λ“œ κ°’ λ³€κ²½ μ„€μ •

ν•„λ“œ 값을 λ³€κ²½ν•  ν…Œμ΄λΈ” (κΈ°λ³Έ: ν˜„μž¬ ν™”λ©΄ ν…Œμ΄λΈ”)

onUpdateProperty("componentConfig.action.updateTargetField", e.target.value)} className="h-8 text-xs" />

λ³€κ²½ν•  DB 컬럼

onUpdateProperty("componentConfig.action.updateTargetValue", e.target.value)} className="h-8 text-xs" />

λ³€κ²½ν•  κ°’ (λ¬Έμžμ—΄, 숫자)

{/* πŸ†• ν‚€ ν•„λ“œ μ„€μ • (λ ˆμ½”λ“œ μ‹λ³„μš©) */}
λ ˆμ½”λ“œ 식별 μ„€μ •
onUpdateProperty("componentConfig.action.updateKeyField", e.target.value)} className="h-8 text-xs" />

λ ˆμ½”λ“œλ₯Ό 찾을 DB 컬럼λͺ…

ν‚€ 값을 κ°€μ Έμ˜¬ μ†ŒμŠ€

λ²„νŠΌ 클릭 μ‹œ μ¦‰μ‹œ DB에 μ €μž₯

onUpdateProperty("componentConfig.action.updateAutoSave", checked)} />
onUpdateProperty("componentConfig.action.confirmMessage", e.target.value)} className="h-8 text-xs" />

μž…λ ₯ν•˜λ©΄ λ³€κ²½ μ „ 확인 창이 ν‘œμ‹œλ©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.successMessage", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.errorMessage", e.target.value)} className="h-8 text-xs" />
{/* μœ„μΉ˜μ •λ³΄ μˆ˜μ§‘ μ˜΅μ…˜ */}

μƒνƒœ λ³€κ²½κ³Ό ν•¨κ»˜ ν˜„μž¬ GPS μ’Œν‘œλ₯Ό μˆ˜μ§‘ν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.updateWithGeolocation", checked)} />
{config.action?.updateWithGeolocation && (
onUpdateProperty("componentConfig.action.updateGeolocationLatField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.updateGeolocationLngField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.updateGeolocationAccuracyField", e.target.value)} className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.updateGeolocationTimestampField", e.target.value)} className="h-8 text-xs" />

λ²„νŠΌ 클릭 μ‹œ GPS μœ„μΉ˜λ₯Ό μˆ˜μ§‘ν•˜μ—¬ μœ„ ν•„λ“œμ— μ €μž₯ν•©λ‹ˆλ‹€.

)}

μ‚¬μš© μ˜ˆμ‹œ:
- μš΄ν–‰μ•Œλ¦Ό λ²„νŠΌ: statusλ₯Ό "active"둜 + μœ„μΉ˜μ •λ³΄ μˆ˜μ§‘
- 좜발 λ²„νŠΌ: statusλ₯Ό "inactive"둜 + μœ„μΉ˜μ •λ³΄ μˆ˜μ§‘
- μ™„λ£Œ λ²„νŠΌ: is_completedλ₯Ό "Y"둜 λ³€κ²½

)} {/* 데이터 전달 μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "transferData" && (

πŸ“¦ 데이터 전달 μ„€μ •

{/* μ†ŒμŠ€ μ»΄ν¬λ„ŒνŠΈ 선택 (Combobox) */}

ν…Œμ΄λΈ”, 반볡 ν•„λ“œ κ·Έλ£Ή λ“± 데이터λ₯Ό μ œκ³΅ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ

{config.action?.dataTransfer?.targetType === "splitPanel" && (

이 λ²„νŠΌμ΄ λΆ„ν•  νŒ¨λ„ 내뢀에 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€. 쒌츑 ν™”λ©΄μ—μ„œ 우츑으둜, λ˜λŠ” μš°μΈ‘μ—μ„œ 쒌츑으둜 데이터가 μ „λ‹¬λ©λ‹ˆλ‹€.

)}
{/* νƒ€κ²Ÿ μ»΄ν¬λ„ŒνŠΈ 선택 (같은 ν™”λ©΄μ˜ μ»΄ν¬λ„ŒνŠΈμΌ λ•Œλ§Œ) */} {config.action?.dataTransfer?.targetType === "component" && (

ν…Œμ΄λΈ”, 반볡 ν•„λ“œ κ·Έλ£Ή λ“± 데이터λ₯Ό λ°›λŠ” μ»΄ν¬λ„ŒνŠΈ

)} {/* λΆ„ν•  νŒ¨λ„ λ°˜λŒ€νŽΈ νƒ€κ²Ÿ μ„€μ • */} {config.action?.dataTransfer?.targetType === "splitPanel" && (
onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value)} placeholder="λΉ„μ›Œλ‘λ©΄ 첫 번째 μˆ˜μ‹  κ°€λŠ₯ μ»΄ν¬λ„ŒνŠΈλ‘œ 전달" className="h-8 text-xs" />

λ°˜λŒ€νŽΈ ν™”λ©΄μ˜ νŠΉμ • μ»΄ν¬λ„ŒνŠΈ IDλ₯Ό μ§€μ •ν•˜κ±°λ‚˜, λΉ„μ›Œλ‘λ©΄ μžλ™μœΌλ‘œ 첫 번째 μˆ˜μ‹  κ°€λŠ₯ μ»΄ν¬λ„ŒνŠΈλ‘œ μ „λ‹¬λ©λ‹ˆλ‹€.

)}

κΈ°μ‘΄ 데이터λ₯Ό μ–΄λ–»κ²Œ μ²˜λ¦¬ν• μ§€ 선택

데이터 전달 ν›„ μ†ŒμŠ€μ˜ 선택을 ν•΄μ œν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.dataTransfer.clearAfterTransfer", checked)} />

데이터 전달 μ „ 확인 λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.dataTransfer.confirmBeforeTransfer", checked)} />
{config.action?.dataTransfer?.confirmBeforeTransfer && (
onUpdateProperty("componentConfig.action.dataTransfer.confirmMessage", e.target.value)} className="h-8 text-xs" />
)}
onUpdateProperty("componentConfig.action.dataTransfer.validation.minSelection", parseInt(e.target.value) || 0)} className="h-8 w-20 text-xs" />
onUpdateProperty("componentConfig.action.dataTransfer.validation.maxSelection", parseInt(e.target.value) || undefined)} className="h-8 w-20 text-xs" />

쑰건뢀 μ»¨ν…Œμ΄λ„ˆμ˜ μΉ΄ν…Œκ³ λ¦¬ κ°’ λ“± μΆ”κ°€ 데이터λ₯Ό ν•¨κ»˜ 전달할 수 μžˆμŠ΅λ‹ˆλ‹€

쑰건뢀 μ»¨ν…Œμ΄λ„ˆ, μ…€λ ‰νŠΈλ°•μŠ€ λ“± (μΉ΄ν…Œκ³ λ¦¬ κ°’ μ „λ‹¬μš©)

{ const currentSources = config.action?.dataTransfer?.additionalSources || []; const newSources = [...currentSources]; if (newSources.length === 0) { newSources.push({ componentId: "", fieldName: e.target.value }); } else { newSources[0] = { ...newSources[0], fieldName: e.target.value }; } onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); }} className="h-8 text-xs" />

νƒ€κ²Ÿ ν…Œμ΄λΈ”μ— μ €μž₯될 ν•„λ“œλͺ…

{/* ν•„λ“œ λ§€ν•‘ κ·œμΉ™ */}
{/* μ†ŒμŠ€/νƒ€κ²Ÿ ν…Œμ΄λΈ” 선택 */}
ν…Œμ΄λΈ”μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {availableTables.map((table) => ( { onUpdateProperty("componentConfig.action.dataTransfer.sourceTable", table.name); }} className="text-xs" > {table.label} ({table.name}) ))}
ν…Œμ΄λΈ”μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {availableTables.map((table) => ( { onUpdateProperty("componentConfig.action.dataTransfer.targetTable", table.name); }} className="text-xs" > {table.label} ({table.name}) ))}
{/* ν•„λ“œ λ§€ν•‘ κ·œμΉ™ */}

μ†ŒμŠ€ ν•„λ“œλ₯Ό νƒ€κ²Ÿ ν•„λ“œμ— λ§€ν•‘ν•©λ‹ˆλ‹€. λΉ„μ›Œλ‘λ©΄ 같은 μ΄λ¦„μ˜ ν•„λ“œλ‘œ μžλ™ λ§€ν•‘λ©λ‹ˆλ‹€.

{(!config.action?.dataTransfer?.sourceTable || !config.action?.dataTransfer?.targetTable) ? (

λ¨Όμ € μ†ŒμŠ€ ν…Œμ΄λΈ”κ³Ό νƒ€κ²Ÿ ν…Œμ΄λΈ”μ„ μ„ νƒν•˜μ„Έμš”.

) : (config.action?.dataTransfer?.mappingRules || []).length === 0 ? (

λ§€ν•‘ κ·œμΉ™μ΄ μ—†μŠ΅λ‹ˆλ‹€. 같은 μ΄λ¦„μ˜ ν•„λ“œλ‘œ μžλ™ λ§€ν•‘λ©λ‹ˆλ‹€.

) : (
{(config.action?.dataTransfer?.mappingRules || []).map((rule: any, index: number) => (
{/* μ†ŒμŠ€ ν•„λ“œ 선택 (Combobox) */}
setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))} > setMappingSourceSearch((prev) => ({ ...prev, [index]: value }))} /> μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {mappingSourceColumns.map((col) => ( { const rules = [...(config.action?.dataTransfer?.mappingRules || [])]; rules[index] = { ...rules[index], sourceField: col.name }; onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules); setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: false })); }} className="text-xs" > {col.label} {col.label !== col.name && ( ({col.name}) )} ))}
β†’ {/* νƒ€κ²Ÿ ν•„λ“œ 선택 (Combobox) */}
setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))} > setMappingTargetSearch((prev) => ({ ...prev, [index]: value }))} /> μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {mappingTargetColumns.map((col) => ( { const rules = [...(config.action?.dataTransfer?.mappingRules || [])]; rules[index] = { ...rules[index], targetField: col.name }; onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules); setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: false })); }} className="text-xs" > {col.label} {col.label !== col.name && ( ({col.name}) )} ))}
))}
)}

μ‚¬μš© 방법:
1. μ†ŒμŠ€ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 데이터λ₯Ό μ„ νƒν•©λ‹ˆλ‹€
2. ν•„λ“œ λ§€ν•‘ κ·œμΉ™μ„ μ„€μ •ν•©λ‹ˆλ‹€ (예: ν’ˆλ²ˆ β†’ ν’ˆλͺ©μ½”λ“œ)
3. 이 λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ λ§€ν•‘λœ 데이터가 νƒ€κ²ŸμœΌλ‘œ μ „λ‹¬λ©λ‹ˆλ‹€

)} {/* μ œμ–΄ κΈ°λŠ₯ μ„Ήμ…˜ */}
{/* πŸ†• ν”Œλ‘œμš° 단계별 ν‘œμ‹œ μ œμ–΄ μ„Ήμ…˜ (ν”Œλ‘œμš° μœ„μ ―μ΄ μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ) */} {hasFlowWidget && (
)}
); };