jskim-node #419

Merged
kjs merged 15 commits from jskim-node into main 2026-03-17 09:56:34 +09:00
3 changed files with 703 additions and 5 deletions
Showing only changes of commit b4a5fb9aa3 - Show all commits

View File

@ -102,7 +102,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
const fetchScreens = async () => {
try {
setScreensLoading(true);
const response = await apiClient.get("/screen-management/screens");
const response = await apiClient.get("/screen-management/screens?size=1000");
if (response.data.success && Array.isArray(response.data.data)) {
const screenList = response.data.data.map((screen: any) => ({

View File

@ -269,6 +269,13 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
const [modalScreenOpen, setModalScreenOpen] = useState(false);
const [modalSearchTerm, setModalSearchTerm] = useState("");
// 데이터 전달 필드 매핑 관련
const [availableTables, setAvailableTables] = useState<Array<{ name: string; label: string }>>([]);
const [mappingSourceColumnsMap, setMappingSourceColumnsMap] = useState<Record<string, Array<{ name: string; label: string }>>>({});
const [mappingTargetColumns, setMappingTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
const [fieldMappingOpen, setFieldMappingOpen] = useState(false);
const [activeMappingGroupIndex, setActiveMappingGroupIndex] = useState(0);
const showIconSettings = displayMode === "icon" || displayMode === "icon-text";
const currentActionIcons = actionIconMap[actionType] || [];
const isNoIconAction = noIconActions.has(actionType);
@ -330,6 +337,76 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
setIconSize(config.icon?.size || "보통");
}, [config.icon?.name, config.icon?.type, config.icon?.size]);
// 테이블 목록 로드 (데이터 전달 액션용)
useEffect(() => {
if (actionType !== "transferData") return;
if (availableTables.length > 0) return;
const loadTables = async () => {
try {
const response = await apiClient.get("/table-management/tables");
if (response.data.success && response.data.data) {
const tables = response.data.data.map((t: any) => ({
name: t.tableName || t.name,
label: t.displayName || t.tableLabel || t.label || t.tableName || t.name,
}));
setAvailableTables(tables);
}
} catch {
setAvailableTables([]);
}
};
loadTables();
}, [actionType, availableTables.length]);
// 테이블 컬럼 로드 헬퍼
const loadTableColumns = useCallback(async (tableName: string): Promise<Array<{ name: string; label: string }>> => {
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/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)) {
return columnData.map((col: any) => ({
name: col.name || col.columnName,
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
}));
}
}
} catch { /* ignore */ }
return [];
}, []);
// 멀티 테이블 매핑: 소스/타겟 테이블 컬럼 로드
useEffect(() => {
if (actionType !== "transferData") return;
const multiTableMappings: Array<{ sourceTable: string }> = config.action?.dataTransfer?.multiTableMappings || [];
const targetTable = config.action?.dataTransfer?.targetTable;
const loadAll = async () => {
const sourceTableNames = multiTableMappings.map((m) => m.sourceTable).filter(Boolean);
const newMap: Record<string, Array<{ name: string; label: string }>> = {};
for (const tbl of sourceTableNames) {
if (!mappingSourceColumnsMap[tbl]) {
newMap[tbl] = await loadTableColumns(tbl);
}
}
if (Object.keys(newMap).length > 0) {
setMappingSourceColumnsMap((prev) => ({ ...prev, ...newMap }));
}
if (targetTable) {
const cols = await loadTableColumns(targetTable);
setMappingTargetColumns(cols);
} else {
setMappingTargetColumns([]);
}
};
loadAll();
}, [actionType, config.action?.dataTransfer?.multiTableMappings, config.action?.dataTransfer?.targetTable, loadTableColumns]);
// 화면 목록 로드 (모달 액션용)
useEffect(() => {
if (actionType !== "modal" && actionType !== "navigate") return;
@ -338,7 +415,7 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
const loadScreens = async () => {
setScreensLoading(true);
try {
const response = await apiClient.get("/screen-management/screens");
const response = await apiClient.get("/screen-management/screens?size=1000");
if (response.data.success && response.data.data) {
const screenList = response.data.data.map((s: any) => ({
id: s.id || s.screenId,
@ -521,6 +598,8 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
modalSearchTerm={modalSearchTerm}
setModalSearchTerm={setModalSearchTerm}
currentTableName={effectiveTableName}
allComponents={allComponents}
handleUpdateProperty={handleUpdateProperty}
/>
{/* ─── 아이콘 설정 (접기) ─── */}
@ -657,6 +736,26 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
)}
</div>
{/* 데이터 전달 필드 매핑 (transferData 액션 전용) */}
{actionType === "transferData" && (
<>
<Separator />
<TransferDataFieldMappingSection
config={config}
onChange={onChange}
availableTables={availableTables}
mappingSourceColumnsMap={mappingSourceColumnsMap}
setMappingSourceColumnsMap={setMappingSourceColumnsMap}
mappingTargetColumns={mappingTargetColumns}
fieldMappingOpen={fieldMappingOpen}
setFieldMappingOpen={setFieldMappingOpen}
activeMappingGroupIndex={activeMappingGroupIndex}
setActiveMappingGroupIndex={setActiveMappingGroupIndex}
loadTableColumns={loadTableColumns}
/>
</>
)}
{/* 제어 기능 */}
{actionType !== "excel_upload" && actionType !== "multi_table_excel_upload" && (
<>
@ -699,6 +798,8 @@ const ActionDetailSection: React.FC<{
modalSearchTerm: string;
setModalSearchTerm: (term: string) => void;
currentTableName?: string;
allComponents?: ComponentData[];
handleUpdateProperty?: (path: string, value: any) => void;
}> = ({
actionType,
config,
@ -711,6 +812,8 @@ const ActionDetailSection: React.FC<{
modalSearchTerm,
setModalSearchTerm,
currentTableName,
allComponents = [],
handleUpdateProperty,
}) => {
const action = config.action || {};
@ -800,7 +903,7 @@ const ActionDetailSection: React.FC<{
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<Command shouldFilter={false}>
<CommandInput
placeholder="화면 검색..."
value={modalSearchTerm}
@ -812,8 +915,10 @@ const ActionDetailSection: React.FC<{
<CommandGroup>
{screens
.filter((s) =>
!modalSearchTerm ||
s.name.toLowerCase().includes(modalSearchTerm.toLowerCase()) ||
s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase())
s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase()) ||
String(s.id).includes(modalSearchTerm)
)
.map((screen) => (
<CommandItem
@ -951,6 +1056,190 @@ const ActionDetailSection: React.FC<{
</div>
);
case "transferData":
return (
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2">
<SendHorizontal className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> </span>
</div>
{/* 소스 컴포넌트 선택 */}
<div>
<Label className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={action.dataTransfer?.sourceComponentId || ""}
onValueChange={(v) => {
const dt = { ...action.dataTransfer, sourceComponentId: v };
updateActionConfig("dataTransfer", dt);
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="데이터를 가져올 컴포넌트 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__auto__">
<span className="text-xs font-medium"> ( )</span>
</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;
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>
</div>
{/* 타겟 타입 */}
<div>
<Label className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={action.dataTransfer?.targetType || "component"}
onValueChange={(v) => {
const dt = { ...action.dataTransfer, targetType: v };
updateActionConfig("dataTransfer", dt);
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="component"> </SelectItem>
<SelectItem value="splitPanel"> </SelectItem>
</SelectContent>
</Select>
</div>
{/* 타겟 컴포넌트 선택 */}
{action.dataTransfer?.targetType === "component" && (
<div>
<Label className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={action.dataTransfer?.targetComponentId || ""}
onValueChange={(v) => {
const dt = { ...action.dataTransfer, targetComponentId: v };
updateActionConfig("dataTransfer", dt);
}}
>
<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 !== 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-muted-foreground text-[10px]">({compType})</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
{/* 데이터 전달 모드 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={action.dataTransfer?.mode || "append"}
onValueChange={(v) => {
const dt = { ...action.dataTransfer, mode: v };
updateActionConfig("dataTransfer", dt);
}}
>
<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>
</div>
{/* 전달 후 초기화 */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium"> </p>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<Switch
checked={action.dataTransfer?.clearAfterTransfer || false}
onCheckedChange={(checked) => {
const dt = { ...action.dataTransfer, clearAfterTransfer: checked };
updateActionConfig("dataTransfer", dt);
}}
/>
</div>
{/* 전달 전 확인 */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium"> </p>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<Switch
checked={action.dataTransfer?.confirmBeforeTransfer || false}
onCheckedChange={(checked) => {
const dt = { ...action.dataTransfer, confirmBeforeTransfer: checked };
updateActionConfig("dataTransfer", dt);
}}
/>
</div>
{action.dataTransfer?.confirmBeforeTransfer && (
<div>
<Label className="text-xs"> </Label>
<Input
value={action.dataTransfer?.confirmMessage || ""}
onChange={(e) => {
const dt = { ...action.dataTransfer, confirmMessage: e.target.value };
updateActionConfig("dataTransfer", dt);
}}
placeholder="선택한 항목을 전달하시겠습니까?"
className="h-7 text-xs"
/>
</div>
)}
{commonMessageSection}
</div>
);
case "event":
return (
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
@ -1373,6 +1662,386 @@ const IconSettingsSection: React.FC<{
);
};
// ─── 데이터 전달 필드 매핑 서브 컴포넌트 (고급 설정 내부) ───
const TransferDataFieldMappingSection: React.FC<{
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
availableTables: Array<{ name: string; label: string }>;
mappingSourceColumnsMap: Record<string, Array<{ name: string; label: string }>>;
setMappingSourceColumnsMap: React.Dispatch<React.SetStateAction<Record<string, Array<{ name: string; label: string }>>>>;
mappingTargetColumns: Array<{ name: string; label: string }>;
fieldMappingOpen: boolean;
setFieldMappingOpen: (open: boolean) => void;
activeMappingGroupIndex: number;
setActiveMappingGroupIndex: (index: number) => void;
loadTableColumns: (tableName: string) => Promise<Array<{ name: string; label: string }>>;
}> = ({
config,
onChange,
availableTables,
mappingSourceColumnsMap,
setMappingSourceColumnsMap,
mappingTargetColumns,
activeMappingGroupIndex,
setActiveMappingGroupIndex,
loadTableColumns,
}) => {
const [sourcePopoverOpen, setSourcePopoverOpen] = useState<Record<string, boolean>>({});
const [targetPopoverOpen, setTargetPopoverOpen] = useState<Record<string, boolean>>({});
const dataTransfer = config.action?.dataTransfer || {};
const multiTableMappings: Array<{ sourceTable: string; mappingRules: Array<{ sourceField: string; targetField: string }> }> =
dataTransfer.multiTableMappings || [];
const updateDataTransfer = (field: string, value: any) => {
const currentAction = config.action || {};
const currentDt = currentAction.dataTransfer || {};
onChange({
...config,
action: {
...currentAction,
dataTransfer: { ...currentDt, [field]: value },
},
});
};
const activeGroup = multiTableMappings[activeMappingGroupIndex];
const activeSourceTable = activeGroup?.sourceTable || "";
const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || [];
const activeRules = activeGroup?.mappingRules || [];
const updateGroupField = (field: string, value: any) => {
const mappings = [...multiTableMappings];
mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value };
updateDataTransfer("multiTableMappings", mappings);
};
return (
<div className="space-y-3">
<div className="space-y-0.5">
<p className="text-sm font-medium"> </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
{/* 타겟 테이블 (공통) */}
<div>
<Label className="text-xs"> ()</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{dataTransfer.targetTable
? availableTables.find((t) => t.name === dataTransfer.targetTable)?.label ||
dataTransfer.targetTable
: "타겟 테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 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" />
<CommandList className="max-h-48">
<CommandEmpty className="py-3 text-xs"> </CommandEmpty>
<CommandGroup>
{availableTables.map((table) => (
<CommandItem
key={table.name}
value={`${table.label} ${table.name}`}
onSelect={() => updateDataTransfer("targetTable", table.name)}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", dataTransfer.targetTable === table.name ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{table.label}</span>
{table.label !== table.name && <span className="text-[10px] text-muted-foreground">{table.name}</span>}
</div>
</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-7 text-xs"
onClick={() => {
updateDataTransfer("multiTableMappings", [
...multiTableMappings,
{ sourceTable: "", mappingRules: [] },
]);
setActiveMappingGroupIndex(multiTableMappings.length);
}}
disabled={!dataTransfer.targetTable}
>
<Plus className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
{!dataTransfer.targetTable ? (
<div className="rounded-lg border border-dashed p-4 text-center">
<p className="text-xs text-muted-foreground"> </p>
</div>
) : multiTableMappings.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center">
<p className="text-xs text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-3">
{/* 그룹 탭 */}
<div className="flex flex-wrap gap-1.5">
{multiTableMappings.map((group, gIdx) => (
<div key={gIdx} className="flex items-center">
<Button
type="button"
variant={activeMappingGroupIndex === gIdx ? "default" : "outline"}
size="sm"
className="h-7 text-xs rounded-r-none"
onClick={() => setActiveMappingGroupIndex(gIdx)}
>
{group.sourceTable
? availableTables.find((t) => t.name === group.sourceTable)?.label || group.sourceTable
: `그룹 ${gIdx + 1}`}
{group.mappingRules?.length > 0 && (
<span className="ml-1.5 rounded-full bg-primary-foreground/20 px-1.5 text-[10px]">
{group.mappingRules.length}
</span>
)}
</Button>
<Button
type="button"
variant={activeMappingGroupIndex === gIdx ? "default" : "outline"}
size="icon"
className="h-7 w-7 rounded-l-none border-l-0"
onClick={() => {
const mappings = [...multiTableMappings];
mappings.splice(gIdx, 1);
updateDataTransfer("multiTableMappings", mappings);
if (activeMappingGroupIndex >= mappings.length) {
setActiveMappingGroupIndex(Math.max(0, mappings.length - 1));
}
}}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
{/* 활성 그룹 편집 */}
{activeGroup && (
<div className="space-y-3 rounded-lg border p-3">
{/* 소스 테이블 선택 */}
<div>
<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="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-48">
<CommandEmpty className="py-3 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 loadTableColumns(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")} />
<div className="flex flex-col">
<span className="font-medium">{table.label}</span>
{table.label !== table.name && <span className="text-[10px] text-muted-foreground">{table.name}</span>}
</div>
</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-7 text-xs"
onClick={() => {
updateGroupField("mappingRules", [
...activeRules,
{ sourceField: "", targetField: "" },
]);
}}
disabled={!activeSourceTable}
>
<Plus className="mr-1 h-3.5 w-3.5" />
</Button>
</div>
{!activeSourceTable ? (
<p className="text-xs text-muted-foreground"> </p>
) : activeRules.length === 0 ? (
<p className="text-xs text-muted-foreground"> ( )</p>
) : (
<div className="space-y-2">
{activeRules.map((rule: any, rIdx: number) => {
const keyS = `${activeMappingGroupIndex}-${rIdx}-s`;
const keyT = `${activeMappingGroupIndex}-${rIdx}-t`;
return (
<div
key={rIdx}
className="grid items-center gap-1.5"
style={{ gridTemplateColumns: "1fr 16px 1fr 32px" }}
>
{/* 소스 필드 */}
<Popover
open={sourcePopoverOpen[keyS] || false}
onOpenChange={(open) => setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: open }))}
>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs overflow-hidden">
<span className="truncate">
{rule.sourceField
? activeSourceColumns.find((c) => c.name === rule.sourceField)?.label || rule.sourceField
: "소스 컬럼"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[220px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList className="max-h-48">
<CommandEmpty className="py-3 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);
setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: false }));
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", rule.sourceField === col.name ? "opacity-100" : "opacity-0")} />
<span className="font-medium">{col.label}</span>
{col.label !== col.name && <span className="ml-1 text-muted-foreground">({col.name})</span>}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<ArrowRight className="mx-auto h-4 w-4 text-muted-foreground" />
{/* 타겟 필드 */}
<Popover
open={targetPopoverOpen[keyT] || false}
onOpenChange={(open) => setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: open }))}
>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs overflow-hidden">
<span className="truncate">
{rule.targetField
? mappingTargetColumns.find((c) => c.name === rule.targetField)?.label || rule.targetField
: "타겟 컬럼"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[220px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList className="max-h-48">
<CommandEmpty className="py-3 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);
setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: false }));
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", rule.targetField === col.name ? "opacity-100" : "opacity-0")} />
<span className="font-medium">{col.label}</span>
{col.label !== col.name && <span className="ml-1 text-muted-foreground">({col.name})</span>}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* 삭제 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:bg-destructive/10"
onClick={() => {
const newRules = [...activeRules];
newRules.splice(rIdx, 1);
updateGroupField("mappingRules", newRules);
}}
>
<X className="h-4 w-4" />
</Button>
</div>
);
})}
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};
V2ButtonConfigPanel.displayName = "V2ButtonConfigPanel";
export default V2ButtonConfigPanel;

View File

@ -6566,7 +6566,36 @@ export class ButtonActionExecutor {
}
// dataTransfer 설정이 있는 경우
const { targetType, targetComponentId, targetScreenId, mappingRules, receiveMode } = dataTransfer;
const { targetType, targetComponentId, targetScreenId, receiveMode } = dataTransfer;
// multiTableMappings 우선: 소스 테이블에 맞는 매핑 규칙 선택
let mappingRules = dataTransfer.mappingRules;
const multiTableMappings = (dataTransfer as any).multiTableMappings as Array<{
sourceTable: string;
mappingRules: Array<{ sourceField: string; targetField: string }>;
}> | undefined;
if (multiTableMappings && multiTableMappings.length > 0) {
const sourceTableName = context.tableName || (dataTransfer as any).sourceTable;
const matchedGroup = multiTableMappings.find((g) => g.sourceTable === sourceTableName);
if (matchedGroup && matchedGroup.mappingRules?.length > 0) {
mappingRules = matchedGroup.mappingRules;
console.log("📋 [transferData] multiTableMappings 매핑 적용:", {
sourceTable: sourceTableName,
rules: matchedGroup.mappingRules,
});
} else if (!mappingRules || mappingRules.length === 0) {
// 매칭되는 그룹이 없고 기존 mappingRules도 없으면 첫 번째 그룹 사용
const fallback = multiTableMappings[0];
if (fallback?.mappingRules?.length > 0) {
mappingRules = fallback.mappingRules;
console.log("📋 [transferData] multiTableMappings 폴백 매핑 적용:", {
sourceTable: fallback.sourceTable,
rules: fallback.mappingRules,
});
}
}
}
if (targetType === "component" && targetComponentId) {
// 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행