504 lines
21 KiB
TypeScript
504 lines
21 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>
|
|
<div className="space-y-2">
|
|
{/* 선택된 필드 표시 */}
|
|
{(localConfig.displayFields && localConfig.displayFields.length > 0) ? (
|
|
<div className="flex flex-wrap gap-1 rounded-md border p-2 min-h-[40px]">
|
|
{localConfig.displayFields.map((fieldName) => {
|
|
const col = sourceTableColumns.find((c) => c.columnName === fieldName);
|
|
return (
|
|
<span
|
|
key={fieldName}
|
|
className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-2 py-1 text-xs"
|
|
>
|
|
{col?.displayName || fieldName}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newFields = localConfig.displayFields?.filter((f) => f !== fieldName) || [];
|
|
updateConfig({
|
|
displayFields: newFields,
|
|
displayField: newFields[0] || "", // 첫 번째 필드를 기본 displayField로
|
|
});
|
|
}}
|
|
className="hover:text-destructive"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border border-dashed p-2 text-center text-xs text-muted-foreground">
|
|
아래에서 표시할 필드를 선택하세요
|
|
</div>
|
|
)}
|
|
|
|
{/* 필드 선택 드롭다운 */}
|
|
<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}
|
|
>
|
|
{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) => {
|
|
const isSelected = localConfig.displayFields?.includes(column.columnName);
|
|
return (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={() => {
|
|
const currentFields = localConfig.displayFields || [];
|
|
let newFields: string[];
|
|
if (isSelected) {
|
|
newFields = currentFields.filter((f) => f !== column.columnName);
|
|
} else {
|
|
newFields = [...currentFields, column.columnName];
|
|
}
|
|
updateConfig({
|
|
displayFields: newFields,
|
|
displayField: newFields[0] || "", // 첫 번째 필드를 기본 displayField로
|
|
});
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", isSelected ? "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>
|
|
|
|
{/* 구분자 설정 */}
|
|
{localConfig.displayFields && localConfig.displayFields.length > 1 && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs whitespace-nowrap">구분자:</Label>
|
|
<Input
|
|
value={localConfig.displaySeparator || " → "}
|
|
onChange={(e) => updateConfig({ displaySeparator: e.target.value })}
|
|
placeholder=" → "
|
|
className="h-7 w-20 text-xs text-center"
|
|
/>
|
|
<span className="text-xs text-muted-foreground">
|
|
미리보기: {localConfig.displayFields.map((f) => {
|
|
const col = sourceTableColumns.find((c) => c.columnName === f);
|
|
return col?.displayName || f;
|
|
}).join(localConfig.displaySeparator || " → ")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</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.displayFields?.length
|
|
? localConfig.displayFields.join(localConfig.displaySeparator || " → ")
|
|
: localConfig.displayField}
|
|
</p>
|
|
<p>
|
|
<strong>저장 테이블:</strong> {localConfig.targetTable}
|
|
</p>
|
|
<p>
|
|
<strong>매핑 개수:</strong> {(localConfig.fieldMappings || []).length}개
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|