"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"; import { QuickInsertConfigSection } from "./QuickInsertConfigSection"; // πŸ†• 제λͺ© 블둝 νƒ€μž… 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 || ""), groupByColumn: String(config.action?.groupByColumns?.[0] || ""), }); 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>({}); // πŸ†• openModalWithData μ „μš© ν•„λ“œ λ§€ν•‘ μƒνƒœ const [modalSourceColumns, setModalSourceColumns] = useState>([]); const [modalTargetColumns, setModalTargetColumns] = useState>([]); const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState>({}); const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState>({}); // πŸ†• κ·Έλ£Ήν™” 컬럼 μ„ νƒμš© μƒνƒœ const [currentTableColumns, setCurrentTableColumns] = useState>([]); const [groupByColumnOpen, setGroupByColumnOpen] = useState(false); const [groupByColumnSearch, setGroupByColumnSearch] = useState(""); const [modalSourceSearch, setModalSourceSearch] = useState>({}); const [modalTargetSearch, setModalTargetSearch] = 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 || ""), groupByColumn: String(latestAction.groupByColumns?.[0] || ""), }); // πŸ†• 제λͺ© 블둝 μ΄ˆκΈ°ν™” 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(() => { if (!currentTableName) return; const loadCurrentTableColumns = async () => { try { const response = await apiClient.get(`/table-management/tables/${currentTableName}/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, })); setCurrentTableColumns(columns); console.log(`βœ… ν˜„μž¬ ν…Œμ΄λΈ” ${currentTableName} 컬럼 λ‘œλ“œ 성곡:`, columns.length, "개"); } } } catch (error) { console.error("ν˜„μž¬ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } }; loadCurrentTableColumns(); }, [currentTableName]); // πŸ†• openModalWithData μ†ŒμŠ€/νƒ€κ²Ÿ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ useEffect(() => { const actionType = config.action?.type; if (actionType !== "openModalWithData") return; const loadModalMappingColumns = async () => { // μ†ŒμŠ€ ν…Œμ΄λΈ”: ν˜„μž¬ ν™”λ©΄μ˜ λΆ„ν•  νŒ¨λ„ λ˜λŠ” ν…Œμ΄λΈ”μ—μ„œ 감지 let sourceTableName: string | null = null; console.log("[openModalWithData] 컬럼 λ‘œλ“œ μ‹œμž‘:", { allComponentsCount: allComponents.length, currentTableName, targetScreenId: config.action?.targetScreenId, }); // λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈ νƒ€μž… 둜그 allComponents.forEach((comp, idx) => { const compType = comp.componentType || (comp as any).componentConfig?.type; console.log( ` [${idx}] componentType: ${compType}, tableName: ${(comp as any).componentConfig?.tableName || (comp as any).componentConfig?.leftPanel?.tableName || "N/A"}`, ); }); for (const comp of allComponents) { const compType = comp.componentType || (comp as any).componentConfig?.type; const compConfig = (comp as any).componentConfig || {}; // λΆ„ν•  νŒ¨λ„ νƒ€μž…λ“€ (λ‹€μ–‘ν•œ κ²½λ‘œμ—μ„œ ν…Œμ΄λΈ”λͺ… μΆ”μΆœ) if (compType === "split-panel-layout" || compType === "screen-split-panel") { sourceTableName = compConfig?.leftPanel?.tableName || compConfig?.leftTableName || compConfig?.tableName; if (sourceTableName) { console.log(`βœ… [openModalWithData] split-panel-layoutμ—μ„œ μ†ŒμŠ€ ν…Œμ΄λΈ” 감지: ${sourceTableName}`); break; } } // split-panel-layout2 νƒ€μž… (μƒˆλ‘œμš΄ λΆ„ν•  νŒ¨λ„) if (compType === "split-panel-layout2") { sourceTableName = compConfig?.leftPanel?.tableName || compConfig?.tableName || compConfig?.leftTableName; if (sourceTableName) { console.log(`βœ… [openModalWithData] split-panel-layout2μ—μ„œ μ†ŒμŠ€ ν…Œμ΄λΈ” 감지: ${sourceTableName}`); break; } } // ν…Œμ΄λΈ” 리슀트 νƒ€μž… if (compType === "table-list") { sourceTableName = compConfig?.tableName; if (sourceTableName) { console.log(`βœ… [openModalWithData] table-listμ—μ„œ μ†ŒμŠ€ ν…Œμ΄λΈ” 감지: ${sourceTableName}`); break; } } // πŸ†• λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈμ—μ„œ tableName μ°ΎκΈ° (폴백) if (!sourceTableName && compConfig?.tableName) { sourceTableName = compConfig.tableName; console.log(`βœ… [openModalWithData] ${compType}μ—μ„œ μ†ŒμŠ€ ν…Œμ΄λΈ” 감지 (폴백): ${sourceTableName}`); break; } } // μ—¬μ „νžˆ μ—†μœΌλ©΄ currentTableName μ‚¬μš© (ν™”λ©΄ 레벨 ν…Œμ΄λΈ”λͺ…) if (!sourceTableName && currentTableName) { sourceTableName = currentTableName; console.log(`βœ… [openModalWithData] currentTableNameμ—μ„œ μ†ŒμŠ€ ν…Œμ΄λΈ” μ‚¬μš©: ${sourceTableName}`); } if (!sourceTableName) { console.warn("[openModalWithData] μ†ŒμŠ€ ν…Œμ΄λΈ”μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); } // μ†ŒμŠ€ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ if (sourceTableName) { try { const response = await apiClient.get(`/table-management/tables/${sourceTableName}/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 || col.column_name, label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name, })); setModalSourceColumns(columns); console.log(`βœ… [openModalWithData] μ†ŒμŠ€ ν…Œμ΄λΈ”(${sourceTableName}) 컬럼 λ‘œλ“œ μ™„λ£Œ:`, columns.length); } } } catch (error) { console.error("μ†ŒμŠ€ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } // νƒ€κ²Ÿ ν™”λ©΄μ˜ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ const targetScreenId = config.action?.targetScreenId; if (targetScreenId) { try { // νƒ€κ²Ÿ ν™”λ©΄ 정보 κ°€μ Έμ˜€κΈ° const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`); console.log("[openModalWithData] νƒ€κ²Ÿ ν™”λ©΄ 응닡:", screenResponse.data); if (screenResponse.data.success && screenResponse.data.data) { const targetTableName = screenResponse.data.data.tableName; console.log("[openModalWithData] νƒ€κ²Ÿ ν™”λ©΄ ν…Œμ΄λΈ”λͺ…:", targetTableName); if (targetTableName) { const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`); if (columnResponse.data.success) { let columnData = columnResponse.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 || col.column_name, label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name, })); setModalTargetColumns(columns); console.log(`βœ… [openModalWithData] νƒ€κ²Ÿ ν…Œμ΄λΈ”(${targetTableName}) 컬럼 λ‘œλ“œ μ™„λ£Œ:`, columns.length); } } } else { console.warn("[openModalWithData] νƒ€κ²Ÿ 화면에 ν…Œμ΄λΈ”λͺ…이 μ—†μŠ΅λ‹ˆλ‹€."); } } } catch (error) { console.error("νƒ€κ²Ÿ ν™”λ©΄ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } else { console.warn("[openModalWithData] νƒ€κ²Ÿ ν™”λ©΄ IDκ°€ μ—†μŠ΅λ‹ˆλ‹€."); } }; loadModalMappingColumns(); }, [config.action?.type, config.action?.targetScreenId, allComponents, currentTableName]); // ν™”λ©΄ λͺ©λ‘ κ°€μ Έμ˜€κΈ° (ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ κΈ°μ€€) 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 μ»΄ν¬λ„ŒνŠΈκ°€ μžˆλŠ” 화면을 μ„ νƒν•˜μ„Έμš”

{/* πŸ†• ν•„λ“œ λ§€ν•‘ μ„€μ • (μ†ŒμŠ€ 컬럼 β†’ νƒ€κ²Ÿ 컬럼) */}

μ†ŒμŠ€ ν…Œμ΄λΈ”μ˜ 컬럼λͺ…이 νƒ€κ²Ÿ ν™”λ©΄μ˜ μž…λ ₯ ν•„λ“œ 컬럼λͺ…κ³Ό λ‹€λ₯Ό λ•Œ 맀핑을 μ„€μ •ν•˜μ„Έμš”.
예: warehouse_code β†’ warehouse_id (λΆ„ν•  νŒ¨λ„μ˜ μ°½κ³ μ½”λ“œλ₯Ό λͺ¨λ‹¬μ˜ μ°½κ³ ID에 λ§€ν•‘)

{/* 컬럼 λ‘œλ“œ μƒνƒœ ν‘œμ‹œ */} {modalSourceColumns.length > 0 || modalTargetColumns.length > 0 ? (
μ†ŒμŠ€ 컬럼: {modalSourceColumns.length}개 / νƒ€κ²Ÿ 컬럼: {modalTargetColumns.length}개
) : (
λΆ„ν•  νŒ¨λ„ λ˜λŠ” ν…Œμ΄λΈ” μ»΄ν¬λ„ŒνŠΈμ™€ λŒ€μƒ 화면을 μ„€μ •ν•˜λ©΄ 컬럼 λͺ©λ‘μ΄ λ‘œλ“œλ©λ‹ˆλ‹€.
)} {(config.action?.fieldMappings || []).length === 0 ? (

맀핑이 μ—†μœΌλ©΄ 같은 μ΄λ¦„μ˜ 컬럼끼리 μžλ™μœΌλ‘œ λ§€ν•‘λ©λ‹ˆλ‹€.

) : (
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
{/* μ†ŒμŠ€ ν•„λ“œ 선택 (Combobox) - μ„Έλ‘œ 배치 */}
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))} > setModalSourceSearch((prev) => ({ ...prev, [index]: value }))} /> μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {modalSourceColumns.map((col) => ( { const mappings = [...(config.action?.fieldMappings || [])]; mappings[index] = { ...mappings[index], sourceField: col.name }; onUpdateProperty("componentConfig.action.fieldMappings", mappings); setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: false })); }} className="text-xs" > {col.label} {col.label !== col.name && ( ({col.name}) )} ))}
{/* ν™”μ‚΄ν‘œ ν‘œμ‹œ */}
↓
{/* νƒ€κ²Ÿ ν•„λ“œ 선택 (Combobox) - μ„Έλ‘œ 배치 */}
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))} > setModalTargetSearch((prev) => ({ ...prev, [index]: value }))} /> μ»¬λŸΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ {modalTargetColumns.map((col) => ( { const mappings = [...(config.action?.fieldMappings || [])]; mappings[index] = { ...mappings[index], targetField: col.name }; onUpdateProperty("componentConfig.action.fieldMappings", mappings); setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: false })); }} className="text-xs" > {col.label} {col.label !== col.name && ( ({col.name}) )} ))}
{/* μ‚­μ œ λ²„νŠΌ */}
))}
)}
)} {/* μˆ˜μ • μ•‘μ…˜ μ„€μ • */} {(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); }} />

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

)}
setGroupByColumnSearch(e.target.value)} className="border-0 p-0 focus-visible:ring-0" />
{currentTableColumns.length === 0 ? (
{currentTableName ? "μ»¬λŸΌμ„ λΆˆλŸ¬μ˜€λŠ” 쀑..." : "ν…Œμ΄λΈ”μ΄ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€"}
) : ( <> {/* 선택 ν•΄μ œ μ˜΅μ…˜ */}
{ setLocalInputs((prev) => ({ ...prev, groupByColumn: "" })); onUpdateProperty("componentConfig.action.groupByColumns", undefined); setGroupByColumnOpen(false); setGroupByColumnSearch(""); }} > 선택 μ•ˆ 함
{/* 컬럼 λͺ©λ‘ */} {currentTableColumns .filter((col) => { if (!groupByColumnSearch) return true; const search = groupByColumnSearch.toLowerCase(); return col.name.toLowerCase().includes(search) || col.label.toLowerCase().includes(search); }) .map((col) => (
{ setLocalInputs((prev) => ({ ...prev, groupByColumn: col.name })); onUpdateProperty("componentConfig.action.groupByColumns", [col.name]); setGroupByColumnOpen(false); setGroupByColumnSearch(""); }} >
{col.name} {col.label !== col.name && ( {col.label} )}
))} )}

μ—¬λŸ¬ 행을 ν•˜λ‚˜μ˜ 그룹으둜 λ¬Άμ–΄μ„œ μˆ˜μ •ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€

)} {/* 볡사 μ•‘μ…˜ μ„€μ • */} {(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" && ( )} {/* λ°”μ½”λ“œ μŠ€μΊ” μ•‘μ…˜ μ„€μ • */} {(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") === "empty_vehicle" && (
... 곡차등둝 μ„€μ • UI μƒλž΅ ...
)} */} {/* μš΄ν–‰μ•Œλ¦Ό 및 μ’…λ£Œ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "operation_control" && (

πŸš— μš΄ν–‰μ•Œλ¦Ό 및 μ’…λ£Œ μ„€μ •

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

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 μœ„μΉ˜λ₯Ό μˆ˜μ§‘ν•˜μ—¬ μœ„ ν•„λ“œμ— μ €μž₯ν•©λ‹ˆλ‹€.

)}
{/* πŸ†• 연속 μœ„μΉ˜ 좔적 μ„€μ • */}

10μ΄ˆλ§ˆλ‹€ μœ„μΉ˜λ₯Ό 경둜 ν…Œμ΄λΈ”μ— μ €μž₯ν•©λ‹ˆλ‹€

onUpdateProperty("componentConfig.action.updateWithTracking", checked)} />
{config.action?.updateWithTracking && (
{config.action?.updateTrackingMode === "start" && (
onUpdateProperty( "componentConfig.action.updateTrackingInterval", parseInt(e.target.value) * 1000 || 10000, ) } className="h-8 text-xs" min={5} max={300} />

5초 ~ 300초 μ‚¬μ΄λ‘œ μ„€μ • (κΈ°λ³Έ: 10초)

)}

{config.action?.updateTrackingMode === "start" ? "λ²„νŠΌ 클릭 μ‹œ 연속 μœ„μΉ˜ 좔적이 μ‹œμž‘λ˜κ³ , vehicle_location_history ν…Œμ΄λΈ”μ— κ²½λ‘œκ°€ μ €μž₯λ©λ‹ˆλ‹€." : "λ²„νŠΌ 클릭 μ‹œ μ§„ν–‰ 쀑인 μœ„μΉ˜ 좔적이 μ’…λ£Œλ©λ‹ˆλ‹€."}

)} {/* πŸ†• λ²„νŠΌ ν™œμ„±ν™” 쑰건 μ„€μ • */}
λ²„νŠΌ ν™œμ„±ν™” 쑰건
{/* μΆœλ°œμ§€/도착지 ν•„μˆ˜ 체크 */}

μ„ νƒν•˜μ§€ μ•ŠμœΌλ©΄ λ²„νŠΌ λΉ„ν™œμ„±ν™”

onUpdateProperty("componentConfig.action.requireLocationFields", checked)} />
{config.action?.requireLocationFields && (
onUpdateProperty("componentConfig.action.trackingDepartureField", e.target.value) } className="h-8 text-xs" />
onUpdateProperty("componentConfig.action.trackingArrivalField", e.target.value)} className="h-8 text-xs" />
)} {/* μƒνƒœ 기반 ν™œμ„±ν™” 쑰건 */}

νŠΉμ • μƒνƒœμΌ λ•Œλ§Œ λ²„νŠΌ ν™œμ„±ν™”

onUpdateProperty("componentConfig.action.enableOnStatusCheck", checked)} />
{config.action?.enableOnStatusCheck && (

μƒνƒœλ₯Ό μ‘°νšŒν•  ν…Œμ΄λΈ” (κΈ°λ³Έ: vehicles)

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

ν˜„μž¬ 둜그인 μ‚¬μš©μž ID둜 μ‘°νšŒν•  ν•„λ“œ (κΈ°λ³Έ: user_id)

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

μƒνƒœ 값이 μ €μž₯된 컬럼λͺ… (κΈ°λ³Έ: status)

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

μ—¬λŸ¬ μƒνƒœκ°’μ€ μ‰Όν‘œ(,)둜 ꡬ뢄

)}

μ‚¬μš© μ˜ˆμ‹œ:
- μš΄ν–‰ μ‹œμž‘: statusλ₯Ό "active"둜 + 연속 좔적 μ‹œμž‘
- μš΄ν–‰ μ’…λ£Œ: statusλ₯Ό "completed"둜 + 연속 좔적 μ’…λ£Œ
- 곡차등둝: statusλ₯Ό "inactive"둜 + 1νšŒμ„± μœ„μΉ˜μ •λ³΄ μˆ˜μ§‘

)} {/* 데이터 전달 μ•‘μ…˜ μ„€μ • */} {(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. 이 λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ λ§€ν•‘λœ 데이터가 νƒ€κ²ŸμœΌλ‘œ μ „λ‹¬λ©λ‹ˆλ‹€

)} {/* πŸ†• μ¦‰μ‹œ μ €μž₯(quickInsert) μ•‘μ…˜ μ„€μ • */} {component.componentConfig?.action?.type === "quickInsert" && ( )} {/* πŸ†• ν–‰ 선택 μ‹œμ—λ§Œ ν™œμ„±ν™” μ„€μ • */}

ν–‰ 선택 ν™œμ„±ν™” 쑰건

ν…Œμ΄λΈ” λ¦¬μŠ€νŠΈλ‚˜ λΆ„ν•  νŒ¨λ„μ—μ„œ 데이터가 μ„ νƒλ˜μ—ˆμ„ λ•Œλ§Œ λ²„νŠΌμ„ ν™œμ„±ν™”ν•©λ‹ˆλ‹€.

μ²΄ν¬ν•˜λ©΄ ν…Œμ΄λΈ”μ—μ„œ 행을 μ„ νƒν•΄μ•Όλ§Œ λ²„νŠΌμ΄ ν™œμ„±ν™”λ©λ‹ˆλ‹€.

{ onUpdateProperty("componentConfig.action.requireRowSelection", checked); }} />
{component.componentConfig?.action?.requireRowSelection && (

μžλ™ 감지: ν…Œμ΄λΈ”, λΆ„ν•  νŒ¨λ„, ν”Œλ‘œμš° μœ„μ ― 쀑 μ„ νƒλœ ν•­λͺ©μ΄ 있으면 ν™œμ„±ν™”

μ—¬λŸ¬ 행이 μ„ νƒλ˜μ–΄λ„ ν™œμ„±ν™” (κΈ°λ³Έ: 1개 이상 선택 μ‹œ ν™œμ„±ν™”)

{ onUpdateProperty("componentConfig.action.allowMultiRowSelection", checked); }} />
{!(component.componentConfig?.action?.allowMultiRowSelection ?? true) && (

μ •ν™•νžˆ 1개의 ν–‰λ§Œ μ„ νƒλ˜μ–΄μ•Ό λ²„νŠΌμ΄ ν™œμ„±ν™”λ©λ‹ˆλ‹€.

)}
)}
{/* μ œμ–΄ κΈ°λŠ₯ μ„Ήμ…˜ - μ—‘μ…€ μ—…λ‘œλ“œκ°€ 아닐 λ•Œλ§Œ ν‘œμ‹œ */} {(component.componentConfig?.action?.type || "save") !== "excel_upload" && (
)} {/* πŸ†• ν”Œλ‘œμš° 단계별 ν‘œμ‹œ μ œμ–΄ μ„Ήμ…˜ (ν”Œλ‘œμš° μœ„μ ―μ΄ μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ) */} {hasFlowWidget && (
)}
); }; /** * λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ • μ»΄ν¬λ„ŒνŠΈ * λΆ„ν•  νŒ¨λ„ + column_labelsμ—μ„œ 관계λ₯Ό μžλ™ κ°μ§€ν•˜κ³ , μ‚¬μš©μžλŠ” μ±„λ²ˆ κ·œμΉ™λ§Œ 선택 */ const MasterDetailExcelUploadConfig: React.FC<{ config: any; onUpdateProperty: (path: string, value: any) => void; allComponents: ComponentData[]; }> = ({ config, onUpdateProperty, allComponents }) => { const [relationInfo, setRelationInfo] = useState<{ masterTable: string; detailTable: string; masterKeyColumn: string; detailFkColumn: string; } | null>(null); const [loading, setLoading] = useState(false); const [masterColumns, setMasterColumns] = useState< Array<{ columnName: string; columnLabel: string; inputType: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; }> >([]); // μ°Έμ‘° ν…Œμ΄λΈ”λ³„ 컬럼 λͺ©λ‘ μΊμ‹œ (컬럼λͺ… + 라벨) const [refTableColumns, setRefTableColumns] = useState>>({}); // λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ„€μ • const masterDetailConfig = config.action?.masterDetailExcel || {}; // λΆ„ν•  νŒ¨λ„μ—μ„œ λ§ˆμŠ€ν„°/λ””ν…ŒμΌ ν…Œμ΄λΈ”λͺ… μžλ™ 감지 const splitPanelInfo = useMemo(() => { const findSplitPanel = (components: any[]): any => { for (const comp of components) { const compId = comp.componentId || comp.componentType; if (compId === "split-panel-layout") { return comp.componentConfig; } if (comp.children && comp.children.length > 0) { const found = findSplitPanel(comp.children); if (found) return found; } } return null; }; return findSplitPanel(allComponents as any[]); }, [allComponents]); const masterTable = splitPanelInfo?.leftPanel?.tableName || ""; const detailTable = splitPanelInfo?.rightPanel?.tableName || ""; // λ§ˆμŠ€ν„° ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ useEffect(() => { if (!masterTable) { setMasterColumns([]); return; } const loadMasterColumns = async () => { try { const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get(`/table-management/tables/${masterTable}/columns`); if (response.data?.success && response.data?.data?.columns) { const cols = response.data.data.columns.map((col: any) => ({ columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, inputType: col.inputType || col.input_type || "text", referenceTable: col.referenceTable || col.reference_table, referenceColumn: col.referenceColumn || col.reference_column, displayColumn: col.displayColumn || col.display_column, })); setMasterColumns(cols); } } catch (error) { console.error("λ§ˆμŠ€ν„° ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } }; loadMasterColumns(); }, [masterTable]); // μ„ νƒλœ μ—”ν‹°ν‹° ν•„λ“œλ“€μ˜ μ°Έμ‘° ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ useEffect(() => { const entityFields = (masterDetailConfig.masterSelectFields || []).filter( (f: any) => f.inputType === "entity" && f.referenceTable, ); const loadRefTableColumns = async () => { const { apiClient } = await import("@/lib/api/client"); for (const field of entityFields) { // 이미 λ‘œλ“œλœ ν…Œμ΄λΈ”μ€ μŠ€ν‚΅ if (refTableColumns[field.referenceTable]) continue; try { const response = await apiClient.get(`/table-management/tables/${field.referenceTable}/columns`); if (response.data?.success && response.data?.data?.columns) { const cols = response.data.data.columns.map((c: any) => ({ name: c.columnName || c.column_name, label: c.displayName || c.columnLabel || c.column_label || c.columnName || c.column_name, })); setRefTableColumns((prev) => ({ ...prev, [field.referenceTable]: cols, })); } } catch (error) { console.error("μ°Έμ‘° ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", field.referenceTable, error); } } }; if (entityFields.length > 0) { loadRefTableColumns(); } }, [masterDetailConfig.masterSelectFields, refTableColumns]); // column_labelsμ—μ„œ FK 관계 μžλ™ 감지 useEffect(() => { if (!masterTable || !detailTable) { setRelationInfo(null); return; } const loadRelationInfo = async () => { setLoading(true); try { const { apiClient } = await import("@/lib/api/client"); // λ””ν…ŒμΌ ν…Œμ΄λΈ”μ˜ 컬럼 정보 쑰회 (referenceTable, referenceColumn 포함) const response = await apiClient.get(`/table-management/tables/${detailTable}/columns`); if (response.data?.success && response.data?.data?.columns) { const columns = response.data.data.columns; // referenceTable이 λ§ˆμŠ€ν„° ν…Œμ΄λΈ”μΈ 컬럼 μ°ΎκΈ° const fkColumn = columns.find((col: any) => col.referenceTable === masterTable); if (fkColumn) { const detailFk = fkColumn.columnName || fkColumn.column_name; const masterKey = fkColumn.referenceColumn || fkColumn.reference_column; setRelationInfo({ masterTable, detailTable, masterKeyColumn: masterKey, detailFkColumn: detailFk, }); // 섀정에 μžλ™μœΌλ‘œ μ €μž₯ onUpdateProperty("componentConfig.action.masterDetailExcel", { ...masterDetailConfig, masterTable, detailTable, masterKeyColumn: masterKey, detailFkColumn: detailFk, }); } else { setRelationInfo(null); } } } catch (error) { console.error("FK 관계 λ‘œλ“œ μ‹€νŒ¨:", error); setRelationInfo(null); } finally { setLoading(false); } }; loadRelationInfo(); }, [masterTable, detailTable]); const updateMasterDetailConfig = (updates: Record) => { onUpdateProperty("componentConfig.action.masterDetailExcel", { ...masterDetailConfig, ...updates, }); }; // λΆ„ν•  νŒ¨λ„μ΄ μ—†μœΌλ©΄ ν‘œμ‹œν•˜μ§€ μ•ŠμŒ if (!splitPanelInfo) { return (

이 화면에 λΆ„ν•  νŒ¨λ„μ΄ μ—†μŠ΅λ‹ˆλ‹€. λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—…λ‘œλ“œλŠ” λΆ„ν•  νŒ¨λ„ ν™”λ©΄μ—μ„œλ§Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

); } return (
λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ„€μ • (μžλ™ 감지)
{/* μžλ™ κ°μ§€λœ 정보 ν‘œμ‹œ */}

λΆ„ν•  νŒ¨λ„μ—μ„œ κ°μ§€λœ 정보:

λ§ˆμŠ€ν„°:{" "} {masterTable || "-"}
λ””ν…ŒμΌ:{" "} {detailTable || "-"}
{loading ? (

FK 관계 쑰회 쀑...

) : relationInfo ? (
λ§ˆμŠ€ν„° ν‚€:{" "} {relationInfo.masterKeyColumn}
λ””ν…ŒμΌ FK:{" "} {relationInfo.detailFkColumn}
) : (

FK 관계λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. ν…Œμ΄λΈ” νƒ€μž…κ΄€λ¦¬μ—μ„œ reference_table을 μ„€μ •ν•΄μ£Όμ„Έμš”.

)}
{/* λ§ˆμŠ€ν„° ν‚€ μžλ™ 생성 μ•ˆλ‚΄ */} {relationInfo && (

λ§ˆμŠ€ν„° ν…Œμ΄λΈ”μ˜ {relationInfo.masterKeyColumn} 값은 μœ„μ—μ„œ μ„€μ •ν•œ μ±„λ²ˆ κ·œμΉ™μœΌλ‘œ μžλ™ μƒμ„±λ©λ‹ˆλ‹€.

)} {/* λ§ˆμŠ€ν„° ν•„λ“œ 선택 - μ‚¬μš©μžκ°€ μ—‘μ…€ μ—…λ‘œλ“œ μ‹œ μž…λ ₯ν•  ν•„λ“œ */} {relationInfo && masterColumns.length > 0 && (

μ—‘μ…€ μ—…λ‘œλ“œ μ‹œ μ‚¬μš©μžκ°€ 직접 선택/μž…λ ₯ν•  λ§ˆμŠ€ν„° ν…Œμ΄λΈ” ν•„λ“œλ₯Ό μ„ νƒν•˜μ„Έμš”.

{masterColumns .filter((col) => col.columnName !== relationInfo.masterKeyColumn) // μ±„λ²ˆμœΌλ‘œ μžλ™ μƒμ„±λ˜λŠ” ν‚€λŠ” μ œμ™Έ .map((col) => { const selectedFields = masterDetailConfig.masterSelectFields || []; const isSelected = selectedFields.some((f: any) => f.columnName === col.columnName); return (
{ const checked = e.target.checked; let newFields = [...selectedFields]; if (checked) { newFields.push({ columnName: col.columnName, columnLabel: col.columnLabel, inputType: col.inputType, referenceTable: col.referenceTable, referenceColumn: col.referenceColumn, displayColumn: col.displayColumn, required: true, }); } else { newFields = newFields.filter((f: any) => f.columnName !== col.columnName); } updateMasterDetailConfig({ masterSelectFields: newFields }); }} className="h-4 w-4 rounded border-gray-300" />
); })}
{(masterDetailConfig.masterSelectFields?.length || 0) > 0 && (

μ„ νƒλœ ν•„λ“œ: {masterDetailConfig.masterSelectFields.length}개

)} {/* μ—”ν‹°ν‹° ν•„λ“œμ˜ ν‘œμ‹œμ»¬λŸΌ μ„€μ • */} {masterDetailConfig.masterSelectFields?.filter((f: any) => f.inputType === "entity").length > 0 && (
{masterDetailConfig.masterSelectFields .filter((f: any) => f.inputType === "entity") .map((field: any) => { const availableColumns = refTableColumns[field.referenceTable] || []; return (
{field.columnLabel}:
); })}

μ°Έμ‘° ν…Œμ΄λΈ”μ—μ„œ μ‚¬μš©μžμ—κ²Œ ν‘œμ‹œν•  μ»¬λŸΌμ„ μ„ νƒν•˜μ„Έμš”.

)}
)}
); }; /** * μ—‘μ…€ μ—…λ‘œλ“œ μ±„λ²ˆ κ·œμΉ™ μ„€μ • (단일 ν…Œμ΄λΈ”/λ§ˆμŠ€ν„°-λ””ν…ŒμΌ λͺ¨λ‘ μ‚¬μš© κ°€λŠ₯) */ const ExcelNumberingRuleConfig: React.FC<{ config: { numberingRuleId?: string; numberingTargetColumn?: string }; updateConfig: (updates: { numberingRuleId?: string; numberingTargetColumn?: string }) => void; tableName?: string; // 단일 ν…Œμ΄λΈ”μΈ 경우 ν…Œμ΄λΈ”λͺ… hasSplitPanel?: boolean; // λΆ„ν•  νŒ¨λ„ μ—¬λΆ€ (λ§ˆμŠ€ν„°-λ””ν…ŒμΌ) }> = ({ config, updateConfig, tableName, hasSplitPanel }) => { const [numberingRules, setNumberingRules] = useState([]); const [ruleSelectOpen, setRuleSelectOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [tableColumns, setTableColumns] = useState>([]); const [columnsLoading, setColumnsLoading] = useState(false); // μ±„λ²ˆ κ·œμΉ™ λͺ©λ‘ λ‘œλ“œ useEffect(() => { const loadNumberingRules = async () => { setIsLoading(true); try { const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get("/numbering-rules"); if (response.data?.success && response.data?.data) { setNumberingRules(response.data.data); } } catch (error) { console.error("μ±„λ²ˆ κ·œμΉ™ λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); } finally { setIsLoading(false); } }; loadNumberingRules(); }, []); // 단일 ν…Œμ΄λΈ”μΈ 경우 컬럼 λͺ©λ‘ λ‘œλ“œ useEffect(() => { if (!tableName || hasSplitPanel) { setTableColumns([]); return; } const loadColumns = async () => { setColumnsLoading(true); try { const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); if (response.data?.success && response.data?.data?.columns) { const cols = response.data.data.columns.map((col: any) => ({ columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, })); setTableColumns(cols); } } catch (error) { console.error("컬럼 λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); } finally { setColumnsLoading(false); } }; loadColumns(); }, [tableName, hasSplitPanel]); const selectedRule = numberingRules.find((r) => String(r.rule_id || r.ruleId) === String(config.numberingRuleId)); return (

μ—…λ‘œλ“œ μ‹œ μžλ™μœΌλ‘œ 생성할 μ½”λ“œ/번호의 μ±„λ²ˆ κ·œμΉ™μ„ μ„ νƒν•˜μ„Έμš”.

검색 κ²°κ³Ό μ—†μŒ { updateConfig({ numberingRuleId: undefined, numberingTargetColumn: undefined }); setRuleSelectOpen(false); }} className="text-xs" > μ±„λ²ˆ μ—†μŒ {numberingRules.map((rule, idx) => { const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`); const ruleName = rule.rule_name || rule.ruleName || "(이름 μ—†μŒ)"; return ( { updateConfig({ numberingRuleId: ruleId }); setRuleSelectOpen(false); }} className="text-xs" > {ruleName} ); })} {/* 단일 ν…Œμ΄λΈ”μ΄κ³  μ±„λ²ˆ κ·œμΉ™μ΄ μ„ νƒλœ 경우, μ μš©ν•  컬럼 선택 */} {config.numberingRuleId && !hasSplitPanel && tableName && (

μ±„λ²ˆ 값이 μž…λ ₯될 μ»¬λŸΌμ„ μ„ νƒν•˜μ„Έμš”.

)} {/* λΆ„ν•  νŒ¨λ„μΈ 경우 μ•ˆλ‚΄ λ©”μ‹œμ§€ */} {config.numberingRuleId && hasSplitPanel && (

λ§ˆμŠ€ν„°-λ””ν…ŒμΌ κ΅¬μ‘°μ—μ„œλŠ” λ§ˆμŠ€ν„° ν‚€ μ»¬λŸΌμ— μžλ™ μ μš©λ©λ‹ˆλ‹€.

)}
); }; /** * μ—‘μ…€ μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • (단일 ν…Œμ΄λΈ”/λ§ˆμŠ€ν„°-λ””ν…ŒμΌ λͺ¨λ‘ μ‚¬μš© κ°€λŠ₯) */ const ExcelAfterUploadControlConfig: React.FC<{ config: { afterUploadFlows?: Array<{ flowId: string; order: number }> }; updateConfig: (updates: { afterUploadFlows?: Array<{ flowId: string; order: number }> }) => void; }> = ({ config, updateConfig }) => { const [nodeFlows, setNodeFlows] = useState>([]); const [flowSelectOpen, setFlowSelectOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const selectedFlows = config.afterUploadFlows || []; // λ…Έλ“œ ν”Œλ‘œμš° λͺ©λ‘ λ‘œλ“œ useEffect(() => { const loadNodeFlows = async () => { setIsLoading(true); try { const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get("/dataflow/node-flows"); if (response.data?.success && response.data?.data) { setNodeFlows(response.data.data); } } catch (error) { console.error("λ…Έλ“œ ν”Œλ‘œμš° λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); } finally { setIsLoading(false); } }; loadNodeFlows(); }, []); const addFlow = (flowId: string) => { if (selectedFlows.some((f) => f.flowId === flowId)) return; const newFlows = [...selectedFlows, { flowId, order: selectedFlows.length + 1 }]; updateConfig({ afterUploadFlows: newFlows }); setFlowSelectOpen(false); }; const removeFlow = (flowId: string) => { const newFlows = selectedFlows.filter((f) => f.flowId !== flowId).map((f, idx) => ({ ...f, order: idx + 1 })); updateConfig({ afterUploadFlows: newFlows }); }; const moveUp = (index: number) => { if (index === 0) return; const newFlows = [...selectedFlows]; [newFlows[index - 1], newFlows[index]] = [newFlows[index], newFlows[index - 1]]; updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) }); }; const moveDown = (index: number) => { if (index === selectedFlows.length - 1) return; const newFlows = [...selectedFlows]; [newFlows[index], newFlows[index + 1]] = [newFlows[index + 1], newFlows[index]]; updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) }); }; const availableFlows = nodeFlows.filter((f) => !selectedFlows.some((s) => s.flowId === String(f.flowId))); return (

μ—‘μ…€ μ—…λ‘œλ“œ μ™„λ£Œ ν›„ μˆœμ„œλŒ€λ‘œ μ‹€ν–‰ν•  μ œμ–΄λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

{selectedFlows.length > 0 && (
{selectedFlows.map((selected, index) => { const flow = nodeFlows.find((f) => String(f.flowId) === selected.flowId); return (
{index + 1} {flow?.flowName || `Flow ${selected.flowId}`}
); })}
)} 검색 κ²°κ³Ό μ—†μŒ {availableFlows.map((flow) => ( addFlow(String(flow.flowId))} className="text-xs" >
{flow.flowName} {flow.flowDescription && ( {flow.flowDescription} )}
))}
{selectedFlows.length > 0 && (

μ—…λ‘œλ“œ μ™„λ£Œ ν›„ μœ„ μˆœμ„œλŒ€λ‘œ {selectedFlows.length}개의 μ œμ–΄κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€.

)}
); }; /** * μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ • μ„Ήμ…˜ μ»΄ν¬λ„ŒνŠΈ * λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 섀정은 λΆ„ν•  νŒ¨λ„ μžλ™ 감지 */ const ExcelUploadConfigSection: React.FC<{ config: any; onUpdateProperty: (path: string, value: any) => void; allComponents: ComponentData[]; currentTableName?: string; // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… (ButtonConfigPanelμ—μ„œ 전달) }> = ({ config, onUpdateProperty, allComponents, currentTableName: propTableName }) => { // μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ • μƒνƒœ 관리 const [excelUploadConfig, setExcelUploadConfig] = useState<{ numberingRuleId?: string; numberingTargetColumn?: string; afterUploadFlows?: Array<{ flowId: string; order: number }>; }>({ numberingRuleId: config.action?.excelNumberingRuleId, numberingTargetColumn: config.action?.excelNumberingTargetColumn, afterUploadFlows: config.action?.excelAfterUploadFlows || [], }); // λΆ„ν•  νŒ¨λ„ 감지 const splitPanelInfo = useMemo(() => { const findSplitPanel = (components: any[]): any => { for (const comp of components) { const compId = comp.componentId || comp.componentType; if (compId === "split-panel-layout") { return comp.componentConfig; } if (comp.children && comp.children.length > 0) { const found = findSplitPanel(comp.children); if (found) return found; } } return null; }; return findSplitPanel(allComponents as any[]); }, [allComponents]); const hasSplitPanel = !!splitPanelInfo; // 단일 ν…Œμ΄λΈ” 감지 (props μš°μ„ , μ—†μœΌλ©΄ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 탐색) const singleTableName = useMemo(() => { if (hasSplitPanel) return undefined; // props둜 μ „λ‹¬λœ ν…Œμ΄λΈ”λͺ… μš°μ„  μ‚¬μš© if (propTableName) return propTableName; // μ»΄ν¬λ„ŒνŠΈμ—μ„œ ν…Œμ΄λΈ”λͺ… 탐색 const findTableName = (components: any[]): string | undefined => { for (const comp of components) { const compId = comp.componentId || comp.componentType; const compConfig = comp.componentConfig || comp.config || comp; // ν…Œμ΄λΈ” νŒ¨λ„μ΄λ‚˜ 데이터 ν…Œμ΄λΈ”μ—μ„œ ν…Œμ΄λΈ”λͺ… μ°ΎκΈ° if ( compId === "table-panel" || compId === "data-table" || compId === "table-list" || compId === "simple-table" ) { const tableName = compConfig?.tableName || compConfig?.table; if (tableName) return tableName; } // 폼 μ»΄ν¬λ„ŒνŠΈμ—μ„œ ν…Œμ΄λΈ”λͺ… μ°ΎκΈ° if (compId === "form-panel" || compId === "input-form" || compId === "form" || compId === "detail-form") { const tableName = compConfig?.tableName || compConfig?.table; if (tableName) return tableName; } // λ²”μš©μ μœΌλ‘œ tableName 속성이 μžˆλŠ” μ»΄ν¬λ„ŒνŠΈ μ°ΎκΈ° if (compConfig?.tableName) { return compConfig.tableName; } if (comp.children && comp.children.length > 0) { const found = findTableName(comp.children); if (found) return found; } } return undefined; }; return findTableName(allComponents as any[]); }, [allComponents, hasSplitPanel, propTableName]); // 디버깅: κ°μ§€λœ ν…Œμ΄λΈ”λͺ… 둜그 useEffect(() => { console.log( "[ExcelUploadConfigSection] λΆ„ν•  νŒ¨λ„:", hasSplitPanel, "단일 ν…Œμ΄λΈ”:", singleTableName, "(props:", propTableName, ")", ); }, [hasSplitPanel, singleTableName, propTableName]); // μ„€μ • μ—…λ°μ΄νŠΈ ν•¨μˆ˜ const updateExcelUploadConfig = (updates: Partial) => { const newConfig = { ...excelUploadConfig, ...updates }; setExcelUploadConfig(newConfig); if (updates.numberingRuleId !== undefined) { onUpdateProperty("componentConfig.action.excelNumberingRuleId", updates.numberingRuleId); } if (updates.numberingTargetColumn !== undefined) { onUpdateProperty("componentConfig.action.excelNumberingTargetColumn", updates.numberingTargetColumn); } if (updates.afterUploadFlows !== undefined) { onUpdateProperty("componentConfig.action.excelAfterUploadFlows", updates.afterUploadFlows); } }; // config λ³€κ²½ μ‹œ 둜컬 μƒνƒœ 동기화 useEffect(() => { setExcelUploadConfig({ numberingRuleId: config.action?.excelNumberingRuleId, numberingTargetColumn: config.action?.excelNumberingTargetColumn, afterUploadFlows: config.action?.excelAfterUploadFlows || [], }); }, [ config.action?.excelNumberingRuleId, config.action?.excelNumberingTargetColumn, config.action?.excelAfterUploadFlows, ]); return (

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

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

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

)} {/* μ±„λ²ˆ κ·œμΉ™ μ„€μ • (항상 ν‘œμ‹œ) */} {/* μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (항상 ν‘œμ‹œ) */} {/* λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ„€μ • (λΆ„ν•  νŒ¨λ„ μžλ™ 감지) */}
); };