삭제버튼 제어 동작하지 않던 오류 수정
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight, Database, Globe, Link2 } from "lucide-react";
|
||||
import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight, Database, Globe, Link2, Sparkles } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
|
||||
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections";
|
||||
import { getNumberingRules } from "@/lib/api/numberingRule";
|
||||
import type { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||
import type { InsertActionNodeData } from "@/types/node-editor";
|
||||
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections";
|
||||
|
||||
@@ -89,6 +91,11 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
const [apiHeaders, setApiHeaders] = useState<Record<string, string>>(data.apiHeaders || {});
|
||||
const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "");
|
||||
|
||||
// 🔥 채번 규칙 관련 상태
|
||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [numberingRulesLoading, setNumberingRulesLoading] = useState(false);
|
||||
const [mappingNumberingRulesOpenState, setMappingNumberingRulesOpenState] = useState<boolean[]>([]);
|
||||
|
||||
// 데이터 변경 시 로컬 상태 업데이트
|
||||
useEffect(() => {
|
||||
setDisplayName(data.displayName || data.targetTable);
|
||||
@@ -128,8 +135,33 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
useEffect(() => {
|
||||
setMappingSourceFieldsOpenState(new Array(fieldMappings.length).fill(false));
|
||||
setMappingTargetFieldsOpenState(new Array(fieldMappings.length).fill(false));
|
||||
setMappingNumberingRulesOpenState(new Array(fieldMappings.length).fill(false));
|
||||
}, [fieldMappings.length]);
|
||||
|
||||
// 🔥 채번 규칙 로딩 (자동 생성 사용 시)
|
||||
useEffect(() => {
|
||||
const loadNumberingRules = async () => {
|
||||
setNumberingRulesLoading(true);
|
||||
try {
|
||||
const response = await getNumberingRules();
|
||||
if (response.success && response.data) {
|
||||
setNumberingRules(response.data);
|
||||
console.log(`✅ 채번 규칙 ${response.data.length}개 로딩 완료`);
|
||||
} else {
|
||||
console.error("❌ 채번 규칙 로딩 실패:", response.error);
|
||||
setNumberingRules([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 채번 규칙 로딩 오류:", error);
|
||||
setNumberingRules([]);
|
||||
} finally {
|
||||
setNumberingRulesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadNumberingRules();
|
||||
}, []);
|
||||
|
||||
// 🔥 외부 테이블 변경 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
if (targetType === "external" && selectedExternalConnectionId && externalTargetTable) {
|
||||
@@ -540,6 +572,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
sourceField: null,
|
||||
targetField: "",
|
||||
staticValue: undefined,
|
||||
valueType: "source" as const, // 🔥 기본값: 소스 필드
|
||||
},
|
||||
];
|
||||
setFieldMappings(newMappings);
|
||||
@@ -548,6 +581,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
// Combobox 열림 상태 배열 초기화
|
||||
setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false));
|
||||
setMappingTargetFieldsOpenState(new Array(newMappings.length).fill(false));
|
||||
setMappingNumberingRulesOpenState(new Array(newMappings.length).fill(false));
|
||||
};
|
||||
|
||||
const handleRemoveMapping = (index: number) => {
|
||||
@@ -558,6 +592,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
// Combobox 열림 상태 배열도 업데이트
|
||||
setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false));
|
||||
setMappingTargetFieldsOpenState(new Array(newMappings.length).fill(false));
|
||||
setMappingNumberingRulesOpenState(new Array(newMappings.length).fill(false));
|
||||
};
|
||||
|
||||
const handleMappingChange = (index: number, field: string, value: any) => {
|
||||
@@ -586,6 +621,24 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
targetField: value,
|
||||
targetFieldLabel: targetColumn?.label_ko || targetColumn?.column_label || targetColumn?.displayName || value,
|
||||
};
|
||||
} else if (field === "valueType") {
|
||||
// 🔥 값 생성 유형 변경 시 관련 필드 초기화
|
||||
newMappings[index] = {
|
||||
...newMappings[index],
|
||||
valueType: value,
|
||||
// 유형 변경 시 다른 유형의 값 초기화
|
||||
...(value !== "source" && { sourceField: null, sourceFieldLabel: undefined }),
|
||||
...(value !== "static" && { staticValue: undefined }),
|
||||
...(value !== "autoGenerate" && { numberingRuleId: undefined, numberingRuleName: undefined }),
|
||||
};
|
||||
} else if (field === "numberingRuleId") {
|
||||
// 🔥 채번 규칙 선택 시 이름도 함께 저장
|
||||
const selectedRule = numberingRules.find((r) => r.ruleId === value);
|
||||
newMappings[index] = {
|
||||
...newMappings[index],
|
||||
numberingRuleId: value,
|
||||
numberingRuleName: selectedRule?.ruleName,
|
||||
};
|
||||
} else {
|
||||
newMappings[index] = {
|
||||
...newMappings[index],
|
||||
@@ -1165,54 +1218,203 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* 소스 필드 입력/선택 */}
|
||||
{/* 🔥 값 생성 유형 선택 */}
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">
|
||||
소스 필드
|
||||
{hasRestAPISource && <span className="ml-1 text-teal-600">(REST API - 직접 입력)</span>}
|
||||
</Label>
|
||||
{hasRestAPISource ? (
|
||||
// REST API 소스인 경우: 직접 입력
|
||||
<Label className="text-xs text-gray-600">값 생성 방식</Label>
|
||||
<div className="mt-1 grid grid-cols-3 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMappingChange(index, "valueType", "source")}
|
||||
className={cn(
|
||||
"rounded border px-2 py-1 text-xs transition-all",
|
||||
(mapping.valueType === "source" || !mapping.valueType)
|
||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
||||
: "border-gray-200 hover:border-gray-300",
|
||||
)}
|
||||
>
|
||||
소스 필드
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMappingChange(index, "valueType", "static")}
|
||||
className={cn(
|
||||
"rounded border px-2 py-1 text-xs transition-all",
|
||||
mapping.valueType === "static"
|
||||
? "border-orange-500 bg-orange-50 text-orange-700"
|
||||
: "border-gray-200 hover:border-gray-300",
|
||||
)}
|
||||
>
|
||||
고정값
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMappingChange(index, "valueType", "autoGenerate")}
|
||||
className={cn(
|
||||
"rounded border px-2 py-1 text-xs transition-all flex items-center justify-center gap-1",
|
||||
mapping.valueType === "autoGenerate"
|
||||
? "border-purple-500 bg-purple-50 text-purple-700"
|
||||
: "border-gray-200 hover:border-gray-300",
|
||||
)}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
자동생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🔥 소스 필드 입력/선택 (valueType === "source" 일 때만) */}
|
||||
{(mapping.valueType === "source" || !mapping.valueType) && (
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">
|
||||
소스 필드
|
||||
{hasRestAPISource && <span className="ml-1 text-teal-600">(REST API - 직접 입력)</span>}
|
||||
</Label>
|
||||
{hasRestAPISource ? (
|
||||
// REST API 소스인 경우: 직접 입력
|
||||
<Input
|
||||
value={mapping.sourceField || ""}
|
||||
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)}
|
||||
placeholder="필드명 입력 (예: userId, userName)"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
) : (
|
||||
// 일반 소스인 경우: Combobox 선택
|
||||
<Popover
|
||||
open={mappingSourceFieldsOpenState[index]}
|
||||
onOpenChange={(open) => {
|
||||
const newState = [...mappingSourceFieldsOpenState];
|
||||
newState[index] = open;
|
||||
setMappingSourceFieldsOpenState(newState);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={mappingSourceFieldsOpenState[index]}
|
||||
className="mt-1 h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
{mapping.sourceField
|
||||
? (() => {
|
||||
const field = sourceFields.find((f) => f.name === mapping.sourceField);
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 overflow-hidden">
|
||||
<span className="truncate font-medium">
|
||||
{field?.label || mapping.sourceField}
|
||||
</span>
|
||||
{field?.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: "소스 필드 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="소스 필드 검색..." className="text-xs sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">
|
||||
필드를 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sourceFields.map((field) => (
|
||||
<CommandItem
|
||||
key={field.name}
|
||||
value={field.name}
|
||||
onSelect={(currentValue) => {
|
||||
handleMappingChange(index, "sourceField", currentValue || null);
|
||||
const newState = [...mappingSourceFieldsOpenState];
|
||||
newState[index] = false;
|
||||
setMappingSourceFieldsOpenState(newState);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
mapping.sourceField === field.name ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-[10px]">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{hasRestAPISource && (
|
||||
<p className="mt-1 text-xs text-gray-500">API 응답 JSON의 필드명을 입력하세요</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🔥 고정값 입력 (valueType === "static" 일 때) */}
|
||||
{mapping.valueType === "static" && (
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">고정값</Label>
|
||||
<Input
|
||||
value={mapping.sourceField || ""}
|
||||
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)}
|
||||
placeholder="필드명 입력 (예: userId, userName)"
|
||||
value={mapping.staticValue || ""}
|
||||
onChange={(e) => handleMappingChange(index, "staticValue", e.target.value || undefined)}
|
||||
placeholder="고정값 입력"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
) : (
|
||||
// 일반 소스인 경우: Combobox 선택
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🔥 채번 규칙 선택 (valueType === "autoGenerate" 일 때) */}
|
||||
{mapping.valueType === "autoGenerate" && (
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">
|
||||
채번 규칙
|
||||
{numberingRulesLoading && <span className="ml-1 text-gray-400">(로딩 중...)</span>}
|
||||
</Label>
|
||||
<Popover
|
||||
open={mappingSourceFieldsOpenState[index]}
|
||||
open={mappingNumberingRulesOpenState[index]}
|
||||
onOpenChange={(open) => {
|
||||
const newState = [...mappingSourceFieldsOpenState];
|
||||
const newState = [...mappingNumberingRulesOpenState];
|
||||
newState[index] = open;
|
||||
setMappingSourceFieldsOpenState(newState);
|
||||
setMappingNumberingRulesOpenState(newState);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={mappingSourceFieldsOpenState[index]}
|
||||
aria-expanded={mappingNumberingRulesOpenState[index]}
|
||||
className="mt-1 h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||
disabled={numberingRulesLoading || numberingRules.length === 0}
|
||||
>
|
||||
{mapping.sourceField
|
||||
{mapping.numberingRuleId
|
||||
? (() => {
|
||||
const field = sourceFields.find((f) => f.name === mapping.sourceField);
|
||||
const rule = numberingRules.find((r) => r.ruleId === mapping.numberingRuleId);
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 overflow-hidden">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Sparkles className="h-3 w-3 text-purple-500" />
|
||||
<span className="truncate font-medium">
|
||||
{field?.label || mapping.sourceField}
|
||||
{rule?.ruleName || mapping.numberingRuleName || mapping.numberingRuleId}
|
||||
</span>
|
||||
{field?.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: "소스 필드 선택"}
|
||||
: "채번 규칙 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -1222,37 +1424,36 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="소스 필드 검색..." className="text-xs sm:text-sm" />
|
||||
<CommandInput placeholder="채번 규칙 검색..." className="text-xs sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">
|
||||
필드를 찾을 수 없습니다.
|
||||
채번 규칙을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sourceFields.map((field) => (
|
||||
{numberingRules.map((rule) => (
|
||||
<CommandItem
|
||||
key={field.name}
|
||||
value={field.name}
|
||||
key={rule.ruleId}
|
||||
value={rule.ruleId}
|
||||
onSelect={(currentValue) => {
|
||||
handleMappingChange(index, "sourceField", currentValue || null);
|
||||
const newState = [...mappingSourceFieldsOpenState];
|
||||
handleMappingChange(index, "numberingRuleId", currentValue);
|
||||
const newState = [...mappingNumberingRulesOpenState];
|
||||
newState[index] = false;
|
||||
setMappingSourceFieldsOpenState(newState);
|
||||
setMappingNumberingRulesOpenState(newState);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
mapping.sourceField === field.name ? "opacity-100" : "opacity-0",
|
||||
mapping.numberingRuleId === rule.ruleId ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-[10px]">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="font-medium">{rule.ruleName}</span>
|
||||
<span className="text-muted-foreground font-mono text-[10px]">
|
||||
{rule.ruleId}
|
||||
{rule.tableName && ` - ${rule.tableName}`}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -1261,11 +1462,13 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{hasRestAPISource && (
|
||||
<p className="mt-1 text-xs text-gray-500">API 응답 JSON의 필드명을 입력하세요</p>
|
||||
)}
|
||||
</div>
|
||||
{numberingRules.length === 0 && !numberingRulesLoading && (
|
||||
<p className="mt-1 text-xs text-orange-600">
|
||||
등록된 채번 규칙이 없습니다. 시스템 관리에서 먼저 채번 규칙을 생성하세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<ArrowRight className="h-4 w-4 text-green-600" />
|
||||
@@ -1400,18 +1603,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 정적 값 */}
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">정적 값 (선택)</Label>
|
||||
<Input
|
||||
value={mapping.staticValue || ""}
|
||||
onChange={(e) => handleMappingChange(index, "staticValue", e.target.value || undefined)}
|
||||
placeholder="소스 필드 대신 고정 값 사용"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-400">소스 필드가 비어있을 때만 사용됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1428,9 +1619,8 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
|
||||
{/* 안내 */}
|
||||
<div className="rounded bg-green-50 p-3 text-xs text-green-700">
|
||||
✅ 테이블과 필드는 실제 데이터베이스에서 조회됩니다.
|
||||
<br />
|
||||
💡 소스 필드가 없으면 정적 값이 사용됩니다.
|
||||
<p>테이블과 필드는 실제 데이터베이스에서 조회됩니다.</p>
|
||||
<p className="mt-1">값 생성 방식: 소스 필드(입력값 연결) / 고정값(직접 입력) / 자동생성(채번 규칙)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user