삭제버튼 제어 동작하지 않던 오류 수정

This commit is contained in:
kjs 2026-01-09 13:43:14 +09:00
parent 80cf20e142
commit ee3a648917
9 changed files with 400 additions and 116 deletions

View File

@ -231,7 +231,7 @@ export const deleteFormData = async (
try { try {
const { id } = req.params; const { id } = req.params;
const { companyCode, userId } = req.user as any; const { companyCode, userId } = req.user as any;
const { tableName } = req.body; const { tableName, screenId } = req.body;
if (!tableName) { if (!tableName) {
return res.status(400).json({ return res.status(400).json({
@ -240,7 +240,16 @@ export const deleteFormData = async (
}); });
} }
await dynamicFormService.deleteFormData(id, tableName, companyCode, userId); // userId 추가 // screenId를 숫자로 변환 (문자열로 전달될 수 있음)
const parsedScreenId = screenId ? parseInt(screenId, 10) : undefined;
await dynamicFormService.deleteFormData(
id,
tableName,
companyCode,
userId,
parsedScreenId // screenId 추가 (제어관리 실행용)
);
res.json({ res.json({
success: true, success: true,

View File

@ -1192,12 +1192,18 @@ export class DynamicFormService {
/** /**
* ( ) * ( )
* @param id ID
* @param tableName
* @param companyCode
* @param userId ID
* @param screenId ID ( , )
*/ */
async deleteFormData( async deleteFormData(
id: string | number, id: string | number,
tableName: string, tableName: string,
companyCode?: string, companyCode?: string,
userId?: string userId?: string,
screenId?: number
): Promise<void> { ): Promise<void> {
try { try {
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", { console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
@ -1310,14 +1316,19 @@ export class DynamicFormService {
const recordCompanyCode = const recordCompanyCode =
deletedRecord?.company_code || companyCode || "*"; deletedRecord?.company_code || companyCode || "*";
await this.executeDataflowControlIfConfigured( // screenId가 전달되지 않으면 제어관리를 실행하지 않음
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요) if (screenId && screenId > 0) {
tableName, await this.executeDataflowControlIfConfigured(
deletedRecord, screenId,
"delete", tableName,
userId || "system", deletedRecord,
recordCompanyCode "delete",
); userId || "system",
recordCompanyCode
);
} else {
console.log(" screenId가 전달되지 않아 제어관리를 건너뜁니다. (screenId:", screenId, ")");
}
} }
} catch (controlError) { } catch (controlError) {
console.error("⚠️ 제어관리 실행 오류:", controlError); console.error("⚠️ 제어관리 실행 오류:", controlError);
@ -1662,10 +1673,16 @@ export class DynamicFormService {
!!properties?.webTypeConfig?.dataflowConfig?.flowControls, !!properties?.webTypeConfig?.dataflowConfig?.flowControls,
}); });
// 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우 // 버튼 컴포넌트이고 제어관리가 활성화된 경우
// triggerType에 맞는 액션 타입 매칭: insert/update -> save, delete -> delete
const buttonActionType = properties?.componentConfig?.action?.type;
const isMatchingAction =
(triggerType === "delete" && buttonActionType === "delete") ||
((triggerType === "insert" || triggerType === "update") && buttonActionType === "save");
if ( if (
properties?.componentType === "button-primary" && properties?.componentType === "button-primary" &&
properties?.componentConfig?.action?.type === "save" && isMatchingAction &&
properties?.webTypeConfig?.enableDataflowControl === true properties?.webTypeConfig?.enableDataflowControl === true
) { ) {
const dataflowConfig = properties?.webTypeConfig?.dataflowConfig; const dataflowConfig = properties?.webTypeConfig?.dataflowConfig;

View File

@ -969,21 +969,56 @@ export class NodeFlowExecutionService {
const insertedData = { ...data }; const insertedData = { ...data };
console.log("🗺️ 필드 매핑 처리 중..."); console.log("🗺️ 필드 매핑 처리 중...");
fieldMappings.forEach((mapping: any) => {
// 🔥 채번 규칙 서비스 동적 import
const { numberingRuleService } = await import("./numberingRuleService");
for (const mapping of fieldMappings) {
fields.push(mapping.targetField); fields.push(mapping.targetField);
const value = let value: any;
mapping.staticValue !== undefined
? mapping.staticValue // 🔥 값 생성 유형에 따른 처리
: data[mapping.sourceField]; const valueType = mapping.valueType || (mapping.staticValue !== undefined ? "static" : "source");
console.log( if (valueType === "autoGenerate" && mapping.numberingRuleId) {
` ${mapping.sourceField}${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}` // 자동 생성 (채번 규칙)
); const companyCode = context.buttonContext?.companyCode || "*";
try {
value = await numberingRuleService.allocateCode(
mapping.numberingRuleId,
companyCode
);
console.log(
` 🔢 자동 생성(채번): ${mapping.targetField} = ${value} (규칙: ${mapping.numberingRuleId})`
);
} catch (error: any) {
logger.error(`채번 규칙 적용 실패: ${error.message}`);
console.error(
` ❌ 채번 실패 → ${mapping.targetField}: ${error.message}`
);
throw new Error(
`채번 규칙 '${mapping.numberingRuleName || mapping.numberingRuleId}' 적용 실패: ${error.message}`
);
}
} else if (valueType === "static" || mapping.staticValue !== undefined) {
// 고정값
value = mapping.staticValue;
console.log(
` 📌 고정값: ${mapping.targetField} = ${value}`
);
} else {
// 소스 필드
value = data[mapping.sourceField];
console.log(
` ${mapping.sourceField}${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
);
}
values.push(value); values.push(value);
// 🔥 삽입된 값을 데이터에 반영 // 🔥 삽입된 값을 데이터에 반영
insertedData[mapping.targetField] = value; insertedData[mapping.targetField] = value;
}); }
// 🆕 writer와 company_code 자동 추가 (필드 매핑에 없는 경우) // 🆕 writer와 company_code 자동 추가 (필드 매핑에 없는 경우)
const hasWriterMapping = fieldMappings.some( const hasWriterMapping = fieldMappings.some(
@ -1528,16 +1563,24 @@ export class NodeFlowExecutionService {
} }
}); });
// 🔑 Primary Key 자동 추가 (context-data 모드) // 🔑 Primary Key 자동 추가 여부 결정:
console.log("🔑 context-data 모드: Primary Key 자동 추가"); // whereConditions가 명시적으로 설정되어 있으면 PK 자동 추가를 하지 않음
const enhancedWhereConditions = await this.enhanceWhereConditionsWithPK( // (사용자가 직접 조건을 설정한 경우 의도를 존중)
whereConditions, let finalWhereConditions: any[];
data, if (whereConditions && whereConditions.length > 0) {
targetTable console.log("📋 사용자 정의 WHERE 조건 사용 (PK 자동 추가 안 함)");
); finalWhereConditions = whereConditions;
} else {
console.log("🔑 context-data 모드: Primary Key 자동 추가");
finalWhereConditions = await this.enhanceWhereConditionsWithPK(
whereConditions,
data,
targetTable
);
}
const whereResult = this.buildWhereClause( const whereResult = this.buildWhereClause(
enhancedWhereConditions, finalWhereConditions,
data, data,
paramIndex paramIndex
); );
@ -1907,22 +1950,30 @@ export class NodeFlowExecutionService {
return deletedDataArray; return deletedDataArray;
} }
// 🆕 context-data 모드: 개별 삭제 (PK 자동 추가) // 🆕 context-data 모드: 개별 삭제
console.log("🎯 context-data 모드: 개별 삭제 시작"); console.log("🎯 context-data 모드: 개별 삭제 시작");
for (const data of dataArray) { for (const data of dataArray) {
console.log("🔍 WHERE 조건 처리 중..."); console.log("🔍 WHERE 조건 처리 중...");
// 🔑 Primary Key 자동 추가 (context-data 모드) // 🔑 Primary Key 자동 추가 여부 결정:
console.log("🔑 context-data 모드: Primary Key 자동 추가"); // whereConditions가 명시적으로 설정되어 있으면 PK 자동 추가를 하지 않음
const enhancedWhereConditions = await this.enhanceWhereConditionsWithPK( // (사용자가 직접 조건을 설정한 경우 의도를 존중)
whereConditions, let finalWhereConditions: any[];
data, if (whereConditions && whereConditions.length > 0) {
targetTable console.log("📋 사용자 정의 WHERE 조건 사용 (PK 자동 추가 안 함)");
); finalWhereConditions = whereConditions;
} else {
console.log("🔑 context-data 모드: Primary Key 자동 추가");
finalWhereConditions = await this.enhanceWhereConditionsWithPK(
whereConditions,
data,
targetTable
);
}
const whereResult = this.buildWhereClause( const whereResult = this.buildWhereClause(
enhancedWhereConditions, finalWhereConditions,
data, data,
1 1
); );
@ -2865,10 +2916,11 @@ export class NodeFlowExecutionService {
if (fieldValue === null || fieldValue === undefined || fieldValue === "") { if (fieldValue === null || fieldValue === undefined || fieldValue === "") {
logger.info( logger.info(
`⚠️ EXISTS 조건: 필드값이 비어있어 ${operator === "NOT_EXISTS_IN" ? "TRUE" : "FALSE"} 반환` `⚠️ EXISTS 조건: 필드값이 비어있어 FALSE 반환 (빈 값은 조건 검사하지 않음)`
); );
// 값이 비어있으면: EXISTS_IN은 false, NOT_EXISTS_IN은 true // 값이 비어있으면 조건 검사 자체가 무의미하므로 항상 false 반환
return operator === "NOT_EXISTS_IN"; // 이렇게 하면 빈 값으로 인한 의도치 않은 INSERT/UPDATE/DELETE가 방지됨
return false;
} }
try { try {

View File

@ -5,7 +5,7 @@
*/ */
import { useEffect, useState } from "react"; 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 { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import { tableTypeApi } from "@/lib/api/screen"; import { tableTypeApi } from "@/lib/api/screen";
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections"; 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 { InsertActionNodeData } from "@/types/node-editor";
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections"; 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 [apiHeaders, setApiHeaders] = useState<Record<string, string>>(data.apiHeaders || {});
const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || ""); const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "");
// 🔥 채번 규칙 관련 상태
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [numberingRulesLoading, setNumberingRulesLoading] = useState(false);
const [mappingNumberingRulesOpenState, setMappingNumberingRulesOpenState] = useState<boolean[]>([]);
// 데이터 변경 시 로컬 상태 업데이트 // 데이터 변경 시 로컬 상태 업데이트
useEffect(() => { useEffect(() => {
setDisplayName(data.displayName || data.targetTable); setDisplayName(data.displayName || data.targetTable);
@ -128,8 +135,33 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
useEffect(() => { useEffect(() => {
setMappingSourceFieldsOpenState(new Array(fieldMappings.length).fill(false)); setMappingSourceFieldsOpenState(new Array(fieldMappings.length).fill(false));
setMappingTargetFieldsOpenState(new Array(fieldMappings.length).fill(false)); setMappingTargetFieldsOpenState(new Array(fieldMappings.length).fill(false));
setMappingNumberingRulesOpenState(new Array(fieldMappings.length).fill(false));
}, [fieldMappings.length]); }, [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(() => { useEffect(() => {
if (targetType === "external" && selectedExternalConnectionId && externalTargetTable) { if (targetType === "external" && selectedExternalConnectionId && externalTargetTable) {
@ -540,6 +572,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
sourceField: null, sourceField: null,
targetField: "", targetField: "",
staticValue: undefined, staticValue: undefined,
valueType: "source" as const, // 🔥 기본값: 소스 필드
}, },
]; ];
setFieldMappings(newMappings); setFieldMappings(newMappings);
@ -548,6 +581,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
// Combobox 열림 상태 배열 초기화 // Combobox 열림 상태 배열 초기화
setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false)); setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false));
setMappingTargetFieldsOpenState(new Array(newMappings.length).fill(false)); setMappingTargetFieldsOpenState(new Array(newMappings.length).fill(false));
setMappingNumberingRulesOpenState(new Array(newMappings.length).fill(false));
}; };
const handleRemoveMapping = (index: number) => { const handleRemoveMapping = (index: number) => {
@ -558,6 +592,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
// Combobox 열림 상태 배열도 업데이트 // Combobox 열림 상태 배열도 업데이트
setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false)); setMappingSourceFieldsOpenState(new Array(newMappings.length).fill(false));
setMappingTargetFieldsOpenState(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) => { const handleMappingChange = (index: number, field: string, value: any) => {
@ -586,6 +621,24 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
targetField: value, targetField: value,
targetFieldLabel: targetColumn?.label_ko || targetColumn?.column_label || targetColumn?.displayName || 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 { } else {
newMappings[index] = { newMappings[index] = {
...newMappings[index], ...newMappings[index],
@ -1165,54 +1218,203 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{/* 소스 필드 입력/선택 */} {/* 🔥 값 생성 유형 선택 */}
<div> <div>
<Label className="text-xs text-gray-600"> <Label className="text-xs text-gray-600"> </Label>
<div className="mt-1 grid grid-cols-3 gap-1">
{hasRestAPISource && <span className="ml-1 text-teal-600">(REST API - )</span>} <button
</Label> type="button"
{hasRestAPISource ? ( onClick={() => handleMappingChange(index, "valueType", "source")}
// REST API 소스인 경우: 직접 입력 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 <Input
value={mapping.sourceField || ""} value={mapping.staticValue || ""}
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)} onChange={(e) => handleMappingChange(index, "staticValue", e.target.value || undefined)}
placeholder="필드명 입력 (예: userId, userName)" placeholder="고정값 입력"
className="mt-1 h-8 text-xs" className="mt-1 h-8 text-xs"
/> />
) : ( </div>
// 일반 소스인 경우: Combobox 선택 )}
{/* 🔥 채번 규칙 선택 (valueType === "autoGenerate" 일 때) */}
{mapping.valueType === "autoGenerate" && (
<div>
<Label className="text-xs text-gray-600">
{numberingRulesLoading && <span className="ml-1 text-gray-400">( ...)</span>}
</Label>
<Popover <Popover
open={mappingSourceFieldsOpenState[index]} open={mappingNumberingRulesOpenState[index]}
onOpenChange={(open) => { onOpenChange={(open) => {
const newState = [...mappingSourceFieldsOpenState]; const newState = [...mappingNumberingRulesOpenState];
newState[index] = open; newState[index] = open;
setMappingSourceFieldsOpenState(newState); setMappingNumberingRulesOpenState(newState);
}} }}
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
role="combobox" 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" 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 ( 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"> <span className="truncate font-medium">
{field?.label || mapping.sourceField} {rule?.ruleName || mapping.numberingRuleName || mapping.numberingRuleId}
</span> </span>
{field?.label && field.label !== field.name && (
<span className="text-muted-foreground font-mono text-xs">
{field.name}
</span>
)}
</div> </div>
); );
})() })()
: "소스 필드 선택"} : "채번 규칙 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -1222,37 +1424,36 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
align="start" align="start"
> >
<Command> <Command>
<CommandInput placeholder="소스 필드 검색..." className="text-xs sm:text-sm" /> <CommandInput placeholder="채번 규칙 검색..." className="text-xs sm:text-sm" />
<CommandList> <CommandList>
<CommandEmpty className="text-xs sm:text-sm"> <CommandEmpty className="text-xs sm:text-sm">
. .
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{sourceFields.map((field) => ( {numberingRules.map((rule) => (
<CommandItem <CommandItem
key={field.name} key={rule.ruleId}
value={field.name} value={rule.ruleId}
onSelect={(currentValue) => { onSelect={(currentValue) => {
handleMappingChange(index, "sourceField", currentValue || null); handleMappingChange(index, "numberingRuleId", currentValue);
const newState = [...mappingSourceFieldsOpenState]; const newState = [...mappingNumberingRulesOpenState];
newState[index] = false; newState[index] = false;
setMappingSourceFieldsOpenState(newState); setMappingNumberingRulesOpenState(newState);
}} }}
className="text-xs sm:text-sm" className="text-xs sm:text-sm"
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "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"> <div className="flex flex-col">
<span className="font-medium">{field.label || field.name}</span> <span className="font-medium">{rule.ruleName}</span>
{field.label && field.label !== field.name && ( <span className="text-muted-foreground font-mono text-[10px]">
<span className="text-muted-foreground font-mono text-[10px]"> {rule.ruleId}
{field.name} {rule.tableName && ` - ${rule.tableName}`}
</span> </span>
)}
</div> </div>
</CommandItem> </CommandItem>
))} ))}
@ -1261,11 +1462,13 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)} {numberingRules.length === 0 && !numberingRulesLoading && (
{hasRestAPISource && ( <p className="mt-1 text-xs text-orange-600">
<p className="mt-1 text-xs text-gray-500">API JSON의 </p> . .
)} </p>
</div> )}
</div>
)}
<div className="flex items-center justify-center py-1"> <div className="flex items-center justify-center py-1">
<ArrowRight className="h-4 w-4 text-green-600" /> <ArrowRight className="h-4 w-4 text-green-600" />
@ -1400,18 +1603,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </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>
</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"> <div className="rounded bg-green-50 p-3 text-xs text-green-700">
. <p> .</p>
<br /> <p className="mt-1"> 방식: 소스 ( ) / ( ) / ( )</p>
💡 .
</div> </div>
</div> </div>
</div> </div>

View File

@ -671,9 +671,11 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
console.log("🗑️ 품목 삭제:", deletedItem); console.log("🗑️ 품목 삭제:", deletedItem);
try { try {
// screenId 전달하여 제어관리 실행 가능하도록 함
const response = await dynamicFormApi.deleteFormDataFromTable( const response = await dynamicFormApi.deleteFormDataFromTable(
deletedItem.id, deletedItem.id,
screenData.screenInfo.tableName, screenData.screenInfo.tableName,
modalState.screenId || screenData.screenInfo?.id,
); );
if (response.success) { if (response.success) {

View File

@ -1676,7 +1676,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
try { try {
// console.log("🗑️ 삭제 실행:", { recordId, tableName, formData }); // console.log("🗑️ 삭제 실행:", { recordId, tableName, formData });
const result = await dynamicFormApi.deleteFormDataFromTable(recordId, tableName); // screenId 전달하여 제어관리 실행 가능하도록 함
const result = await dynamicFormApi.deleteFormDataFromTable(recordId, tableName, screenInfo?.id);
if (result.success) { if (result.success) {
alert("삭제되었습니다."); alert("삭제되었습니다.");

View File

@ -202,14 +202,19 @@ export class DynamicFormApi {
* *
* @param id ID * @param id ID
* @param tableName * @param tableName
* @param screenId ID ( , )
* @returns * @returns
*/ */
static async deleteFormDataFromTable(id: string | number, tableName: string): Promise<ApiResponse<void>> { static async deleteFormDataFromTable(
id: string | number,
tableName: string,
screenId?: number
): Promise<ApiResponse<void>> {
try { try {
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName }); console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName, screenId });
await apiClient.delete(`/dynamic-form/${id}`, { await apiClient.delete(`/dynamic-form/${id}`, {
data: { tableName }, data: { tableName, screenId },
}); });
console.log("✅ 실제 테이블에서 폼 데이터 삭제 성공"); console.log("✅ 실제 테이블에서 폼 데이터 삭제 성공");

View File

@ -967,11 +967,11 @@ export class ButtonActionExecutor {
deletedItemIds, deletedItemIds,
}); });
// 삭제 API 호출 // 삭제 API 호출 - screenId 전달하여 제어관리 실행 가능하도록 함
for (const itemId of deletedItemIds) { for (const itemId of deletedItemIds) {
try { try {
console.log(`🗑️ [handleSave] 항목 삭제 중: ${itemId} from ${targetTable}`); console.log(`🗑️ [handleSave] 항목 삭제 중: ${itemId} from ${targetTable}`);
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(itemId, targetTable); const deleteResult = await DynamicFormApi.deleteFormDataFromTable(itemId, targetTable, context.screenId);
if (deleteResult.success) { if (deleteResult.success) {
console.log(`✅ [handleSave] 항목 삭제 완료: ${itemId}`); console.log(`✅ [handleSave] 항목 삭제 완료: ${itemId}`);
} else { } else {
@ -1967,7 +1967,8 @@ export class ButtonActionExecutor {
for (const deletedItem of deletedItems) { for (const deletedItem of deletedItems) {
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`); console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`);
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deletedItem.id, saveTableName); // screenId 전달하여 제어관리 실행 가능하도록 함
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deletedItem.id, saveTableName, context.screenId);
if (!deleteResult.success) { if (!deleteResult.success) {
throw new Error(deleteResult.message || "품목 삭제 실패"); throw new Error(deleteResult.message || "품목 삭제 실패");
@ -2434,7 +2435,8 @@ export class ButtonActionExecutor {
if (deleteId) { if (deleteId) {
console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId }); console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName); // screenId 전달하여 제어관리 실행 가능하도록 함
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName, screenId);
if (!deleteResult.success) { if (!deleteResult.success) {
throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`); throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`);
} }
@ -2469,8 +2471,8 @@ export class ButtonActionExecutor {
if (tableName && screenId && formData.id) { if (tableName && screenId && formData.id) {
console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id }); console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });
// 실제 삭제 API 호출 // 실제 삭제 API 호출 - screenId 전달하여 제어관리 실행 가능하도록 함
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName); const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName, screenId);
if (!deleteResult.success) { if (!deleteResult.success) {
throw new Error(deleteResult.message || "삭제에 실패했습니다."); throw new Error(deleteResult.message || "삭제에 실패했습니다.");
@ -4251,7 +4253,8 @@ export class ButtonActionExecutor {
throw new Error("삭제할 항목의 ID를 찾을 수 없습니다."); throw new Error("삭제할 항목의 ID를 찾을 수 없습니다.");
} }
const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName); // screenId 전달하여 제어관리 실행 가능하도록 함
const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName, context.screenId);
if (result.success) { if (result.success) {
console.log("✅ 삭제 성공:", result); console.log("✅ 삭제 성공:", result);

View File

@ -344,6 +344,11 @@ export interface InsertActionNodeData {
targetField: string; targetField: string;
targetFieldLabel?: string; targetFieldLabel?: string;
staticValue?: any; staticValue?: any;
// 🔥 값 생성 유형 추가
valueType?: "source" | "static" | "autoGenerate"; // 소스 필드 / 고정값 / 자동 생성
// 자동 생성 옵션 (valueType === "autoGenerate" 일 때)
numberingRuleId?: string; // 채번 규칙 ID
numberingRuleName?: string; // 채번 규칙명 (표시용)
}>; }>;
options: { options: {
batchSize?: number; batchSize?: number;