Merge remote-tracking branch 'origin/main' into ksh
This commit is contained in:
commit
ca73685bc2
|
|
@ -50,6 +50,9 @@ export class EntityJoinController {
|
||||||
// search가 문자열인 경우 JSON 파싱
|
// search가 문자열인 경우 JSON 파싱
|
||||||
searchConditions =
|
searchConditions =
|
||||||
typeof search === "string" ? JSON.parse(search) : search;
|
typeof search === "string" ? JSON.parse(search) : search;
|
||||||
|
|
||||||
|
// 🔍 디버그: 파싱된 검색 조건 로깅
|
||||||
|
logger.info(`🔍 파싱된 검색 조건:`, JSON.stringify(searchConditions, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("검색 조건 파싱 오류:", error);
|
logger.warn("검색 조건 파싱 오류:", error);
|
||||||
searchConditions = {};
|
searchConditions = {};
|
||||||
|
|
|
||||||
|
|
@ -744,7 +744,9 @@ class MasterDetailExcelService {
|
||||||
result.masterInserted = 1;
|
result.masterInserted = 1;
|
||||||
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
|
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
|
||||||
|
|
||||||
// 4. 디테일 레코드들 생성
|
// 4. 디테일 레코드들 생성 (삽입된 데이터 수집)
|
||||||
|
const insertedDetailRows: Record<string, any>[] = [];
|
||||||
|
|
||||||
for (const row of detailData) {
|
for (const row of detailData) {
|
||||||
try {
|
try {
|
||||||
const detailRowData: Record<string, any> = {
|
const detailRowData: Record<string, any> = {
|
||||||
|
|
@ -764,17 +766,26 @@ class MasterDetailExcelService {
|
||||||
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
|
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
|
||||||
const detailValues = detailCols.map(k => detailRowData[k]);
|
const detailValues = detailCols.map(k => detailRowData[k]);
|
||||||
|
|
||||||
await client.query(
|
// RETURNING *로 삽입된 데이터 반환받기
|
||||||
|
const insertResult = await client.query(
|
||||||
`INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date)
|
`INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date)
|
||||||
VALUES (${detailPlaceholders.join(", ")}, NOW())`,
|
VALUES (${detailPlaceholders.join(", ")}, NOW())
|
||||||
|
RETURNING *`,
|
||||||
detailValues
|
detailValues
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (insertResult.rows && insertResult.rows[0]) {
|
||||||
|
insertedDetailRows.push(insertResult.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
result.detailInserted++;
|
result.detailInserted++;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
result.errors.push(`디테일 행 처리 실패: ${error.message}`);
|
result.errors.push(`디테일 행 처리 실패: ${error.message}`);
|
||||||
logger.error(`디테일 행 처리 실패:`, error);
|
logger.error(`디테일 행 처리 실패:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`디테일 레코드 ${insertedDetailRows.length}건 삽입 완료`);
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
result.success = result.errors.length === 0 || result.detailInserted > 0;
|
result.success = result.errors.length === 0 || result.detailInserted > 0;
|
||||||
|
|
@ -797,7 +808,7 @@ class MasterDetailExcelService {
|
||||||
try {
|
try {
|
||||||
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
|
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
|
||||||
|
|
||||||
// 마스터 데이터를 제어에 전달
|
// 마스터 데이터 구성
|
||||||
const masterData = {
|
const masterData = {
|
||||||
...masterFieldValues,
|
...masterFieldValues,
|
||||||
[relation!.masterKeyColumn]: result.generatedKey,
|
[relation!.masterKeyColumn]: result.generatedKey,
|
||||||
|
|
@ -809,17 +820,28 @@ class MasterDetailExcelService {
|
||||||
// 순서대로 제어 실행
|
// 순서대로 제어 실행
|
||||||
for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) {
|
for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) {
|
||||||
logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`);
|
logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`);
|
||||||
|
logger.info(` 전달 데이터: 마스터 1건, 디테일 ${insertedDetailRows.length}건`);
|
||||||
|
|
||||||
|
// 🆕 삽입된 디테일 데이터를 sourceData로 전달 (성능 최적화)
|
||||||
|
// - 전체 테이블 조회 대신 방금 INSERT한 데이터만 처리
|
||||||
|
// - tableSource 노드가 context-data 모드일 때 이 데이터를 사용
|
||||||
const controlResult = await NodeFlowExecutionService.executeFlow(
|
const controlResult = await NodeFlowExecutionService.executeFlow(
|
||||||
parseInt(flow.flowId),
|
parseInt(flow.flowId),
|
||||||
{
|
{
|
||||||
sourceData: [masterData],
|
sourceData: insertedDetailRows.length > 0 ? insertedDetailRows : [masterData],
|
||||||
dataSourceType: "formData",
|
dataSourceType: "excelUpload", // 엑셀 업로드 데이터임을 명시
|
||||||
buttonId: "excel-upload-button",
|
buttonId: "excel-upload-button",
|
||||||
screenId: screenId,
|
screenId: screenId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
companyCode: companyCode,
|
companyCode: companyCode,
|
||||||
formData: masterData,
|
formData: masterData,
|
||||||
|
// 추가 컨텍스트: 마스터/디테일 정보
|
||||||
|
masterData: masterData,
|
||||||
|
detailData: insertedDetailRows,
|
||||||
|
masterTable: relation!.masterTable,
|
||||||
|
detailTable: relation!.detailTable,
|
||||||
|
masterKeyColumn: relation!.masterKeyColumn,
|
||||||
|
detailFkColumn: relation!.detailFkColumn,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4446,6 +4446,8 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 산술 연산 계산
|
* 산술 연산 계산
|
||||||
|
* 다중 연산 지원: (leftOperand operator rightOperand) 이후 additionalOperations 순차 적용
|
||||||
|
* 예: (width * height) / 1000000 * qty
|
||||||
*/
|
*/
|
||||||
private static evaluateArithmetic(
|
private static evaluateArithmetic(
|
||||||
arithmetic: any,
|
arithmetic: any,
|
||||||
|
|
@ -4472,27 +4474,67 @@ export class NodeFlowExecutionService {
|
||||||
const leftNum = Number(left) || 0;
|
const leftNum = Number(left) || 0;
|
||||||
const rightNum = Number(right) || 0;
|
const rightNum = Number(right) || 0;
|
||||||
|
|
||||||
switch (arithmetic.operator) {
|
// 기본 연산 수행
|
||||||
|
let result = this.applyOperator(leftNum, arithmetic.operator, rightNum);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 추가 연산 처리 (다중 연산 지원)
|
||||||
|
if (arithmetic.additionalOperations && Array.isArray(arithmetic.additionalOperations)) {
|
||||||
|
for (const addOp of arithmetic.additionalOperations) {
|
||||||
|
const operandValue = this.getOperandValue(
|
||||||
|
addOp.operand,
|
||||||
|
sourceRow,
|
||||||
|
targetRow,
|
||||||
|
resultValues
|
||||||
|
);
|
||||||
|
const operandNum = Number(operandValue) || 0;
|
||||||
|
|
||||||
|
result = this.applyOperator(result, addOp.operator, operandNum);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
logger.warn(`⚠️ 추가 연산 실패: ${addOp.operator}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` 추가 연산: ${addOp.operator} ${operandNum} = ${result}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 연산자 적용
|
||||||
|
*/
|
||||||
|
private static applyOperator(
|
||||||
|
left: number,
|
||||||
|
operator: string,
|
||||||
|
right: number
|
||||||
|
): number | null {
|
||||||
|
switch (operator) {
|
||||||
case "+":
|
case "+":
|
||||||
return leftNum + rightNum;
|
return left + right;
|
||||||
case "-":
|
case "-":
|
||||||
return leftNum - rightNum;
|
return left - right;
|
||||||
case "*":
|
case "*":
|
||||||
return leftNum * rightNum;
|
return left * right;
|
||||||
case "/":
|
case "/":
|
||||||
if (rightNum === 0) {
|
if (right === 0) {
|
||||||
logger.warn(`⚠️ 0으로 나누기 시도`);
|
logger.warn(`⚠️ 0으로 나누기 시도`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return leftNum / rightNum;
|
return left / right;
|
||||||
case "%":
|
case "%":
|
||||||
if (rightNum === 0) {
|
if (right === 0) {
|
||||||
logger.warn(`⚠️ 0으로 나머지 연산 시도`);
|
logger.warn(`⚠️ 0으로 나머지 연산 시도`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return leftNum % rightNum;
|
return left % right;
|
||||||
default:
|
default:
|
||||||
throw new Error(`지원하지 않는 연산자: ${arithmetic.operator}`);
|
throw new Error(`지원하지 않는 연산자: ${operator}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,14 @@ const OPERATOR_LABELS: Record<string, string> = {
|
||||||
"%": "%",
|
"%": "%",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 피연산자를 문자열로 변환
|
||||||
|
function getOperandStr(operand: any): string {
|
||||||
|
if (!operand) return "?";
|
||||||
|
if (operand.type === "static") return String(operand.value || "?");
|
||||||
|
if (operand.fieldLabel) return operand.fieldLabel;
|
||||||
|
return operand.field || operand.resultField || "?";
|
||||||
|
}
|
||||||
|
|
||||||
// 수식 요약 생성
|
// 수식 요약 생성
|
||||||
function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string {
|
function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string {
|
||||||
const { formulaType, arithmetic, function: func, condition, staticValue } = transformation;
|
const { formulaType, arithmetic, function: func, condition, staticValue } = transformation;
|
||||||
|
|
@ -35,11 +43,19 @@ function getFormulaSummary(transformation: FormulaTransformNodeData["transformat
|
||||||
switch (formulaType) {
|
switch (formulaType) {
|
||||||
case "arithmetic": {
|
case "arithmetic": {
|
||||||
if (!arithmetic) return "미설정";
|
if (!arithmetic) return "미설정";
|
||||||
const left = arithmetic.leftOperand;
|
const leftStr = getOperandStr(arithmetic.leftOperand);
|
||||||
const right = arithmetic.rightOperand;
|
const rightStr = getOperandStr(arithmetic.rightOperand);
|
||||||
const leftStr = left.type === "static" ? left.value : `${left.type}.${left.field || left.resultField}`;
|
let formula = `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`;
|
||||||
const rightStr = right.type === "static" ? right.value : `${right.type}.${right.field || right.resultField}`;
|
|
||||||
return `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`;
|
// 추가 연산 표시
|
||||||
|
if (arithmetic.additionalOperations && arithmetic.additionalOperations.length > 0) {
|
||||||
|
for (const addOp of arithmetic.additionalOperations) {
|
||||||
|
const opStr = getOperandStr(addOp.operand);
|
||||||
|
formula += ` ${OPERATOR_LABELS[addOp.operator] || addOp.operator} ${opStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formula;
|
||||||
}
|
}
|
||||||
case "function": {
|
case "function": {
|
||||||
if (!func) return "미설정";
|
if (!func) return "미설정";
|
||||||
|
|
|
||||||
|
|
@ -797,6 +797,85 @@ export function FormulaTransformProperties({ nodeId, data }: FormulaTransformPro
|
||||||
index,
|
index,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 추가 연산 목록 */}
|
||||||
|
{trans.arithmetic.additionalOperations && trans.arithmetic.additionalOperations.length > 0 && (
|
||||||
|
<div className="space-y-2 border-t pt-2">
|
||||||
|
<Label className="text-xs text-gray-500">추가 연산</Label>
|
||||||
|
{trans.arithmetic.additionalOperations.map((addOp: any, addIndex: number) => (
|
||||||
|
<div key={addIndex} className="flex items-center gap-2 rounded bg-orange-50 p-2">
|
||||||
|
<Select
|
||||||
|
value={addOp.operator}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAdditionalOps = [...(trans.arithmetic!.additionalOperations || [])];
|
||||||
|
newAdditionalOps[addIndex] = { ...newAdditionalOps[addIndex], operator: value };
|
||||||
|
handleTransformationChange(index, {
|
||||||
|
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 w-20 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ARITHMETIC_OPERATORS.map((op) => (
|
||||||
|
<SelectItem key={op.value} value={op.value}>
|
||||||
|
{op.value}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex-1">
|
||||||
|
{renderOperandSelector(
|
||||||
|
addOp.operand,
|
||||||
|
(updates) => {
|
||||||
|
const newAdditionalOps = [...(trans.arithmetic!.additionalOperations || [])];
|
||||||
|
newAdditionalOps[addIndex] = { ...newAdditionalOps[addIndex], operand: updates };
|
||||||
|
handleTransformationChange(index, {
|
||||||
|
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
index,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-red-500 hover:text-red-700"
|
||||||
|
onClick={() => {
|
||||||
|
const newAdditionalOps = trans.arithmetic!.additionalOperations!.filter(
|
||||||
|
(_: any, i: number) => i !== addIndex
|
||||||
|
);
|
||||||
|
handleTransformationChange(index, {
|
||||||
|
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 추가 연산 버튼 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-full text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
const newAdditionalOps = [
|
||||||
|
...(trans.arithmetic!.additionalOperations || []),
|
||||||
|
{ operator: "*", operand: { type: "static" as const, value: "" } },
|
||||||
|
];
|
||||||
|
handleTransformationChange(index, {
|
||||||
|
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
연산 추가
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
||||||
|
|
||||||
// 기본 색상 팔레트
|
// 기본 색상 팔레트
|
||||||
|
|
@ -51,6 +52,7 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
const [valueLabel, setValueLabel] = useState("");
|
const [valueLabel, setValueLabel] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [color, setColor] = useState("none");
|
const [color, setColor] = useState("none");
|
||||||
|
const [continuousAdd, setContinuousAdd] = useState(false); // 연속 입력 체크박스
|
||||||
|
|
||||||
// 라벨에서 코드 자동 생성 (항상 고유한 코드 생성)
|
// 라벨에서 코드 자동 생성 (항상 고유한 코드 생성)
|
||||||
const generateCode = (): string => {
|
const generateCode = (): string => {
|
||||||
|
|
@ -60,6 +62,12 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
return `CATEGORY_${timestamp}${random}`;
|
return `CATEGORY_${timestamp}${random}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setValueLabel("");
|
||||||
|
setDescription("");
|
||||||
|
setColor("none");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!valueLabel.trim()) {
|
if (!valueLabel.trim()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -77,14 +85,28 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
} as TableCategoryValue);
|
} as TableCategoryValue);
|
||||||
|
|
||||||
// 초기화
|
// 연속 입력 체크되어 있으면 폼만 초기화하고 모달 유지
|
||||||
setValueLabel("");
|
if (continuousAdd) {
|
||||||
setDescription("");
|
resetForm();
|
||||||
setColor("none");
|
} else {
|
||||||
|
// 연속 입력 아니면 모달 닫기
|
||||||
|
resetForm();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
resetForm();
|
||||||
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
onOpenChange(isOpen);
|
||||||
|
}}>
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base sm:text-lg">
|
<DialogTitle className="text-base sm:text-lg">
|
||||||
|
|
@ -165,24 +187,42 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="flex-col gap-3 sm:flex-row sm:gap-0">
|
||||||
<Button
|
{/* 연속 입력 체크박스 */}
|
||||||
variant="outline"
|
<div className="flex items-center gap-2 w-full sm:w-auto sm:mr-auto">
|
||||||
onClick={() => onOpenChange(false)}
|
<Checkbox
|
||||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
id="continuousAdd"
|
||||||
>
|
checked={continuousAdd}
|
||||||
취소
|
onCheckedChange={(checked) => setContinuousAdd(checked as boolean)}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
<label
|
||||||
onClick={handleSubmit}
|
htmlFor="continuousAdd"
|
||||||
disabled={!valueLabel.trim()}
|
className="text-xs sm:text-sm text-muted-foreground cursor-pointer"
|
||||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
>
|
||||||
>
|
연속 입력
|
||||||
추가
|
</label>
|
||||||
</Button>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 w-full sm:w-auto">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!valueLabel.trim()}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
await loadCategoryValues();
|
await loadCategoryValues();
|
||||||
setIsAddDialogOpen(false);
|
// 모달 닫기는 CategoryValueAddDialog에서 연속 입력 체크박스로 제어
|
||||||
toast({
|
toast({
|
||||||
title: "성공",
|
title: "성공",
|
||||||
description: "카테고리 값이 추가되었습니다",
|
description: "카테고리 값이 추가되었습니다",
|
||||||
|
|
@ -142,7 +142,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
title: "오류",
|
title: "오류",
|
||||||
description: error.message || "카테고리 값 추가에 실패했습니다",
|
description: error.message || "카테고리 값 추가에 실패했습니다",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1147,8 +1147,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 🆕 추가 탭 데이터 로딩 함수
|
// 🆕 추가 탭 데이터 로딩 함수
|
||||||
const loadTabData = useCallback(
|
const loadTabData = useCallback(
|
||||||
async (tabIndex: number, leftItem: any) => {
|
async (tabIndex: number, leftItem: any) => {
|
||||||
|
console.log(`📥 loadTabData 호출됨: tabIndex=${tabIndex}`, {
|
||||||
|
leftItem: leftItem ? Object.keys(leftItem) : null,
|
||||||
|
additionalTabs: componentConfig.rightPanel?.additionalTabs?.length,
|
||||||
|
isDesignMode,
|
||||||
|
});
|
||||||
|
|
||||||
const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1];
|
const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1];
|
||||||
if (!tabConfig || !leftItem || isDesignMode) return;
|
|
||||||
|
console.log(`📥 tabConfig:`, {
|
||||||
|
tabIndex,
|
||||||
|
configIndex: tabIndex - 1,
|
||||||
|
tabConfig: tabConfig ? {
|
||||||
|
tableName: tabConfig.tableName,
|
||||||
|
relation: tabConfig.relation,
|
||||||
|
dataFilter: tabConfig.dataFilter
|
||||||
|
} : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tabConfig || !leftItem || isDesignMode) {
|
||||||
|
console.log(`⚠️ loadTabData 중단:`, { hasTabConfig: !!tabConfig, hasLeftItem: !!leftItem, isDesignMode });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tabTableName = tabConfig.tableName;
|
const tabTableName = tabConfig.tableName;
|
||||||
if (!tabTableName) return;
|
if (!tabTableName) return;
|
||||||
|
|
@ -1160,6 +1180,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
|
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
|
||||||
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
|
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
|
||||||
|
|
||||||
|
console.log(`🔑 [추가탭 ${tabIndex}] 조인 키 분석:`, {
|
||||||
|
hasRelation: !!tabConfig.relation,
|
||||||
|
keys,
|
||||||
|
leftColumn,
|
||||||
|
rightColumn,
|
||||||
|
willUseJoin: !!(leftColumn && rightColumn),
|
||||||
|
});
|
||||||
|
|
||||||
let resultData: any[] = [];
|
let resultData: any[] = [];
|
||||||
|
|
||||||
if (leftColumn && rightColumn) {
|
if (leftColumn && rightColumn) {
|
||||||
|
|
@ -1171,14 +1199,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 복합키
|
// 복합키
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
||||||
searchConditions[key.rightColumn] = leftItem[key.leftColumn];
|
// operator: "equals"를 추가하여 정확한 값 매칭 (entity 타입 컬럼에서 코드값으로 검색)
|
||||||
|
searchConditions[key.rightColumn] = {
|
||||||
|
value: leftItem[key.leftColumn],
|
||||||
|
operator: "equals",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 단일키
|
// 단일키
|
||||||
const leftValue = leftItem[leftColumn];
|
const leftValue = leftItem[leftColumn];
|
||||||
if (leftValue !== undefined) {
|
if (leftValue !== undefined) {
|
||||||
searchConditions[rightColumn] = leftValue;
|
// operator: "equals"를 추가하여 정확한 값 매칭 (entity 타입 컬럼에서 코드값으로 검색)
|
||||||
|
searchConditions[rightColumn] = {
|
||||||
|
value: leftValue,
|
||||||
|
operator: "equals",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1193,43 +1229,68 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
resultData = result.data || [];
|
resultData = result.data || [];
|
||||||
} else {
|
} else {
|
||||||
// 조인 조건이 없는 경우: 전체 데이터 조회 (독립 탭)
|
// 조인 조건이 없는 경우: 전체 데이터 조회 (독립 탭)
|
||||||
|
console.log(`📋 [추가탭 ${tabIndex}] 조인 없이 전체 데이터 조회: ${tabTableName}`);
|
||||||
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
||||||
enableEntityJoin: true,
|
enableEntityJoin: true,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
});
|
});
|
||||||
resultData = result.data || [];
|
resultData = result.data || [];
|
||||||
|
console.log(`📋 [추가탭 ${tabIndex}] 전체 데이터 조회 결과:`, resultData.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 필터 적용
|
// 데이터 필터 적용
|
||||||
const dataFilter = tabConfig.dataFilter;
|
const dataFilter = tabConfig.dataFilter;
|
||||||
// filters 또는 conditions 배열 지원 (DataFilterConfigPanel은 filters 사용)
|
// filters 또는 conditions 배열 지원 (DataFilterConfigPanel은 filters 사용)
|
||||||
const filterConditions = dataFilter?.filters || dataFilter?.conditions || [];
|
const filterConditions = dataFilter?.filters || dataFilter?.conditions || [];
|
||||||
|
|
||||||
|
console.log(`🔍 [추가탭 ${tabIndex}] 필터 설정:`, {
|
||||||
|
enabled: dataFilter?.enabled,
|
||||||
|
filterConditions,
|
||||||
|
dataBeforeFilter: resultData.length,
|
||||||
|
});
|
||||||
|
|
||||||
if (dataFilter?.enabled && filterConditions.length > 0) {
|
if (dataFilter?.enabled && filterConditions.length > 0) {
|
||||||
|
const beforeCount = resultData.length;
|
||||||
resultData = resultData.filter((item: any) => {
|
resultData = resultData.filter((item: any) => {
|
||||||
return filterConditions.every((cond: any) => {
|
return filterConditions.every((cond: any) => {
|
||||||
// columnName 또는 column 지원
|
// columnName 또는 column 지원
|
||||||
const columnName = cond.columnName || cond.column;
|
const columnName = cond.columnName || cond.column;
|
||||||
const value = item[columnName];
|
const value = item[columnName];
|
||||||
const condValue = cond.value;
|
const condValue = cond.value;
|
||||||
|
|
||||||
|
let result = true;
|
||||||
switch (cond.operator) {
|
switch (cond.operator) {
|
||||||
case "equals":
|
case "equals":
|
||||||
return value === condValue;
|
result = value === condValue;
|
||||||
|
break;
|
||||||
case "notEquals":
|
case "notEquals":
|
||||||
return value !== condValue;
|
result = value !== condValue;
|
||||||
|
break;
|
||||||
case "contains":
|
case "contains":
|
||||||
return String(value).includes(String(condValue));
|
result = String(value).includes(String(condValue));
|
||||||
|
break;
|
||||||
case "is_null":
|
case "is_null":
|
||||||
case "NULL":
|
case "NULL":
|
||||||
return value === null || value === undefined || value === "";
|
result = value === null || value === undefined || value === "";
|
||||||
|
break;
|
||||||
case "is_not_null":
|
case "is_not_null":
|
||||||
case "NOT NULL":
|
case "NOT NULL":
|
||||||
return value !== null && value !== undefined && value !== "";
|
result = value !== null && value !== undefined && value !== "";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return true;
|
result = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 첫 5개 항목만 로그 출력
|
||||||
|
if (resultData.indexOf(item) < 5) {
|
||||||
|
console.log(` 필터 체크: ${columnName}=${value}, operator=${cond.operator}, result=${result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
console.log(`🔍 [추가탭 ${tabIndex}] 필터 적용 후: ${beforeCount} → ${resultData.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중복 제거 적용
|
// 중복 제거 적용
|
||||||
|
|
@ -1301,6 +1362,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 🆕 탭 변경 핸들러
|
// 🆕 탭 변경 핸들러
|
||||||
const handleTabChange = useCallback(
|
const handleTabChange = useCallback(
|
||||||
(newTabIndex: number) => {
|
(newTabIndex: number) => {
|
||||||
|
console.log(`🔄 탭 변경: ${activeTabIndex} → ${newTabIndex}`, {
|
||||||
|
selectedLeftItem: !!selectedLeftItem,
|
||||||
|
tabsData: Object.keys(tabsData),
|
||||||
|
hasTabData: !!tabsData[newTabIndex],
|
||||||
|
});
|
||||||
|
|
||||||
setActiveTabIndex(newTabIndex);
|
setActiveTabIndex(newTabIndex);
|
||||||
|
|
||||||
// 선택된 좌측 항목이 있으면 해당 탭의 데이터 로드
|
// 선택된 좌측 항목이 있으면 해당 탭의 데이터 로드
|
||||||
|
|
@ -1311,14 +1378,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
loadRightData(selectedLeftItem);
|
loadRightData(selectedLeftItem);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 추가 탭: 해당 탭 데이터가 없으면 로드
|
// 추가 탭: 항상 새로 로드 (필터 설정 변경 반영을 위해)
|
||||||
if (!tabsData[newTabIndex]) {
|
console.log(`🔄 추가 탭 ${newTabIndex} 데이터 로드 (항상 새로고침)`);
|
||||||
loadTabData(newTabIndex, selectedLeftItem);
|
loadTabData(newTabIndex, selectedLeftItem);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ 좌측 항목이 선택되지 않아 탭 데이터를 로드하지 않음`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedLeftItem, rightData, tabsData, loadRightData, loadTabData],
|
[selectedLeftItem, rightData, tabsData, loadRightData, loadTabData, activeTabIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 우측 항목 확장/축소 토글
|
// 우측 항목 확장/축소 토글
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,12 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
// 탭 업데이트 헬퍼
|
// 탭 업데이트 헬퍼
|
||||||
const updateTab = (updates: Partial<AdditionalTabConfig>) => {
|
const updateTab = (updates: Partial<AdditionalTabConfig>) => {
|
||||||
const newTabs = [...(config.rightPanel?.additionalTabs || [])];
|
const newTabs = [...(config.rightPanel?.additionalTabs || [])];
|
||||||
newTabs[tabIndex] = { ...tab, ...updates };
|
// undefined 값도 명시적으로 덮어쓰기 위해 Object.assign 대신 직접 처리
|
||||||
|
const updatedTab = { ...tab };
|
||||||
|
Object.keys(updates).forEach((key) => {
|
||||||
|
(updatedTab as any)[key] = (updates as any)[key];
|
||||||
|
});
|
||||||
|
newTabs[tabIndex] = updatedTab;
|
||||||
updateRightPanel({ additionalTabs: newTabs });
|
updateRightPanel({ additionalTabs: newTabs });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -393,21 +398,31 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">좌측 컬럼</Label>
|
<Label className="text-[10px]">좌측 컬럼</Label>
|
||||||
<Select
|
<Select
|
||||||
value={tab.relation?.keys?.[0]?.leftColumn || tab.relation?.leftColumn || ""}
|
value={tab.relation?.keys?.[0]?.leftColumn || tab.relation?.leftColumn || "__none__"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateTab({
|
if (value === "__none__") {
|
||||||
relation: {
|
// 선택 안 함 - 조인 키 제거
|
||||||
...tab.relation,
|
updateTab({
|
||||||
type: "join",
|
relation: undefined,
|
||||||
keys: [{ leftColumn: value, rightColumn: tab.relation?.keys?.[0]?.rightColumn || "" }],
|
});
|
||||||
},
|
} else {
|
||||||
});
|
updateTab({
|
||||||
|
relation: {
|
||||||
|
...tab.relation,
|
||||||
|
type: "join",
|
||||||
|
keys: [{ leftColumn: value, rightColumn: tab.relation?.keys?.[0]?.rightColumn || "" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">
|
||||||
|
<span className="text-muted-foreground">선택 안 함 (전체 데이터)</span>
|
||||||
|
</SelectItem>
|
||||||
{leftTableColumns.map((col) => (
|
{leftTableColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.columnLabel || col.columnName}
|
{col.columnLabel || col.columnName}
|
||||||
|
|
@ -419,21 +434,31 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">우측 컬럼</Label>
|
<Label className="text-[10px]">우측 컬럼</Label>
|
||||||
<Select
|
<Select
|
||||||
value={tab.relation?.keys?.[0]?.rightColumn || tab.relation?.foreignKey || ""}
|
value={tab.relation?.keys?.[0]?.rightColumn || tab.relation?.foreignKey || "__none__"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateTab({
|
if (value === "__none__") {
|
||||||
relation: {
|
// 선택 안 함 - 조인 키 제거
|
||||||
...tab.relation,
|
updateTab({
|
||||||
type: "join",
|
relation: undefined,
|
||||||
keys: [{ leftColumn: tab.relation?.keys?.[0]?.leftColumn || "", rightColumn: value }],
|
});
|
||||||
},
|
} else {
|
||||||
});
|
updateTab({
|
||||||
|
relation: {
|
||||||
|
...tab.relation,
|
||||||
|
type: "join",
|
||||||
|
keys: [{ leftColumn: tab.relation?.keys?.[0]?.leftColumn || "", rightColumn: value }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">
|
||||||
|
<span className="text-muted-foreground">선택 안 함 (전체 데이터)</span>
|
||||||
|
</SelectItem>
|
||||||
{tabColumns.map((col) => (
|
{tabColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.columnLabel || col.columnName}
|
{col.columnLabel || col.columnName}
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,14 @@ export function UniversalFormModalComponent({
|
||||||
console.log("[UniversalFormModal] beforeFormSave 이벤트 수신");
|
console.log("[UniversalFormModal] beforeFormSave 이벤트 수신");
|
||||||
console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields));
|
console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields));
|
||||||
|
|
||||||
|
// 🆕 시스템 필드 병합: id는 설정 여부와 관계없이 항상 전달 (UPDATE/INSERT 판단용)
|
||||||
|
// - 신규 등록: formData.id가 없으므로 영향 없음
|
||||||
|
// - 편집 모드: formData.id가 있으면 메인 테이블 UPDATE에 사용
|
||||||
|
if (formData.id !== undefined && formData.id !== null && formData.id !== "") {
|
||||||
|
event.detail.formData.id = formData.id;
|
||||||
|
console.log(`[UniversalFormModal] 시스템 필드 병합: id =`, formData.id);
|
||||||
|
}
|
||||||
|
|
||||||
// UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함)
|
// UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함)
|
||||||
// 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀
|
// 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀
|
||||||
// (UniversalFormModal이 해당 필드의 주인이므로)
|
// (UniversalFormModal이 해당 필드의 주인이므로)
|
||||||
|
|
|
||||||
|
|
@ -1971,19 +1971,43 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave);
|
// 🆕 메인 테이블 UPDATE/INSERT 판단
|
||||||
|
// - formData.id가 있으면 편집 모드 → UPDATE
|
||||||
|
// - formData.id가 없으면 신규 등록 → INSERT
|
||||||
|
const existingMainId = formData.id;
|
||||||
|
const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== "";
|
||||||
|
|
||||||
const mainSaveResult = await DynamicFormApi.saveFormData({
|
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave);
|
||||||
screenId: screenId!,
|
console.log("📦 [handleUniversalFormModalTableSectionSave] UPDATE/INSERT 판단:", {
|
||||||
tableName: tableName!,
|
existingMainId,
|
||||||
data: mainRowToSave,
|
isMainUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mainSaveResult: { success: boolean; data?: any; message?: string };
|
||||||
|
|
||||||
|
if (isMainUpdate) {
|
||||||
|
// 🔄 편집 모드: UPDATE 실행
|
||||||
|
console.log("🔄 [handleUniversalFormModalTableSectionSave] 메인 테이블 UPDATE 실행, ID:", existingMainId);
|
||||||
|
mainSaveResult = await DynamicFormApi.updateFormData(existingMainId, {
|
||||||
|
tableName: tableName!,
|
||||||
|
data: mainRowToSave,
|
||||||
|
});
|
||||||
|
mainRecordId = existingMainId;
|
||||||
|
} else {
|
||||||
|
// ➕ 신규 등록: INSERT 실행
|
||||||
|
console.log("➕ [handleUniversalFormModalTableSectionSave] 메인 테이블 INSERT 실행");
|
||||||
|
mainSaveResult = await DynamicFormApi.saveFormData({
|
||||||
|
screenId: screenId!,
|
||||||
|
tableName: tableName!,
|
||||||
|
data: mainRowToSave,
|
||||||
|
});
|
||||||
|
mainRecordId = mainSaveResult.data?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mainSaveResult.success) {
|
if (!mainSaveResult.success) {
|
||||||
throw new Error(mainSaveResult.message || "메인 데이터 저장 실패");
|
throw new Error(mainSaveResult.message || "메인 데이터 저장 실패");
|
||||||
}
|
}
|
||||||
|
|
||||||
mainRecordId = mainSaveResult.data?.id || null;
|
|
||||||
console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId);
|
console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4996,6 +5020,12 @@ export class ButtonActionExecutor {
|
||||||
masterDetailRelation = relationResponse.data;
|
masterDetailRelation = relationResponse.data;
|
||||||
|
|
||||||
// 버튼 설정에서 채번 규칙 등 추가 설정 가져오기
|
// 버튼 설정에서 채번 규칙 등 추가 설정 가져오기
|
||||||
|
// 업로드 후 제어: excelAfterUploadFlows를 우선 사용 (통합된 설정)
|
||||||
|
// masterDetailExcel.afterUploadFlows는 레거시 호환성을 위해 fallback으로만 사용
|
||||||
|
const afterUploadFlows = config.excelAfterUploadFlows?.length > 0
|
||||||
|
? config.excelAfterUploadFlows
|
||||||
|
: config.masterDetailExcel?.afterUploadFlows;
|
||||||
|
|
||||||
if (config.masterDetailExcel) {
|
if (config.masterDetailExcel) {
|
||||||
masterDetailExcelConfig = {
|
masterDetailExcelConfig = {
|
||||||
...config.masterDetailExcel,
|
...config.masterDetailExcel,
|
||||||
|
|
@ -5006,8 +5036,8 @@ export class ButtonActionExecutor {
|
||||||
detailFkColumn: relationResponse.data.detailFkColumn,
|
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||||
// 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑)
|
// 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑)
|
||||||
numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId,
|
numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId,
|
||||||
// 업로드 후 제어 설정 추가
|
// 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
|
||||||
afterUploadFlows: config.masterDetailExcel.afterUploadFlows || config.excelAfterUploadFlows,
|
afterUploadFlows,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// 버튼 설정이 없으면 분할 패널 정보만 사용
|
// 버튼 설정이 없으면 분할 패널 정보만 사용
|
||||||
|
|
@ -5019,8 +5049,8 @@ export class ButtonActionExecutor {
|
||||||
simpleMode: true, // 기본값으로 간단 모드 사용
|
simpleMode: true, // 기본값으로 간단 모드 사용
|
||||||
// 채번 규칙 ID 추가 (excelNumberingRuleId 사용)
|
// 채번 규칙 ID 추가 (excelNumberingRuleId 사용)
|
||||||
numberingRuleId: config.excelNumberingRuleId,
|
numberingRuleId: config.excelNumberingRuleId,
|
||||||
// 업로드 후 제어 설정 추가
|
// 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
|
||||||
afterUploadFlows: config.excelAfterUploadFlows,
|
afterUploadFlows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,17 @@ export interface FormulaTransformNodeData {
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
resultField?: string;
|
resultField?: string;
|
||||||
};
|
};
|
||||||
|
// 추가 연산 (다중 연산 지원: (left op right) op1 val1 op2 val2 ...)
|
||||||
|
additionalOperations?: Array<{
|
||||||
|
operator: "+" | "-" | "*" | "/" | "%";
|
||||||
|
operand: {
|
||||||
|
type: "source" | "target" | "static" | "result";
|
||||||
|
field?: string;
|
||||||
|
fieldLabel?: string;
|
||||||
|
value?: string | number;
|
||||||
|
resultField?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 함수 (formulaType === "function")
|
// 함수 (formulaType === "function")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue