ERP-node/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConf...

436 lines
18 KiB
TypeScript

"use client";
import React, { useState, useEffect, useRef } 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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
import { AutocompleteSearchInputConfig } from "./types";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { cn } from "@/lib/utils";
interface AutocompleteSearchInputConfigPanelProps {
config: AutocompleteSearchInputConfig;
onConfigChange: (config: AutocompleteSearchInputConfig) => void;
}
export function AutocompleteSearchInputConfigPanel({
config,
onConfigChange,
}: AutocompleteSearchInputConfigPanelProps) {
// 초기화 여부 추적 (첫 마운트 시에만 config로 초기화)
const isInitialized = useRef(false);
const [localConfig, setLocalConfig] = useState<AutocompleteSearchInputConfig>(config);
const [allTables, setAllTables] = useState<any[]>([]);
const [sourceTableColumns, setSourceTableColumns] = useState<any[]>([]);
const [targetTableColumns, setTargetTableColumns] = useState<any[]>([]);
const [isLoadingTables, setIsLoadingTables] = useState(false);
const [isLoadingSourceColumns, setIsLoadingSourceColumns] = useState(false);
const [isLoadingTargetColumns, setIsLoadingTargetColumns] = useState(false);
const [openSourceTableCombo, setOpenSourceTableCombo] = useState(false);
const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false);
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
// 첫 마운트 시에만 config로 초기화 (이후에는 localConfig 유지)
useEffect(() => {
if (!isInitialized.current && config) {
setLocalConfig(config);
isInitialized.current = true;
}
}, [config]);
const updateConfig = (updates: Partial<AutocompleteSearchInputConfig>) => {
const newConfig = { ...localConfig, ...updates };
console.log("🔧 [AutocompleteConfigPanel] updateConfig:", {
updates,
localConfig,
newConfig,
});
setLocalConfig(newConfig);
onConfigChange(newConfig);
};
// 테이블 목록 로드
useEffect(() => {
const loadTables = async () => {
setIsLoadingTables(true);
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setAllTables(response.data);
}
} catch (error) {
setAllTables([]);
} finally {
setIsLoadingTables(false);
}
};
loadTables();
}, []);
// 외부 테이블 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!localConfig.tableName) {
setSourceTableColumns([]);
return;
}
setIsLoadingSourceColumns(true);
try {
const response = await tableManagementApi.getColumnList(localConfig.tableName);
if (response.success && response.data) {
setSourceTableColumns(response.data.columns);
}
} catch (error) {
setSourceTableColumns([]);
} finally {
setIsLoadingSourceColumns(false);
}
};
loadColumns();
}, [localConfig.tableName]);
// 저장 테이블 컬럼 로드
useEffect(() => {
const loadTargetColumns = async () => {
if (!localConfig.targetTable) {
setTargetTableColumns([]);
return;
}
setIsLoadingTargetColumns(true);
try {
const response = await tableManagementApi.getColumnList(localConfig.targetTable);
if (response.success && response.data) {
setTargetTableColumns(response.data.columns);
}
} catch (error) {
setTargetTableColumns([]);
} finally {
setIsLoadingTargetColumns(false);
}
};
loadTargetColumns();
}, [localConfig.targetTable]);
const addFieldMapping = () => {
const mappings = localConfig.fieldMappings || [];
updateConfig({
fieldMappings: [...mappings, { sourceField: "", targetField: "", label: "" }],
});
};
const updateFieldMapping = (index: number, updates: any) => {
const mappings = [...(localConfig.fieldMappings || [])];
mappings[index] = { ...mappings[index], ...updates };
updateConfig({ fieldMappings: mappings });
};
const removeFieldMapping = (index: number) => {
const mappings = [...(localConfig.fieldMappings || [])];
mappings.splice(index, 1);
updateConfig({ fieldMappings: mappings });
};
return (
<div className="space-y-6 p-4">
{/* 1. 외부 테이블 선택 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">1. *</Label>
<Popover open={openSourceTableCombo} onOpenChange={setOpenSourceTableCombo}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openSourceTableCombo}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
disabled={isLoadingTables}
>
{localConfig.tableName
? allTables.find((t) => t.tableName === localConfig.tableName)?.displayName || localConfig.tableName
: isLoadingTables ? "로딩 중..." : "데이터를 가져올 테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup>
{allTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={() => {
updateConfig({ tableName: table.tableName });
setOpenSourceTableCombo(false);
}}
className="text-xs sm:text-sm"
>
<Check className={cn("mr-2 h-4 w-4", localConfig.tableName === table.tableName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{table.displayName || table.tableName}</span>
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 2. 표시 필드 선택 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">2. *</Label>
<Popover open={openDisplayFieldCombo} onOpenChange={setOpenDisplayFieldCombo}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openDisplayFieldCombo}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
disabled={!localConfig.tableName || isLoadingSourceColumns}
>
{localConfig.displayField
? sourceTableColumns.find((c) => c.columnName === localConfig.displayField)?.displayName || localConfig.displayField
: isLoadingSourceColumns ? "로딩 중..." : "사용자에게 보여줄 필드"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="필드 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup>
{sourceTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => {
updateConfig({ displayField: column.columnName });
setOpenDisplayFieldCombo(false);
}}
className="text-xs sm:text-sm"
>
<Check className={cn("mr-2 h-4 w-4", localConfig.displayField === column.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{column.displayName || column.columnName}</span>
{column.displayName && <span className="text-[10px] text-gray-500">{column.columnName}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 3. 저장 대상 테이블 선택 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">3. *</Label>
<Popover open={openTargetTableCombo} onOpenChange={setOpenTargetTableCombo}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openTargetTableCombo}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
disabled={isLoadingTables}
>
{localConfig.targetTable
? allTables.find((t) => t.tableName === localConfig.targetTable)?.displayName || localConfig.targetTable
: "데이터를 저장할 테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup>
{allTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={() => {
updateConfig({ targetTable: table.tableName });
setOpenTargetTableCombo(false);
}}
className="text-xs sm:text-sm"
>
<Check className={cn("mr-2 h-4 w-4", localConfig.targetTable === table.tableName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{table.displayName || table.tableName}</span>
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 4. 필드 매핑 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold sm:text-sm">4. *</Label>
<Button
size="sm"
variant="outline"
onClick={addFieldMapping}
className="h-7 text-xs"
disabled={!localConfig.tableName || !localConfig.targetTable || isLoadingSourceColumns || isLoadingTargetColumns}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{(localConfig.fieldMappings || []).length === 0 && (
<div className="rounded-lg border border-dashed p-6 text-center">
<p className="text-sm text-muted-foreground">
</p>
</div>
)}
<div className="space-y-3">
{(localConfig.fieldMappings || []).map((mapping, index) => (
<div key={index} className="rounded-lg border bg-card p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">
#{index + 1}
</span>
<Button
size="sm"
variant="ghost"
onClick={() => removeFieldMapping(index)}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
<div className="space-y-1.5">
<Label className="text-xs"></Label>
<Input
value={mapping.label || ""}
onChange={(e) =>
updateFieldMapping(index, { label: e.target.value })
}
placeholder="예: 거래처 코드"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs"> *</Label>
<Select
value={mapping.sourceField || undefined}
onValueChange={(value) => {
console.log("🔧 [Select] sourceField 변경:", value);
updateFieldMapping(index, { sourceField: value });
}}
disabled={!localConfig.tableName || isLoadingSourceColumns}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="가져올 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs"> *</Label>
<Select
value={mapping.targetField || undefined}
onValueChange={(value) => {
console.log("🔧 [Select] targetField 변경:", value);
updateFieldMapping(index, { targetField: value });
}}
disabled={!localConfig.targetTable || isLoadingTargetColumns}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="저장할 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{targetTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{mapping.sourceField && mapping.targetField && (
<div className="rounded bg-blue-50 p-2 dark:bg-blue-950">
<p className="text-[10px] text-blue-700 dark:text-blue-300">
<code className="rounded bg-blue-100 px-1 font-mono dark:bg-blue-900">
{localConfig.tableName}.{mapping.sourceField}
</code>
{" → "}
<code className="rounded bg-blue-100 px-1 font-mono dark:bg-blue-900">
{localConfig.targetTable}.{mapping.targetField}
</code>
</p>
</div>
)}
</div>
))}
</div>
</div>
{/* 플레이스홀더 */}
<div className="space-y-2">
<Label className="text-xs sm:text-sm"></Label>
<Input
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig({ placeholder: e.target.value })}
placeholder="검색..."
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
{/* 설정 요약 */}
{localConfig.tableName && localConfig.targetTable && (localConfig.fieldMappings || []).length > 0 && (
<div className="rounded-lg border bg-green-50 p-4 dark:bg-green-950">
<h3 className="mb-2 text-sm font-semibold text-green-800 dark:text-green-200">
</h3>
<div className="space-y-1 text-xs text-green-700 dark:text-green-300">
<p>
<strong> :</strong> {localConfig.tableName}
</p>
<p>
<strong> :</strong> {localConfig.displayField}
</p>
<p>
<strong> :</strong> {localConfig.targetTable}
</p>
<p>
<strong> :</strong> {(localConfig.fieldMappings || []).length}
</p>
</div>
</div>
)}
</div>
);
}