feature/screen-management #348
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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("삭제되었습니다.");
|
||||
|
|
|
|||
|
|
@ -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("✅ 실제 테이블에서 폼 데이터 삭제 성공");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue