feature/screen-management #348

Merged
kjs merged 7 commits from feature/screen-management into main 2026-01-09 15:46:43 +09:00
9 changed files with 400 additions and 116 deletions
Showing only changes of commit ee3a648917 - Show all commits

View File

@ -231,7 +231,7 @@ export const deleteFormData = async (
try {
const { id } = req.params;
const { companyCode, userId } = req.user as any;
const { tableName } = req.body;
const { tableName, screenId } = req.body;
if (!tableName) {
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({
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(
id: string | number,
tableName: string,
companyCode?: string,
userId?: string
userId?: string,
screenId?: number
): Promise<void> {
try {
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
@ -1310,14 +1316,19 @@ export class DynamicFormService {
const recordCompanyCode =
deletedRecord?.company_code || companyCode || "*";
await this.executeDataflowControlIfConfigured(
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
tableName,
deletedRecord,
"delete",
userId || "system",
recordCompanyCode
);
// screenId가 전달되지 않으면 제어관리를 실행하지 않음
if (screenId && screenId > 0) {
await this.executeDataflowControlIfConfigured(
screenId,
tableName,
deletedRecord,
"delete",
userId || "system",
recordCompanyCode
);
} else {
console.log(" screenId가 전달되지 않아 제어관리를 건너뜁니다. (screenId:", screenId, ")");
}
}
} catch (controlError) {
console.error("⚠️ 제어관리 실행 오류:", controlError);
@ -1662,10 +1673,16 @@ export class DynamicFormService {
!!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 (
properties?.componentType === "button-primary" &&
properties?.componentConfig?.action?.type === "save" &&
isMatchingAction &&
properties?.webTypeConfig?.enableDataflowControl === true
) {
const dataflowConfig = properties?.webTypeConfig?.dataflowConfig;

View File

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

View File

@ -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>

View File

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

View File

@ -1676,7 +1676,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
try {
// 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) {
alert("삭제되었습니다.");

View File

@ -202,14 +202,19 @@ export class DynamicFormApi {
*
* @param id ID
* @param tableName
* @param screenId ID ( , )
* @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 {
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName });
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName, screenId });
await apiClient.delete(`/dynamic-form/${id}`, {
data: { tableName },
data: { tableName, screenId },
});
console.log("✅ 실제 테이블에서 폼 데이터 삭제 성공");

View File

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

View File

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