ERP-node/frontend/components/screen/config-panels/button/DataTab.tsx

873 lines
44 KiB
TypeScript
Raw Normal View History

"use client";
import React 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, Plus, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { QuickInsertConfigSection } from "../QuickInsertConfigSection";
import { ComponentData } from "@/types/screen";
export interface DataTabProps {
config: any;
onChange: (key: string, value: any) => void;
component: ComponentData;
allComponents: ComponentData[];
currentTableName?: string;
availableTables: Array<{ name: string; label: string }>;
mappingTargetColumns: Array<{ name: string; label: string }>;
mappingSourceColumnsMap: Record<string, Array<{ name: string; label: string }>>;
currentTableColumns: Array<{ name: string; label: string }>;
mappingSourcePopoverOpen: Record<string, boolean>;
setMappingSourcePopoverOpen: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
mappingTargetPopoverOpen: Record<string, boolean>;
setMappingTargetPopoverOpen: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
activeMappingGroupIndex: number;
setActiveMappingGroupIndex: React.Dispatch<React.SetStateAction<number>>;
loadMappingColumns: (tableName: string) => Promise<Array<{ name: string; label: string }>>;
setMappingSourceColumnsMap: React.Dispatch<
React.SetStateAction<Record<string, Array<{ name: string; label: string }>>>
>;
}
export const DataTab: React.FC<DataTabProps> = ({
config,
onChange,
component,
allComponents,
currentTableName,
availableTables,
mappingTargetColumns,
mappingSourceColumnsMap,
currentTableColumns,
mappingSourcePopoverOpen,
setMappingSourcePopoverOpen,
mappingTargetPopoverOpen,
setMappingTargetPopoverOpen,
activeMappingGroupIndex,
setActiveMappingGroupIndex,
loadMappingColumns,
setMappingSourceColumnsMap,
}) => {
const actionType = config.action?.type;
const onUpdateProperty = (path: string, value: any) => onChange(path, value);
if (actionType === "quickInsert") {
return (
<div className="space-y-4">
<QuickInsertConfigSection
component={component}
onUpdateProperty={onUpdateProperty}
allComponents={allComponents}
currentTableName={currentTableName}
/>
</div>
);
}
if (actionType !== "transferData") {
return (
<div className="text-muted-foreground py-8 text-center text-sm">
.
</div>
);
}
return (
<div className="space-y-4">
<div className="bg-muted/50 space-y-4 rounded-lg border p-4">
<h4 className="text-foreground text-sm font-medium"> </h4>
<div>
<Label>
<span className="text-destructive">*</span>
</Label>
<Select
value={config.action?.dataTransfer?.sourceComponentId || ""}
onValueChange={(value) =>
onUpdateProperty("componentConfig.action.dataTransfer.sourceComponentId", value)
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="데이터를 가져올 컴포넌트 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__auto__">
<div className="flex items-center gap-2">
<span className="text-xs font-medium"> ( )</span>
<span className="text-muted-foreground text-[10px]">(auto)</span>
</div>
</SelectItem>
{allComponents
.filter((comp: any) => {
const type = comp.componentType || comp.type || "";
return ["table-list", "repeater-field-group", "form-group", "data-table"].some((t) =>
type.includes(t),
);
})
.map((comp: any) => {
const compType = comp.componentType || comp.type || "unknown";
const compLabel = comp.label || comp.componentConfig?.title || comp.id;
const layerName = comp._layerName;
return (
<SelectItem key={comp.id} value={comp.id}>
<div className="flex items-center gap-2">
<span className="text-xs font-medium">{compLabel}</span>
<span className="text-muted-foreground text-[10px]">({compType})</span>
{layerName && (
<span className="rounded bg-amber-100 px-1 text-[9px] text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
{layerName}
</span>
)}
</div>
</SelectItem>
);
})}
{allComponents.filter((comp: any) => {
const type = comp.componentType || comp.type || "";
return ["table-list", "repeater-field-group", "form-group", "data-table"].some((t) =>
type.includes(t),
);
}).length === 0 && (
<SelectItem value="__none__" disabled>
</SelectItem>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs">
&quot; &quot;
</p>
</div>
<div>
<Label htmlFor="target-type">
<span className="text-destructive">*</span>
</Label>
<Select
value={config.action?.dataTransfer?.targetType || "component"}
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.targetType", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="component"> </SelectItem>
<SelectItem value="splitPanel"> </SelectItem>
<SelectItem value="screen" disabled>
( )
</SelectItem>
</SelectContent>
</Select>
{config.action?.dataTransfer?.targetType === "splitPanel" && (
<p className="text-muted-foreground mt-1 text-[10px]">
. , .
</p>
)}
</div>
{config.action?.dataTransfer?.targetType === "component" && (
<div>
<Label>
<span className="text-destructive">*</span>
</Label>
<Select
value={config.action?.dataTransfer?.targetComponentId || ""}
onValueChange={(value) => {
onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", value);
const selectedComp = allComponents.find((c: any) => c.id === value);
if (selectedComp && (selectedComp as any)._layerId) {
onUpdateProperty(
"componentConfig.action.dataTransfer.targetLayerId",
(selectedComp as any)._layerId,
);
} else {
onUpdateProperty("componentConfig.action.dataTransfer.targetLayerId", undefined);
}
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="데이터를 받을 컴포넌트 선택" />
</SelectTrigger>
<SelectContent>
{allComponents
.filter((comp: any) => {
const type = comp.componentType || comp.type || "";
const isReceivable = ["table-list", "repeater-field-group", "form-group", "data-table"].some(
(t) => type.includes(t),
);
return isReceivable && comp.id !== config.action?.dataTransfer?.sourceComponentId;
})
.map((comp: any) => {
const compType = comp.componentType || comp.type || "unknown";
const compLabel = comp.label || comp.componentConfig?.title || comp.id;
const layerName = comp._layerName;
return (
<SelectItem key={comp.id} value={comp.id}>
<div className="flex items-center gap-2">
<span className="text-xs font-medium">{compLabel}</span>
<span className="text-muted-foreground text-[10px]">({compType})</span>
{layerName && (
<span className="rounded bg-amber-100 px-1 text-[9px] text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
{layerName}
</span>
)}
</div>
</SelectItem>
);
})}
{allComponents.filter((comp: any) => {
const type = comp.componentType || comp.type || "";
const isReceivable = ["table-list", "repeater-field-group", "form-group", "data-table"].some(
(t) => type.includes(t),
);
return isReceivable && comp.id !== config.action?.dataTransfer?.sourceComponentId;
}).length === 0 && (
<SelectItem value="__none__" disabled>
</SelectItem>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs">, </p>
</div>
)}
{config.action?.dataTransfer?.targetType === "splitPanel" && (
<div>
<Label> ID ()</Label>
<Input
value={config.action?.dataTransfer?.targetComponentId || ""}
onChange={(e) =>
onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value)
}
placeholder="비워두면 첫 번째 수신 가능 컴포넌트로 전달"
className="h-8 text-xs"
/>
<p className="text-muted-foreground mt-1 text-xs">
ID를 , .
</p>
</div>
)}
<div>
<Label htmlFor="transfer-mode"> </Label>
<Select
value={config.action?.dataTransfer?.mode || "append"}
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.mode", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="append"> (Append)</SelectItem>
<SelectItem value="replace"> (Replace)</SelectItem>
<SelectItem value="merge"> (Merge)</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs"> </p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="clear-after-transfer"> </Label>
<p className="text-muted-foreground text-xs"> </p>
</div>
<Switch
id="clear-after-transfer"
checked={config.action?.dataTransfer?.clearAfterTransfer === true}
onCheckedChange={(checked) =>
onUpdateProperty("componentConfig.action.dataTransfer.clearAfterTransfer", checked)
}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="confirm-before-transfer"> </Label>
<p className="text-muted-foreground text-xs"> </p>
</div>
<Switch
id="confirm-before-transfer"
checked={config.action?.dataTransfer?.confirmBeforeTransfer === true}
onCheckedChange={(checked) =>
onUpdateProperty("componentConfig.action.dataTransfer.confirmBeforeTransfer", checked)
}
/>
</div>
{config.action?.dataTransfer?.confirmBeforeTransfer && (
<div>
<Label htmlFor="confirm-message"> </Label>
<Input
id="confirm-message"
placeholder="선택한 항목을 전달하시겠습니까?"
value={config.action?.dataTransfer?.confirmMessage || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.confirmMessage", e.target.value)}
className="h-8 text-xs"
/>
</div>
)}
<div className="space-y-2">
<Label> </Label>
<div className="space-y-2 rounded-md border p-3">
<div className="flex items-center gap-2">
<Label htmlFor="min-selection" className="text-xs">
</Label>
<Input
id="min-selection"
type="number"
placeholder="0"
value={config.action?.dataTransfer?.validation?.minSelection || ""}
onChange={(e) =>
onUpdateProperty(
"componentConfig.action.dataTransfer.validation.minSelection",
parseInt(e.target.value) || 0,
)
}
className="h-8 w-20 text-xs"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="max-selection" className="text-xs">
</Label>
<Input
id="max-selection"
type="number"
placeholder="제한없음"
value={config.action?.dataTransfer?.validation?.maxSelection || ""}
onChange={(e) =>
onUpdateProperty(
"componentConfig.action.dataTransfer.validation.maxSelection",
parseInt(e.target.value) || undefined,
)
}
className="h-8 w-20 text-xs"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label> ()</Label>
<p className="text-muted-foreground text-xs">
</p>
<div className="space-y-2 rounded-md border p-3">
<div>
<Label className="text-xs"> </Label>
<Select
value={config.action?.dataTransfer?.additionalSources?.[0]?.componentId || ""}
onValueChange={(value) => {
const currentSources = config.action?.dataTransfer?.additionalSources || [];
const newSources = [...currentSources];
if (newSources.length === 0) {
newSources.push({ componentId: value, fieldName: "" });
} else {
newSources[0] = { ...newSources[0], componentId: value };
}
onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources);
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="추가 데이터 컴포넌트 선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__clear__">
<span className="text-muted-foreground"> </span>
</SelectItem>
{allComponents
.filter((comp: any) => {
const type = comp.componentType || comp.type || "";
return ["conditional-container", "select-basic", "select", "combobox"].some((t) =>
type.includes(t),
);
})
.map((comp: any) => {
const compType = comp.componentType || comp.type || "unknown";
const compLabel = comp.label || comp.componentConfig?.controlLabel || comp.id;
return (
<SelectItem key={comp.id} value={comp.id}>
<div className="flex items-center gap-2">
<span className="text-xs font-medium">{compLabel}</span>
<span className="text-muted-foreground text-[10px]">({compType})</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs">
, ( )
</p>
</div>
<div>
<Label htmlFor="additional-field-name" className="text-xs">
()
</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{(() => {
const fieldName = config.action?.dataTransfer?.additionalSources?.[0]?.fieldName;
if (!fieldName) return "필드 선택 (비워두면 전체 데이터)";
const cols = mappingTargetColumns.length > 0 ? mappingTargetColumns : currentTableColumns;
const found = cols.find((c) => c.name === fieldName);
return found ? `${found.label || found.name}` : fieldName;
})()}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> .</CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
const currentSources = config.action?.dataTransfer?.additionalSources || [];
const newSources = [...currentSources];
if (newSources.length === 0) {
newSources.push({ componentId: "", fieldName: "" });
} else {
newSources[0] = { ...newSources[0], fieldName: "" };
}
onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!config.action?.dataTransfer?.additionalSources?.[0]?.fieldName ? "opacity-100" : "opacity-0",
)}
/>
<span className="text-muted-foreground"> ( )</span>
</CommandItem>
{(mappingTargetColumns.length > 0 ? mappingTargetColumns : currentTableColumns).map((col) => (
<CommandItem
key={col.name}
value={`${col.label || ""} ${col.name}`}
onSelect={() => {
const currentSources = config.action?.dataTransfer?.additionalSources || [];
const newSources = [...currentSources];
if (newSources.length === 0) {
newSources.push({ componentId: "", fieldName: col.name });
} else {
newSources[0] = { ...newSources[0], fieldName: col.name };
}
onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.action?.dataTransfer?.additionalSources?.[0]?.fieldName === col.name
? "opacity-100"
: "opacity-0",
)}
/>
<span className="font-medium">{col.label || col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground ml-1 text-[10px]">({col.name})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="text-muted-foreground mt-1 text-xs"> </p>
</div>
</div>
</div>
<div className="space-y-3">
<Label> </Label>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{config.action?.dataTransfer?.targetTable
? availableTables.find((t) => t.name === config.action?.dataTransfer?.targetTable)?.label ||
config.action?.dataTransfer?.targetTable
: "타겟 테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{availableTables.map((table) => (
<CommandItem
key={table.name}
value={`${table.label} ${table.name}`}
onSelect={() => {
onUpdateProperty("componentConfig.action.dataTransfer.targetTable", table.name);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.action?.dataTransfer?.targetTable === table.name ? "opacity-100" : "opacity-0",
)}
/>
<span className="font-medium">{table.label}</span>
<span className="text-muted-foreground ml-1">({table.name})</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Button
type="button"
variant="outline"
size="sm"
className="h-6 text-[10px]"
onClick={() => {
const currentMappings = config.action?.dataTransfer?.multiTableMappings || [];
onUpdateProperty("componentConfig.action.dataTransfer.multiTableMappings", [
...currentMappings,
{ sourceTable: "", mappingRules: [] },
]);
setActiveMappingGroupIndex(currentMappings.length);
}}
disabled={!config.action?.dataTransfer?.targetTable}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-muted-foreground text-[10px]">
, . .
</p>
{!config.action?.dataTransfer?.targetTable ? (
<div className="rounded-md border border-dashed p-3 text-center">
<p className="text-muted-foreground text-xs"> .</p>
</div>
) : !(config.action?.dataTransfer?.multiTableMappings || []).length ? (
<div className="rounded-md border border-dashed p-3 text-center">
<p className="text-muted-foreground text-xs"> . .</p>
</div>
) : (
<div className="space-y-2">
<div className="flex flex-wrap gap-1">
{(config.action?.dataTransfer?.multiTableMappings || []).map((group: any, gIdx: number) => (
<div key={gIdx} className="flex items-center gap-0.5">
<Button
type="button"
variant={activeMappingGroupIndex === gIdx ? "default" : "outline"}
size="sm"
className="h-6 text-[10px]"
onClick={() => setActiveMappingGroupIndex(gIdx)}
>
{group.sourceTable
? availableTables.find((t) => t.name === group.sourceTable)?.label || group.sourceTable
: `그룹 ${gIdx + 1}`}
{group.mappingRules?.length > 0 && (
<span className="bg-primary/20 ml-1 rounded-full px-1 text-[9px]">
{group.mappingRules.length}
</span>
)}
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="text-destructive hover:bg-destructive/10 h-5 w-5"
onClick={() => {
const mappings = [...(config.action?.dataTransfer?.multiTableMappings || [])];
mappings.splice(gIdx, 1);
onUpdateProperty("componentConfig.action.dataTransfer.multiTableMappings", mappings);
if (activeMappingGroupIndex >= mappings.length) {
setActiveMappingGroupIndex(Math.max(0, mappings.length - 1));
}
}}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
{(() => {
const multiMappings = config.action?.dataTransfer?.multiTableMappings || [];
const activeGroup = multiMappings[activeMappingGroupIndex];
if (!activeGroup) return null;
const activeSourceTable = activeGroup.sourceTable || "";
const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || [];
const activeRules: any[] = activeGroup.mappingRules || [];
const updateGroupField = (field: string, value: any) => {
const mappings = [...multiMappings];
mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value };
onUpdateProperty("componentConfig.action.dataTransfer.multiTableMappings", mappings);
};
return (
<div className="space-y-2 rounded-md border p-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{activeSourceTable
? availableTables.find((t) => t.name === activeSourceTable)?.label || activeSourceTable
: "소스 테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{availableTables.map((table) => (
<CommandItem
key={table.name}
value={`${table.label} ${table.name}`}
onSelect={async () => {
updateGroupField("sourceTable", table.name);
if (!mappingSourceColumnsMap[table.name]) {
const cols = await loadMappingColumns(table.name);
setMappingSourceColumnsMap((prev) => ({ ...prev, [table.name]: cols }));
}
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
activeSourceTable === table.name ? "opacity-100" : "opacity-0",
)}
/>
<span className="font-medium">{table.label}</span>
<span className="text-muted-foreground ml-1">({table.name})</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label className="text-[10px]"> </Label>
<Button
type="button"
variant="outline"
size="sm"
className="h-5 text-[10px]"
onClick={() => {
updateGroupField("mappingRules", [...activeRules, { sourceField: "", targetField: "" }]);
}}
disabled={!activeSourceTable}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{!activeSourceTable ? (
<p className="text-muted-foreground text-[10px]"> .</p>
) : activeRules.length === 0 ? (
<p className="text-muted-foreground text-[10px]"> ( )</p>
) : (
activeRules.map((rule: any, rIdx: number) => {
const popoverKeyS = `${activeMappingGroupIndex}-${rIdx}-s`;
const popoverKeyT = `${activeMappingGroupIndex}-${rIdx}-t`;
return (
<div key={rIdx} className="bg-background flex items-center gap-2 rounded-md border p-2">
<div className="flex-1">
<Popover
open={mappingSourcePopoverOpen[popoverKeyS] || false}
onOpenChange={(open) =>
setMappingSourcePopoverOpen((prev) => ({ ...prev, [popoverKeyS]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{rule.sourceField
? activeSourceColumns.find((c) => c.name === rule.sourceField)?.label ||
rule.sourceField
: "소스 필드"}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{activeSourceColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.label} ${col.name}`}
onSelect={() => {
const newRules = [...activeRules];
newRules[rIdx] = { ...newRules[rIdx], sourceField: col.name };
updateGroupField("mappingRules", newRules);
setMappingSourcePopoverOpen((prev) => ({
...prev,
[popoverKeyS]: false,
}));
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
rule.sourceField === col.name ? "opacity-100" : "opacity-0",
)}
/>
<span>{col.label}</span>
{col.label !== col.name && (
<span className="text-muted-foreground ml-1">({col.name})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<span className="text-muted-foreground text-xs"></span>
<div className="flex-1">
<Popover
open={mappingTargetPopoverOpen[popoverKeyT] || false}
onOpenChange={(open) =>
setMappingTargetPopoverOpen((prev) => ({ ...prev, [popoverKeyT]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{rule.targetField
? mappingTargetColumns.find((c) => c.name === rule.targetField)?.label ||
rule.targetField
: "타겟 필드"}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{mappingTargetColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.label} ${col.name}`}
onSelect={() => {
const newRules = [...activeRules];
newRules[rIdx] = { ...newRules[rIdx], targetField: col.name };
updateGroupField("mappingRules", newRules);
setMappingTargetPopoverOpen((prev) => ({
...prev,
[popoverKeyT]: false,
}));
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
rule.targetField === col.name ? "opacity-100" : "opacity-0",
)}
/>
<span>{col.label}</span>
{col.label !== col.name && (
<span className="text-muted-foreground ml-1">({col.name})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="text-destructive hover:bg-destructive/10 h-7 w-7"
onClick={() => {
const newRules = [...activeRules];
newRules.splice(rIdx, 1);
updateGroupField("mappingRules", newRules);
}}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})
)}
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
<p className="text-xs text-blue-900 dark:text-blue-100">
<strong> :</strong>
<br />
1.
<br />
2.
<br />
3.
</p>
</div>
</div>
</div>
);
};