"use client"; import React, { useState, useEffect, useMemo, useCallback } 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 { Checkbox } from "@/components/ui/checkbox"; 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, Info, RotateCcw } 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"; import DOMPurify from "isomorphic-dompurify"; import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent"; import { icons as allLucideIcons } from "lucide-react"; import { actionIconMap, noIconActions, NO_ICON_MESSAGE, iconSizePresets, getLucideIcon, addToIconMap, getDefaultIconForAction, } from "@/lib/button-icon-map"; // πŸ†• 제λͺ© 블둝 νƒ€μž… 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κ°€ μ—†λŠ” 경우 λ°©μ–΄ 처리 if (!component) { return
μ»΄ν¬λ„ŒνŠΈ 정보λ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.
; } // πŸ”§ componentμ—μ„œ 직접 읽기 (useMemo 제거) const config = component.componentConfig || {}; const currentAction = component.componentConfig?.action || {}; // 둜컬 μƒνƒœ 관리 (μ‹€μ‹œκ°„ μž…λ ₯ 반영) const [localInputs, setLocalInputs] = useState({ text: config.text !== undefined ? config.text : "λ²„νŠΌ", actionType: String(config.action?.type || "save"), 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 [displayMode, setDisplayMode] = useState<"text" | "icon" | "icon-text">( config.displayMode || "text", ); const [selectedIcon, setSelectedIcon] = useState(config.icon?.name || ""); const [selectedIconType, setSelectedIconType] = useState<"lucide" | "svg">( config.icon?.type || "lucide", ); const [iconSize, setIconSize] = useState(config.icon?.size || "보톡"); const [iconColor, setIconColor] = useState(config.icon?.color || ""); const [iconGap, setIconGap] = useState(config.iconGap ?? 6); const [iconTextPosition, setIconTextPosition] = useState<"right" | "left" | "bottom" | "top">( config.iconTextPosition || "right", ); // μ»€μŠ€ν…€ μ•„μ΄μ½˜ UI μƒνƒœ const [lucideSearchOpen, setLucideSearchOpen] = useState(false); const [lucideSearchTerm, setLucideSearchTerm] = useState(""); const [svgPasteOpen, setSvgPasteOpen] = useState(false); const [svgInput, setSvgInput] = useState(""); const [svgName, setSvgName] = useState(""); const [svgError, setSvgError] = useState(""); 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 [mappingSourceColumnsMap, setMappingSourceColumnsMap] = useState>>({}); const [mappingTargetColumns, setMappingTargetColumns] = useState>([]); const [mappingSourcePopoverOpen, setMappingSourcePopoverOpen] = useState>({}); const [mappingTargetPopoverOpen, setMappingTargetPopoverOpen] = useState>({}); const [mappingSourceSearch, setMappingSourceSearch] = useState>({}); const [mappingTargetSearch, setMappingTargetSearch] = useState>({}); const [activeMappingGroupIndex, setActiveMappingGroupIndex] = useState(0); // πŸ†• 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>({}); // πŸ†• modal μ•‘μ…˜μš© ν•„λ“œ λ§€ν•‘ μƒνƒœ const [modalActionSourceTable, setModalActionSourceTable] = useState(null); const [modalActionTargetTable, setModalActionTargetTable] = useState(null); const [modalActionSourceColumns, setModalActionSourceColumns] = useState>([]); const [modalActionTargetColumns, setModalActionTargetColumns] = useState>([]); const [modalActionFieldMappings, setModalActionFieldMappings] = useState< Array<{ sourceField: string; targetField: string }> >([]); const [modalFieldMappingSourceOpen, setModalFieldMappingSourceOpen] = useState>({}); const [modalFieldMappingTargetOpen, setModalFieldMappingTargetOpen] = useState>({}); const [modalFieldMappingSourceSearch, setModalFieldMappingSourceSearch] = useState>({}); const [modalFieldMappingTargetSearch, setModalFieldMappingTargetSearch] = 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"); return isFlow; }); return found; }, [allComponents]); // μ»΄ν¬λ„ŒνŠΈ prop λ³€κ²½ μ‹œ 둜컬 μƒνƒœ 동기화 (Input만) useEffect(() => { const latestConfig = component.componentConfig || {}; const latestAction = latestConfig.action || {}; setLocalInputs({ text: latestConfig.text !== undefined ? latestConfig.text : "λ²„νŠΌ", actionType: String(latestAction.type || "save"), 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, component.componentConfig?.action?.type]); // πŸ†• 제λͺ© 블둝 ν•Έλ“€λŸ¬ 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); } } 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); } }; // λ©€ν‹° ν…Œμ΄λΈ” λ§€ν•‘: μ†ŒμŠ€/νƒ€κ²Ÿ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ const loadMappingColumns = useCallback(async (tableName: string): Promise> => { try { const response = await apiClient.get(`/table-management/tables/${tableName}/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)) { return columnData.map((col: any) => ({ name: col.name || col.columnName, label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, })); } } } catch (error) { console.error(`ν…Œμ΄λΈ” ${tableName} 컬럼 λ‘œλ“œ μ‹€νŒ¨:`, error); } return []; }, []); useEffect(() => { const multiTableMappings: Array<{ sourceTable: string }> = config.action?.dataTransfer?.multiTableMappings || []; const legacySourceTable = config.action?.dataTransfer?.sourceTable; const targetTable = config.action?.dataTransfer?.targetTable; const loadAll = async () => { const sourceTableNames = multiTableMappings.map((m) => m.sourceTable).filter(Boolean); if (legacySourceTable && !sourceTableNames.includes(legacySourceTable)) { sourceTableNames.push(legacySourceTable); } const newMap: Record> = {}; for (const tbl of sourceTableNames) { if (!mappingSourceColumnsMap[tbl]) { newMap[tbl] = await loadMappingColumns(tbl); } } if (Object.keys(newMap).length > 0) { setMappingSourceColumnsMap((prev) => ({ ...prev, ...newMap })); } if (targetTable && mappingTargetColumns.length === 0) { const cols = await loadMappingColumns(targetTable); setMappingTargetColumns(cols); } }; loadAll(); }, [config.action?.dataTransfer?.multiTableMappings, config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable, loadMappingColumns]); // πŸ†• modal μ•‘μ…˜: λŒ€μƒ ν™”λ©΄ ν…Œμ΄λΈ” 쑰회 및 ν•„λ“œ λ§€ν•‘ λ‘œλ“œ useEffect(() => { const actionType = config.action?.type; if (actionType !== "modal") return; const autoDetect = config.action?.autoDetectDataSource; if (!autoDetect) { // 데이터 전달이 λΉ„ν™œμ„±ν™”λ˜λ©΄ μƒνƒœ μ΄ˆκΈ°ν™” setModalActionSourceTable(null); setModalActionTargetTable(null); setModalActionSourceColumns([]); setModalActionTargetColumns([]); return; } const targetScreenId = config.action?.targetScreenId; if (!targetScreenId) return; const loadModalActionMappingData = async () => { // 1. μ†ŒμŠ€ ν…Œμ΄λΈ” 감지 (ν˜„μž¬ ν™”λ©΄) let sourceTableName: string | null = currentTableName || null; // allComponentsμ—μ„œ λΆ„ν• νŒ¨λ„/ν…Œμ΄λΈ”λ¦¬μŠ€νŠΈ/톡합λͺ©λ‘ 감지 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.tableName || null; if (sourceTableName) break; } if (compType === "table-list") { sourceTableName = compConfig.tableName || compConfig.selectedTable || null; if (sourceTableName) break; } if (compType === "v2-list") { sourceTableName = compConfig.dataSource?.table || compConfig.tableName || null; if (sourceTableName) break; } } setModalActionSourceTable(sourceTableName); // 2. λŒ€μƒ ν™”λ©΄μ˜ ν…Œμ΄λΈ” 쑰회 let targetTableName: string | null = null; try { const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`); if (screenResponse.data.success && screenResponse.data.data) { targetTableName = screenResponse.data.data.tableName || null; } else if (screenResponse.data?.tableName) { // 직접 데이터 λ°˜ν™˜ ν˜•μ‹μΈ 경우 targetTableName = screenResponse.data.tableName || null; } } catch (error) { console.error("λŒ€μƒ ν™”λ©΄ 정보 λ‘œλ“œ μ‹€νŒ¨:", error); } setModalActionTargetTable(targetTableName); // 3. μ†ŒμŠ€ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ 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, label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, })); setModalActionSourceColumns(columns); } } } catch (error) { console.error("μ†ŒμŠ€ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } // 4. λŒ€μƒ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ if (targetTableName) { try { const response = await apiClient.get(`/table-management/tables/${targetTableName}/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, })); setModalActionTargetColumns(columns); } } } catch (error) { console.error("λŒ€μƒ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); } } // 5. κΈ°μ‘΄ ν•„λ“œ λ§€ν•‘ λ‘œλ“œ λ˜λŠ” μžλ™ λ§€ν•‘ 생성 const existingMappings = config.action?.fieldMappings || []; if (existingMappings.length > 0) { setModalActionFieldMappings(existingMappings); } else if (sourceTableName && targetTableName && sourceTableName === targetTableName) { // ν…Œμ΄λΈ”μ΄ κ°™μœΌλ©΄ μžλ™ λ§€ν•‘ (동일 컬럼λͺ…) setModalActionFieldMappings([]); // 빈 λ°°μ—΄ = μžλ™ λ§€ν•‘ } }; loadModalActionMappingData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ config.action?.type, config.action?.autoDetectDataSource, config.action?.targetScreenId, currentTableName, allComponents, ]); // πŸ†• ν˜„μž¬ ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ (κ·Έλ£Ήν™” 컬럼 μ„ νƒμš©) 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); } } } 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())), ); }; // μ•„μ΄μ½˜ 선택 ν•Έλ“€λŸ¬ const handleSelectIcon = (iconName: string, iconType: "lucide" | "svg" = "lucide") => { setSelectedIcon(iconName); setSelectedIconType(iconType); onUpdateProperty("componentConfig.icon", { name: iconName, type: iconType, size: iconSize, ...(iconColor ? { color: iconColor } : {}), }); }; // 선택 쀑인 μ•„μ΄μ½˜μ΄ μ‚­μ œλ˜μ—ˆμ„ λ•Œ λ””ν΄νŠΈ μ•„μ΄μ½˜μœΌλ‘œ 볡귀 const revertToDefaultIcon = () => { const def = getDefaultIconForAction(localInputs.actionType); setSelectedIcon(def.name); setSelectedIconType(def.type); handleSelectIcon(def.name, def.type); }; // ν‘œμ‹œ λͺ¨λ“œ λ³€κ²½ ν•Έλ“€λŸ¬ β€” icon/icon-text μ „ν™˜ μ‹œ μ•„μ΄μ½˜ 미선택이면 λ””ν΄νŠΈ λΆ€μ—¬ const handleDisplayModeChange = (mode: "text" | "icon" | "icon-text") => { setDisplayMode(mode); onUpdateProperty("componentConfig.displayMode", mode); if ((mode === "icon" || mode === "icon-text") && !selectedIcon) { revertToDefaultIcon(); } }; // μ•„μ΄μ½˜ 크기 프리셋 λ³€κ²½ (μ•„μ΄μ½˜ 미선택 μ‹œ 둜컬만 μ—…λ°μ΄νŠΈ) const handleIconSizePreset = (preset: string) => { setIconSize(preset); if (selectedIcon) { onUpdateProperty("componentConfig.icon.size", preset); } }; // μ•„μ΄μ½˜ 색상 λ³€κ²½ const handleIconColorChange = (color: string | undefined) => { const val = color || ""; setIconColor(val); if (selectedIcon) { if (val) { onUpdateProperty("componentConfig.icon.color", val); } else { onUpdateProperty("componentConfig.icon.color", undefined); } } }; // ν˜„μž¬ μ•‘μ…˜μ˜ μΆ”μ²œ μ•„μ΄μ½˜ λͺ©λ‘ const currentActionIcons = actionIconMap[localInputs.actionType] || []; const isNoIconAction = noIconActions.has(localInputs.actionType); const customIcons: string[] = config.customIcons || []; const customSvgIcons: Array<{ name: string; svg: string }> = config.customSvgIcons || []; const showIconSettings = displayMode === "icon" || displayMode === "icon-text"; return (
{/* ν‘œμ‹œ λͺ¨λ“œ 선택 */}
{( [ { value: "text", label: "ν…μŠ€νŠΈ" }, { value: "icon", label: "μ•„μ΄μ½˜" }, { value: "icon-text", label: "μ•„μ΄μ½˜+ν…μŠ€νŠΈ" }, ] as const ).map((opt) => ( ))}
{/* μ•„μ΄μ½˜ λͺ¨λ“œ λ ˆμ΄μ•„μ›ƒ μ•ˆλ‚΄ */} {displayMode === "icon" && (
μ•„μ΄μ½˜λ§Œ ν‘œμ‹œν•  λ•ŒλŠ” λ²„νŠΌ μ˜μ—­μ˜ κ°€λ‘œ 폭을 쀄여 μ •μ‚¬κ°ν˜•μ— κ°€κΉκ²Œ λ§Œλ“€λ©΄ 더 κΉ”λ”ν•©λ‹ˆλ‹€.
)} {/* λ²„νŠΌ ν…μŠ€νŠΈ (ν…μŠ€νŠΈ / μ•„μ΄μ½˜+ν…μŠ€νŠΈ λͺ¨λ“œμ—μ„œ ν‘œμ‹œ) */} {(displayMode === "text" || displayMode === "icon-text") && (
{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, text: newValue })); onUpdateProperty("componentConfig.text", newValue); }} placeholder="λ²„νŠΌ ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•˜μ„Έμš”" />
)}
{/* ────────────────── μ•„μ΄μ½˜ μ„€μ • μ˜μ—­ ────────────────── */} {showIconSettings && (
{/* μΆ”μ²œ μ•„μ΄μ½˜ / μ•ˆλ‚΄ 문ꡬ */} {isNoIconAction ? (
{NO_ICON_MESSAGE}
{/* μ»€μŠ€ν…€ μ•„μ΄μ½˜μ΄ 있으면 ν‘œμ‹œ */} {(customIcons.length > 0 || customSvgIcons.length > 0) && ( <>
μ»€μŠ€ν…€ μ•„μ΄μ½˜
{customIcons.map((iconName) => { const Icon = getLucideIcon(iconName); if (!Icon) return null; return (
); })} {customSvgIcons.map((svgIcon) => (
))}
)} {/* μ»€μŠ€ν…€ μ•„μ΄μ½˜ μΆ”κ°€ λ²„νŠΌ */}
μ•„μ΄μ½˜μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. {Object.keys(allLucideIcons) .filter((name) => name.toLowerCase().includes(lucideSearchTerm.toLowerCase())) .slice(0, 30) .map((iconName) => { const Icon = allLucideIcons[iconName as keyof typeof allLucideIcons]; return ( { const next = [...customIcons]; if (!next.includes(iconName)) { next.push(iconName); onUpdateProperty("componentConfig.customIcons", next); if (Icon) addToIconMap(iconName, Icon); } setLucideSearchOpen(false); setLucideSearchTerm(""); }} className="flex items-center gap-2 text-xs" > {Icon ? : } {iconName} {customIcons.includes(iconName) && } ); })} setSvgName(e.target.value)} placeholder="예: νšŒμ‚¬λ‘œκ³ " className="h-7 text-xs" />