"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 { Check, ChevronsUpDown, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { ComponentData } from "@/types/screen"; import { apiClient } from "@/lib/api/client"; import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel"; import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel"; import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel"; interface ButtonConfigPanelProps { component: ComponentData; onUpdateProperty: (path: string, value: any) => void; allComponents?: ComponentData[]; // πŸ†• ν”Œλ‘œμš° μœ„μ ― κ°μ§€μš© currentTableName?: string; // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… (μžλ™ κ°μ§€μš©) currentScreenCompanyCode?: string; // ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ } interface ScreenOption { id: number; name: string; description?: string; } export const ButtonConfigPanel: React.FC = ({ component, onUpdateProperty, allComponents = [], // πŸ†• κΈ°λ³Έκ°’ 빈 λ°°μ—΄ currentTableName, // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… currentScreenCompanyCode, // ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ }) => { // πŸ”§ componentμ—μ„œ 직접 읽기 (useMemo 제거) const config = component.componentConfig || {}; const currentAction = component.componentConfig?.action || {}; // 둜컬 μƒνƒœ 관리 (μ‹€μ‹œκ°„ μž…λ ₯ 반영) const [localInputs, setLocalInputs] = useState({ text: config.text !== undefined ? config.text : "λ²„νŠΌ", modalTitle: String(config.action?.modalTitle || ""), modalDescription: String(config.action?.modalDescription || ""), editModalTitle: String(config.action?.editModalTitle || ""), editModalDescription: String(config.action?.editModalDescription || ""), targetUrl: String(config.action?.targetUrl || ""), }); const [screens, setScreens] = useState([]); const [screensLoading, setScreensLoading] = useState(false); const [modalScreenOpen, setModalScreenOpen] = useState(false); const [navScreenOpen, setNavScreenOpen] = useState(false); const [modalSearchTerm, setModalSearchTerm] = useState(""); const [navSearchTerm, setNavSearchTerm] = useState(""); // ν…Œμ΄λΈ” 컬럼 λͺ©λ‘ μƒνƒœ const [tableColumns, setTableColumns] = useState([]); const [columnsLoading, setColumnsLoading] = useState(false); const [displayColumnOpen, setDisplayColumnOpen] = useState(false); const [displayColumnSearch, setDisplayColumnSearch] = useState(""); // 🎯 ν”Œλ‘œμš° μœ„μ ―μ΄ 화면에 μžˆλŠ”μ§€ 확인 const 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 || ""), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [component.id]); // ν™”λ©΄ λͺ©λ‘ κ°€μ Έμ˜€κΈ° (ν˜„μž¬ νŽΈμ§‘ 쀑인 ν™”λ©΄μ˜ νšŒμ‚¬ μ½”λ“œ κΈ°μ€€) 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λ₯Ό μžλ™μœΌλ‘œ κ°μ§€ν•©λ‹ˆλ‹€

직접 μ§€μ •ν•˜λ €λ©΄ ν…Œμ΄λΈ”λͺ…을 μž…λ ₯ν•˜μ„Έμš” (예: item_info)

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, modalTitle: newValue })); onUpdateProperty("componentConfig.action.modalTitle", 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}}
)); })()}

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

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

μˆ˜μ • μ„€μ •

setModalSearchTerm(e.target.value)} className="border-0 p-0 focus-visible:ring-0" />
{(() => { const filteredScreens = filterScreens(modalSearchTerm); if (screensLoading) { return
ν™”λ©΄ λͺ©λ‘μ„ λΆˆλŸ¬μ˜€λŠ” 쀑...
; } if (filteredScreens.length === 0) { return
검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.
; } return filteredScreens.map((screen, index) => (
{ onUpdateProperty("componentConfig.action.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

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

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

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

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

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

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

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

setModalSearchTerm(e.target.value)} className="border-0 p-0 focus-visible:ring-0" />
{(() => { const filteredScreens = filterScreens(modalSearchTerm); if (screensLoading) { return
ν™”λ©΄ λͺ©λ‘μ„ λΆˆλŸ¬μ˜€λŠ” 쀑...
; } if (filteredScreens.length === 0) { return
검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.
; } return filteredScreens.map((screen, index) => (
{ onUpdateProperty("componentConfig.action.targetScreenId", screen.id); setModalScreenOpen(false); setModalSearchTerm(""); }} >
{screen.name} {screen.description && {screen.description}}
)); })()}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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