659 lines
29 KiB
TypeScript
659 lines
29 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } 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 { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Check, ChevronsUpDown, Plus, X, Search } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { ComponentData } from "@/types/screen";
|
|
import { QuickInsertConfig, QuickInsertColumnMapping } from "@/types/screen-management";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface QuickInsertConfigSectionProps {
|
|
component: ComponentData;
|
|
onUpdateProperty: (path: string, value: any) => void;
|
|
allComponents?: ComponentData[];
|
|
currentTableName?: string;
|
|
}
|
|
|
|
interface TableOption {
|
|
name: string;
|
|
label: string;
|
|
}
|
|
|
|
interface ColumnOption {
|
|
name: string;
|
|
label: string;
|
|
}
|
|
|
|
export const QuickInsertConfigSection: React.FC<QuickInsertConfigSectionProps> = ({
|
|
component,
|
|
onUpdateProperty,
|
|
allComponents = [],
|
|
currentTableName,
|
|
}) => {
|
|
// 현재 설정 가져오기
|
|
const config: QuickInsertConfig = component.componentConfig?.action?.quickInsertConfig || {
|
|
targetTable: "",
|
|
columnMappings: [],
|
|
afterInsert: {
|
|
refreshData: true,
|
|
clearComponents: [],
|
|
showSuccessMessage: true,
|
|
successMessage: "저장되었습니다.",
|
|
},
|
|
duplicateCheck: {
|
|
enabled: false,
|
|
columns: [],
|
|
errorMessage: "이미 존재하는 데이터입니다.",
|
|
},
|
|
};
|
|
|
|
// 테이블 목록 상태
|
|
const [tables, setTables] = useState<TableOption[]>([]);
|
|
const [tablesLoading, setTablesLoading] = useState(false);
|
|
const [tablePopoverOpen, setTablePopoverOpen] = useState(false);
|
|
const [tableSearch, setTableSearch] = useState("");
|
|
|
|
// 대상 테이블 컬럼 목록 상태
|
|
const [targetColumns, setTargetColumns] = useState<ColumnOption[]>([]);
|
|
const [targetColumnsLoading, setTargetColumnsLoading] = useState(false);
|
|
|
|
// 매핑별 Popover 상태
|
|
const [targetColumnPopoverOpen, setTargetColumnPopoverOpen] = useState<Record<number, boolean>>({});
|
|
const [targetColumnSearch, setTargetColumnSearch] = useState<Record<number, string>>({});
|
|
const [sourceComponentPopoverOpen, setSourceComponentPopoverOpen] = useState<Record<number, boolean>>({});
|
|
const [sourceComponentSearch, setSourceComponentSearch] = useState<Record<number, string>>({});
|
|
|
|
// 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setTablesLoading(true);
|
|
try {
|
|
const response = await apiClient.get("/table-management/tables");
|
|
if (response.data?.success && response.data?.data) {
|
|
setTables(
|
|
response.data.data.map((t: any) => ({
|
|
name: t.tableName,
|
|
label: t.displayName || t.tableName,
|
|
}))
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
} finally {
|
|
setTablesLoading(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 대상 테이블 선택 시 컬럼 로드
|
|
useEffect(() => {
|
|
const loadTargetColumns = async () => {
|
|
if (!config.targetTable) {
|
|
setTargetColumns([]);
|
|
return;
|
|
}
|
|
|
|
setTargetColumnsLoading(true);
|
|
try {
|
|
const response = await apiClient.get(`/table-management/tables/${config.targetTable}/columns`);
|
|
if (response.data?.success && response.data?.data) {
|
|
// columns가 배열인지 확인 (data.columns 또는 data 직접)
|
|
const columns = response.data.data.columns || response.data.data;
|
|
setTargetColumns(
|
|
(Array.isArray(columns) ? columns : []).map((col: any) => ({
|
|
name: col.columnName || col.column_name,
|
|
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
|
}))
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 목록 로드 실패:", error);
|
|
setTargetColumns([]);
|
|
} finally {
|
|
setTargetColumnsLoading(false);
|
|
}
|
|
};
|
|
loadTargetColumns();
|
|
}, [config.targetTable]);
|
|
|
|
// 설정 업데이트 헬퍼
|
|
const updateConfig = useCallback(
|
|
(updates: Partial<QuickInsertConfig>) => {
|
|
const newConfig = { ...config, ...updates };
|
|
onUpdateProperty("componentConfig.action.quickInsertConfig", newConfig);
|
|
},
|
|
[config, onUpdateProperty]
|
|
);
|
|
|
|
// 컬럼 매핑 추가
|
|
const addMapping = () => {
|
|
const newMapping: QuickInsertColumnMapping = {
|
|
targetColumn: "",
|
|
sourceType: "component",
|
|
sourceComponentId: "",
|
|
};
|
|
updateConfig({
|
|
columnMappings: [...(config.columnMappings || []), newMapping],
|
|
});
|
|
};
|
|
|
|
// 컬럼 매핑 삭제
|
|
const removeMapping = (index: number) => {
|
|
const newMappings = [...(config.columnMappings || [])];
|
|
newMappings.splice(index, 1);
|
|
updateConfig({ columnMappings: newMappings });
|
|
};
|
|
|
|
// 컬럼 매핑 업데이트
|
|
const updateMapping = (index: number, updates: Partial<QuickInsertColumnMapping>) => {
|
|
const newMappings = [...(config.columnMappings || [])];
|
|
newMappings[index] = { ...newMappings[index], ...updates };
|
|
updateConfig({ columnMappings: newMappings });
|
|
};
|
|
|
|
// 필터링된 테이블 목록
|
|
const filteredTables = tables.filter(
|
|
(t) =>
|
|
t.name.toLowerCase().includes(tableSearch.toLowerCase()) ||
|
|
t.label.toLowerCase().includes(tableSearch.toLowerCase())
|
|
);
|
|
|
|
// 컴포넌트 목록 (entity 타입 우선)
|
|
const availableComponents = allComponents.filter((comp: any) => {
|
|
// entity 타입 또는 select 타입 컴포넌트 필터링
|
|
const widgetType = comp.widgetType || comp.componentType || "";
|
|
return widgetType === "entity" || widgetType === "select" || widgetType === "text";
|
|
});
|
|
|
|
return (
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4 dark:bg-green-950/20">
|
|
<h4 className="text-sm font-medium text-foreground">즉시 저장 설정</h4>
|
|
<p className="text-xs text-muted-foreground">
|
|
화면에서 선택한 데이터를 버튼 클릭 시 특정 테이블에 즉시 저장합니다.
|
|
</p>
|
|
|
|
{/* 대상 테이블 선택 */}
|
|
<div>
|
|
<Label>대상 테이블 *</Label>
|
|
<Popover open={tablePopoverOpen} onOpenChange={setTablePopoverOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={tablePopoverOpen}
|
|
className="h-8 w-full justify-between text-xs"
|
|
disabled={tablesLoading}
|
|
>
|
|
{config.targetTable
|
|
? tables.find((t) => t.name === config.targetTable)?.label || config.targetTable
|
|
: "테이블을 선택하세요..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="테이블 검색..."
|
|
value={tableSearch}
|
|
onValueChange={setTableSearch}
|
|
className="text-xs"
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{filteredTables.map((table) => (
|
|
<CommandItem
|
|
key={table.name}
|
|
value={`${table.label} ${table.name}`}
|
|
onSelect={() => {
|
|
updateConfig({ targetTable: table.name, columnMappings: [] });
|
|
setTablePopoverOpen(false);
|
|
setTableSearch("");
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn("mr-2 h-4 w-4", config.targetTable === table.name ? "opacity-100" : "opacity-0")}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.label}</span>
|
|
<span className="text-[10px] text-muted-foreground">{table.name}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 컬럼 매핑 */}
|
|
{config.targetTable && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label>컬럼 매핑</Label>
|
|
<Button type="button" variant="outline" size="sm" onClick={addMapping} className="h-6 text-xs">
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
매핑 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(config.columnMappings || []).length === 0 ? (
|
|
<div className="rounded border-2 border-dashed py-4 text-center text-xs text-muted-foreground">
|
|
컬럼 매핑을 추가하세요
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{(config.columnMappings || []).map((mapping, index) => (
|
|
<Card key={index} className="p-3">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-medium">매핑 #{index + 1}</span>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeMapping(index)}
|
|
className="h-5 w-5 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 대상 컬럼 */}
|
|
<div>
|
|
<Label className="text-xs">대상 컬럼 (저장할 컬럼)</Label>
|
|
<Popover
|
|
open={targetColumnPopoverOpen[index] || false}
|
|
onOpenChange={(open) => setTargetColumnPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-7 w-full justify-between text-xs"
|
|
disabled={targetColumnsLoading}
|
|
>
|
|
{mapping.targetColumn
|
|
? targetColumns.find((c) => c.name === mapping.targetColumn)?.label || mapping.targetColumn
|
|
: "컬럼 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="컬럼 검색..."
|
|
value={targetColumnSearch[index] || ""}
|
|
onValueChange={(v) => setTargetColumnSearch((prev) => ({ ...prev, [index]: v }))}
|
|
className="text-xs"
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{targetColumns
|
|
.filter(
|
|
(c) =>
|
|
c.name.toLowerCase().includes((targetColumnSearch[index] || "").toLowerCase()) ||
|
|
c.label.toLowerCase().includes((targetColumnSearch[index] || "").toLowerCase())
|
|
)
|
|
.map((col) => (
|
|
<CommandItem
|
|
key={col.name}
|
|
value={`${col.label} ${col.name}`}
|
|
onSelect={() => {
|
|
updateMapping(index, { targetColumn: col.name });
|
|
setTargetColumnPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
|
setTargetColumnSearch((prev) => ({ ...prev, [index]: "" }));
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
mapping.targetColumn === col.name ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span>{col.label}</span>
|
|
<span className="text-[10px] text-muted-foreground">{col.name}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 값 소스 타입 */}
|
|
<div>
|
|
<Label className="text-xs">값 소스</Label>
|
|
<Select
|
|
value={mapping.sourceType}
|
|
onValueChange={(value: "component" | "leftPanel" | "fixed" | "currentUser") => {
|
|
updateMapping(index, {
|
|
sourceType: value,
|
|
sourceComponentId: undefined,
|
|
sourceColumn: undefined,
|
|
fixedValue: undefined,
|
|
userField: undefined,
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="component" className="text-xs">
|
|
컴포넌트 선택값
|
|
</SelectItem>
|
|
<SelectItem value="leftPanel" className="text-xs">
|
|
좌측 패널 선택 데이터
|
|
</SelectItem>
|
|
<SelectItem value="fixed" className="text-xs">
|
|
고정값
|
|
</SelectItem>
|
|
<SelectItem value="currentUser" className="text-xs">
|
|
현재 사용자 정보
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 소스 타입별 추가 설정 */}
|
|
{mapping.sourceType === "component" && (
|
|
<div>
|
|
<Label className="text-xs">소스 컴포넌트</Label>
|
|
<Popover
|
|
open={sourceComponentPopoverOpen[index] || false}
|
|
onOpenChange={(open) => setSourceComponentPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="h-7 w-full justify-between text-xs">
|
|
{mapping.sourceComponentId
|
|
? (() => {
|
|
const comp = allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
|
return comp?.label || comp?.columnName || mapping.sourceComponentId;
|
|
})()
|
|
: "컴포넌트 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="컴포넌트 검색..."
|
|
value={sourceComponentSearch[index] || ""}
|
|
onValueChange={(v) => setSourceComponentSearch((prev) => ({ ...prev, [index]: v }))}
|
|
className="text-xs"
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs">컴포넌트를 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{availableComponents
|
|
.filter((comp: any) => {
|
|
const search = (sourceComponentSearch[index] || "").toLowerCase();
|
|
const label = (comp.label || "").toLowerCase();
|
|
const colName = (comp.columnName || "").toLowerCase();
|
|
return label.includes(search) || colName.includes(search);
|
|
})
|
|
.map((comp: any) => (
|
|
<CommandItem
|
|
key={comp.id}
|
|
value={comp.id}
|
|
onSelect={() => {
|
|
// sourceComponentId와 함께 sourceColumnName도 저장 (formData 접근용)
|
|
updateMapping(index, {
|
|
sourceComponentId: comp.id,
|
|
sourceColumnName: comp.columnName || undefined,
|
|
});
|
|
setSourceComponentPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
|
setSourceComponentSearch((prev) => ({ ...prev, [index]: "" }));
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
mapping.sourceComponentId === comp.id ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span>{comp.label || comp.columnName || comp.id}</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{comp.widgetType || comp.componentType}
|
|
</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)}
|
|
|
|
{mapping.sourceType === "leftPanel" && (
|
|
<div>
|
|
<Label className="text-xs">좌측 패널 컬럼명</Label>
|
|
<Input
|
|
placeholder="예: process_code"
|
|
value={mapping.sourceColumn || ""}
|
|
onChange={(e) => updateMapping(index, { sourceColumn: e.target.value })}
|
|
className="h-7 text-xs"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
분할 패널 좌측에서 선택된 데이터의 컬럼명을 입력하세요
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{mapping.sourceType === "fixed" && (
|
|
<div>
|
|
<Label className="text-xs">고정값</Label>
|
|
<Input
|
|
placeholder="고정값 입력"
|
|
value={mapping.fixedValue || ""}
|
|
onChange={(e) => updateMapping(index, { fixedValue: e.target.value })}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{mapping.sourceType === "currentUser" && (
|
|
<div>
|
|
<Label className="text-xs">사용자 정보 필드</Label>
|
|
<Select
|
|
value={mapping.userField || ""}
|
|
onValueChange={(value: "userId" | "userName" | "companyCode" | "deptCode") => {
|
|
updateMapping(index, { userField: value });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="필드 선택..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="userId" className="text-xs">
|
|
사용자 ID
|
|
</SelectItem>
|
|
<SelectItem value="userName" className="text-xs">
|
|
사용자 이름
|
|
</SelectItem>
|
|
<SelectItem value="companyCode" className="text-xs">
|
|
회사 코드
|
|
</SelectItem>
|
|
<SelectItem value="deptCode" className="text-xs">
|
|
부서 코드
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 저장 후 동작 설정 */}
|
|
{config.targetTable && (
|
|
<div className="space-y-3 rounded border bg-background p-3">
|
|
<Label className="text-xs font-medium">저장 후 동작</Label>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-normal">데이터 새로고침</Label>
|
|
<Switch
|
|
checked={config.afterInsert?.refreshData ?? true}
|
|
onCheckedChange={(checked) => {
|
|
updateConfig({
|
|
afterInsert: { ...config.afterInsert, refreshData: checked },
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground -mt-2">
|
|
테이블리스트, 카드 디스플레이 컴포넌트를 새로고침합니다
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-normal">성공 메시지 표시</Label>
|
|
<Switch
|
|
checked={config.afterInsert?.showSuccessMessage ?? true}
|
|
onCheckedChange={(checked) => {
|
|
updateConfig({
|
|
afterInsert: { ...config.afterInsert, showSuccessMessage: checked },
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{config.afterInsert?.showSuccessMessage && (
|
|
<div>
|
|
<Label className="text-xs">성공 메시지</Label>
|
|
<Input
|
|
placeholder="저장되었습니다."
|
|
value={config.afterInsert?.successMessage || ""}
|
|
onChange={(e) => {
|
|
updateConfig({
|
|
afterInsert: { ...config.afterInsert, successMessage: e.target.value },
|
|
});
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 중복 체크 설정 */}
|
|
{config.targetTable && (
|
|
<div className="space-y-3 rounded border bg-background p-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium">중복 체크</Label>
|
|
<Switch
|
|
checked={config.duplicateCheck?.enabled ?? false}
|
|
onCheckedChange={(checked) => {
|
|
updateConfig({
|
|
duplicateCheck: { ...config.duplicateCheck, enabled: checked },
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{config.duplicateCheck?.enabled && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs">중복 체크 컬럼</Label>
|
|
<div className="mt-1 max-h-40 overflow-y-auto rounded border bg-background p-2">
|
|
{targetColumns.length === 0 ? (
|
|
<p className="text-[10px] text-muted-foreground">컬럼을 불러오는 중...</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{targetColumns.map((col) => {
|
|
const isChecked = (config.duplicateCheck?.columns || []).includes(col.name);
|
|
return (
|
|
<div
|
|
key={col.name}
|
|
className="flex cursor-pointer items-center gap-2 rounded px-1 py-0.5 hover:bg-muted"
|
|
onClick={() => {
|
|
const currentColumns = config.duplicateCheck?.columns || [];
|
|
const newColumns = isChecked
|
|
? currentColumns.filter((c) => c !== col.name)
|
|
: [...currentColumns, col.name];
|
|
updateConfig({
|
|
duplicateCheck: { ...config.duplicateCheck, columns: newColumns },
|
|
});
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isChecked}
|
|
onChange={() => {}}
|
|
className="h-3 w-3 flex-shrink-0"
|
|
/>
|
|
<span className="flex-1 text-xs whitespace-nowrap">
|
|
{col.label}{col.label !== col.name && ` (${col.name})`}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
선택한 컬럼들의 조합으로 중복 여부를 체크합니다
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs">중복 시 에러 메시지</Label>
|
|
<Input
|
|
placeholder="이미 존재하는 데이터입니다."
|
|
value={config.duplicateCheck?.errorMessage || ""}
|
|
onChange={(e) => {
|
|
updateConfig({
|
|
duplicateCheck: { ...config.duplicateCheck, errorMessage: e.target.value },
|
|
});
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 사용 안내 */}
|
|
<div className="rounded-md bg-green-100 p-3 dark:bg-green-900/30">
|
|
<p className="text-xs text-green-900 dark:text-green-100">
|
|
<strong>사용 방법:</strong>
|
|
<br />
|
|
1. 저장할 대상 테이블을 선택합니다
|
|
<br />
|
|
2. 컬럼 매핑을 추가하여 각 컬럼에 어떤 값을 저장할지 설정합니다
|
|
<br />
|
|
3. 버튼 클릭 시 설정된 값들이 대상 테이블에 즉시 저장됩니다
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QuickInsertConfigSection;
|
|
|