화면 분할 패널 기능
This commit is contained in:
@@ -83,6 +83,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
const [blockTablePopoverOpen, setBlockTablePopoverOpen] = useState<Record<string, boolean>>({}); // 블록별 테이블 Popover 열림 상태
|
||||
const [blockColumnPopoverOpen, setBlockColumnPopoverOpen] = useState<Record<string, boolean>>({}); // 블록별 컬럼 Popover 열림 상태
|
||||
|
||||
// 🆕 데이터 전달 필드 매핑용 상태
|
||||
const [mappingSourceColumns, setMappingSourceColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [mappingTargetColumns, setMappingTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [mappingSourcePopoverOpen, setMappingSourcePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [mappingTargetPopoverOpen, setMappingTargetPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [mappingSourceSearch, setMappingSourceSearch] = useState<Record<number, string>>({});
|
||||
const [mappingTargetSearch, setMappingTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
// 🎯 플로우 위젯이 화면에 있는지 확인
|
||||
const hasFlowWidget = useMemo(() => {
|
||||
const found = allComponents.some((comp: any) => {
|
||||
@@ -258,6 +266,58 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 데이터 전달 소스/타겟 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const sourceTable = config.action?.dataTransfer?.sourceTable;
|
||||
const targetTable = config.action?.dataTransfer?.targetTable;
|
||||
|
||||
const loadColumns = async () => {
|
||||
if (sourceTable) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${sourceTable}/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,
|
||||
}));
|
||||
setMappingSourceColumns(columns);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTable) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${targetTable}/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,
|
||||
}));
|
||||
setMappingTargetColumns(columns);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("타겟 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
|
||||
|
||||
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
|
||||
useEffect(() => {
|
||||
const fetchScreens = async () => {
|
||||
@@ -1607,19 +1667,52 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 className="text-sm font-medium text-foreground">📦 데이터 전달 설정</h4>
|
||||
|
||||
{/* 소스 컴포넌트 선택 (Combobox) */}
|
||||
<div>
|
||||
<Label htmlFor="source-component-id">
|
||||
소스 컴포넌트 ID <span className="text-destructive">*</span>
|
||||
<Label>
|
||||
소스 컴포넌트 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="source-component-id"
|
||||
placeholder="예: table-list-1"
|
||||
<Select
|
||||
value={config.action?.dataTransfer?.sourceComponentId || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.sourceComponentId", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.sourceComponentId", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="데이터를 가져올 컴포넌트 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* 데이터 제공 가능한 컴포넌트 필터링 */}
|
||||
{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;
|
||||
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-[10px] text-muted-foreground">({compType})</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="mt-1 text-xs text-muted-foreground">
|
||||
데이터를 가져올 컴포넌트의 ID (테이블 등)
|
||||
테이블, 반복 필드 그룹 등 데이터를 제공하는 컴포넌트
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1636,25 +1729,85 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="component">같은 화면의 컴포넌트</SelectItem>
|
||||
<SelectItem value="screen">다른 화면 (구현 예정)</SelectItem>
|
||||
<SelectItem value="splitPanel">분할 패널 반대편 화면</SelectItem>
|
||||
<SelectItem value="screen" disabled>다른 화면 (구현 예정)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{config.action?.dataTransfer?.targetType === "splitPanel" && (
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
이 버튼이 분할 패널 내부에 있어야 합니다. 좌측 화면에서 우측으로, 또는 우측에서 좌측으로 데이터가 전달됩니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{config.action?.dataTransfer?.targetType !== "screen" && (
|
||||
{/* 타겟 컴포넌트 선택 (같은 화면의 컴포넌트일 때만) */}
|
||||
{config.action?.dataTransfer?.targetType === "component" && (
|
||||
<div>
|
||||
<Label htmlFor="target-component-id">
|
||||
타겟 컴포넌트 ID <span className="text-destructive">*</span>
|
||||
<Label>
|
||||
타겟 컴포넌트 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={config.action?.dataTransfer?.targetComponentId || ""}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", value)}
|
||||
>
|
||||
<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;
|
||||
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-[10px] text-muted-foreground">({compType})</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="mt-1 text-xs text-muted-foreground">
|
||||
테이블, 반복 필드 그룹 등 데이터를 받는 컴포넌트
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 분할 패널 반대편 타겟 설정 */}
|
||||
{config.action?.dataTransfer?.targetType === "splitPanel" && (
|
||||
<div>
|
||||
<Label>
|
||||
타겟 컴포넌트 ID (선택사항)
|
||||
</Label>
|
||||
<Input
|
||||
id="target-component-id"
|
||||
placeholder="예: table-list-2"
|
||||
value={config.action?.dataTransfer?.targetComponentId || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value)}
|
||||
placeholder="비워두면 첫 번째 수신 가능 컴포넌트로 전달"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
데이터를 받을 컴포넌트의 ID
|
||||
반대편 화면의 특정 컴포넌트 ID를 지정하거나, 비워두면 자동으로 첫 번째 수신 가능 컴포넌트로 전달됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1748,15 +1901,382 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>추가 데이터 소스 (선택사항)</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
조건부 컨테이너의 카테고리 값 등 추가 데이터를 함께 전달할 수 있습니다
|
||||
</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-[10px] text-muted-foreground">({compType})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
조건부 컨테이너, 셀렉트박스 등 (카테고리 값 전달용)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="additional-field-name" className="text-xs">
|
||||
필드명 (선택사항)
|
||||
</Label>
|
||||
<Input
|
||||
id="additional-field-name"
|
||||
placeholder="예: inbound_type (비워두면 전체 데이터)"
|
||||
value={config.action?.dataTransfer?.additionalSources?.[0]?.fieldName || ""}
|
||||
onChange={(e) => {
|
||||
const currentSources = config.action?.dataTransfer?.additionalSources || [];
|
||||
const newSources = [...currentSources];
|
||||
if (newSources.length === 0) {
|
||||
newSources.push({ componentId: "", fieldName: e.target.value });
|
||||
} else {
|
||||
newSources[0] = { ...newSources[0], fieldName: e.target.value };
|
||||
}
|
||||
onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources);
|
||||
}}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
타겟 테이블에 저장될 필드명
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필드 매핑 규칙 */}
|
||||
<div className="space-y-3">
|
||||
<Label>필드 매핑 설정</Label>
|
||||
|
||||
{/* 소스/타겟 테이블 선택 */}
|
||||
<div className="grid grid-cols-2 gap-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"
|
||||
>
|
||||
{config.action?.dataTransfer?.sourceTable
|
||||
? availableTables.find((t) => t.name === config.action?.dataTransfer?.sourceTable)?.label ||
|
||||
config.action?.dataTransfer?.sourceTable
|
||||
: "테이블 선택"}
|
||||
<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.sourceTable", table.name);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.action?.dataTransfer?.sourceTable === table.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">{table.label}</span>
|
||||
<span className="ml-1 text-muted-foreground">({table.name})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<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="ml-1 text-muted-foreground">({table.name})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</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 currentRules = config.action?.dataTransfer?.mappingRules || [];
|
||||
const newRule = { sourceField: "", targetField: "", transform: "" };
|
||||
onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", [...currentRules, newRule]);
|
||||
}}
|
||||
disabled={!config.action?.dataTransfer?.sourceTable || !config.action?.dataTransfer?.targetTable}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
소스 필드를 타겟 필드에 매핑합니다. 비워두면 같은 이름의 필드로 자동 매핑됩니다.
|
||||
</p>
|
||||
|
||||
{(!config.action?.dataTransfer?.sourceTable || !config.action?.dataTransfer?.targetTable) ? (
|
||||
<div className="rounded-md border border-dashed p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
먼저 소스 테이블과 타겟 테이블을 선택하세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (config.action?.dataTransfer?.mappingRules || []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
매핑 규칙이 없습니다. 같은 이름의 필드로 자동 매핑됩니다.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(config.action?.dataTransfer?.mappingRules || []).map((rule: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded-md border bg-background p-2">
|
||||
{/* 소스 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={mappingSourcePopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-7 w-full justify-between text-xs"
|
||||
>
|
||||
{rule.sourceField
|
||||
? mappingSourceColumns.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"
|
||||
value={mappingSourceSearch[index] || ""}
|
||||
onValueChange={(value) => setMappingSourceSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{mappingSourceColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.label} ${col.name}`}
|
||||
onSelect={() => {
|
||||
const rules = [...(config.action?.dataTransfer?.mappingRules || [])];
|
||||
rules[index] = { ...rules[index], sourceField: col.name };
|
||||
onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules);
|
||||
setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: 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="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
|
||||
{/* 타겟 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={mappingTargetPopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: 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"
|
||||
value={mappingTargetSearch[index] || ""}
|
||||
onValueChange={(value) => setMappingTargetSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<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 rules = [...(config.action?.dataTransfer?.mappingRules || [])];
|
||||
rules[index] = { ...rules[index], targetField: col.name };
|
||||
onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules);
|
||||
setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: 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="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
const rules = [...(config.action?.dataTransfer?.mappingRules || [])];
|
||||
rules.splice(index, 1);
|
||||
onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</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. 이 버튼을 클릭하면 선택된 데이터가 타겟으로 전달됩니다
|
||||
2. 필드 매핑 규칙을 설정합니다 (예: 품번 → 품목코드)
|
||||
<br />
|
||||
3. 매핑 규칙은 추후 고급 설정에서 추가 예정입니다
|
||||
3. 이 버튼을 클릭하면 매핑된 데이터가 타겟으로 전달됩니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user