Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -50,6 +50,9 @@ export class EntityJoinController {
|
||||
// search가 문자열인 경우 JSON 파싱
|
||||
searchConditions =
|
||||
typeof search === "string" ? JSON.parse(search) : search;
|
||||
|
||||
// 🔍 디버그: 파싱된 검색 조건 로깅
|
||||
logger.info(`🔍 파싱된 검색 조건:`, JSON.stringify(searchConditions, null, 2));
|
||||
} catch (error) {
|
||||
logger.warn("검색 조건 파싱 오류:", error);
|
||||
searchConditions = {};
|
||||
|
||||
@@ -47,8 +47,13 @@ export interface SplitPanelConfig {
|
||||
columns: Array<{ name: string; label: string; width?: number }>;
|
||||
relation?: {
|
||||
type: string;
|
||||
foreignKey: string;
|
||||
leftColumn: string;
|
||||
foreignKey?: string;
|
||||
leftColumn?: string;
|
||||
// 복합키 지원 (새로운 방식)
|
||||
keys?: Array<{
|
||||
leftColumn: string;
|
||||
rightColumn: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -210,8 +215,21 @@ class MasterDetailExcelService {
|
||||
}
|
||||
|
||||
// 2. 분할 패널의 relation 정보가 있으면 우선 사용
|
||||
let masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn;
|
||||
let detailFkColumn = splitPanel.rightPanel.relation?.foreignKey;
|
||||
// 🔥 keys 배열을 우선 사용 (새로운 복합키 지원 방식)
|
||||
let masterKeyColumn: string | undefined;
|
||||
let detailFkColumn: string | undefined;
|
||||
|
||||
const relationKeys = splitPanel.rightPanel.relation?.keys;
|
||||
if (relationKeys && relationKeys.length > 0) {
|
||||
// keys 배열에서 첫 번째 키 사용
|
||||
masterKeyColumn = relationKeys[0].leftColumn;
|
||||
detailFkColumn = relationKeys[0].rightColumn;
|
||||
logger.info(`keys 배열에서 관계 정보 사용: ${masterKeyColumn} -> ${detailFkColumn}`);
|
||||
} else {
|
||||
// 하위 호환성: 기존 leftColumn/foreignKey 사용
|
||||
masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn;
|
||||
detailFkColumn = splitPanel.rightPanel.relation?.foreignKey;
|
||||
}
|
||||
|
||||
// 3. relation 정보가 없으면 column_labels에서 Entity 관계 조회
|
||||
if (!masterKeyColumn || !detailFkColumn) {
|
||||
@@ -726,7 +744,9 @@ class MasterDetailExcelService {
|
||||
result.masterInserted = 1;
|
||||
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
|
||||
|
||||
// 4. 디테일 레코드들 생성
|
||||
// 4. 디테일 레코드들 생성 (삽입된 데이터 수집)
|
||||
const insertedDetailRows: Record<string, any>[] = [];
|
||||
|
||||
for (const row of detailData) {
|
||||
try {
|
||||
const detailRowData: Record<string, any> = {
|
||||
@@ -746,17 +766,26 @@ class MasterDetailExcelService {
|
||||
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
|
||||
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)
|
||||
VALUES (${detailPlaceholders.join(", ")}, NOW())`,
|
||||
VALUES (${detailPlaceholders.join(", ")}, NOW())
|
||||
RETURNING *`,
|
||||
detailValues
|
||||
);
|
||||
|
||||
if (insertResult.rows && insertResult.rows[0]) {
|
||||
insertedDetailRows.push(insertResult.rows[0]);
|
||||
}
|
||||
|
||||
result.detailInserted++;
|
||||
} catch (error: any) {
|
||||
result.errors.push(`디테일 행 처리 실패: ${error.message}`);
|
||||
logger.error(`디테일 행 처리 실패:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`디테일 레코드 ${insertedDetailRows.length}건 삽입 완료`);
|
||||
|
||||
await client.query("COMMIT");
|
||||
result.success = result.errors.length === 0 || result.detailInserted > 0;
|
||||
@@ -779,7 +808,7 @@ class MasterDetailExcelService {
|
||||
try {
|
||||
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
|
||||
|
||||
// 마스터 데이터를 제어에 전달
|
||||
// 마스터 데이터 구성
|
||||
const masterData = {
|
||||
...masterFieldValues,
|
||||
[relation!.masterKeyColumn]: result.generatedKey,
|
||||
@@ -791,17 +820,28 @@ class MasterDetailExcelService {
|
||||
// 순서대로 제어 실행
|
||||
for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) {
|
||||
logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`);
|
||||
logger.info(` 전달 데이터: 마스터 1건, 디테일 ${insertedDetailRows.length}건`);
|
||||
|
||||
// 🆕 삽입된 디테일 데이터를 sourceData로 전달 (성능 최적화)
|
||||
// - 전체 테이블 조회 대신 방금 INSERT한 데이터만 처리
|
||||
// - tableSource 노드가 context-data 모드일 때 이 데이터를 사용
|
||||
const controlResult = await NodeFlowExecutionService.executeFlow(
|
||||
parseInt(flow.flowId),
|
||||
{
|
||||
sourceData: [masterData],
|
||||
dataSourceType: "formData",
|
||||
sourceData: insertedDetailRows.length > 0 ? insertedDetailRows : [masterData],
|
||||
dataSourceType: "excelUpload", // 엑셀 업로드 데이터임을 명시
|
||||
buttonId: "excel-upload-button",
|
||||
screenId: screenId,
|
||||
userId: userId,
|
||||
companyCode: companyCode,
|
||||
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(
|
||||
arithmetic: any,
|
||||
@@ -4472,27 +4474,67 @@ export class NodeFlowExecutionService {
|
||||
const leftNum = Number(left) || 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 "+":
|
||||
return leftNum + rightNum;
|
||||
return left + right;
|
||||
case "-":
|
||||
return leftNum - rightNum;
|
||||
return left - right;
|
||||
case "*":
|
||||
return leftNum * rightNum;
|
||||
return left * right;
|
||||
case "/":
|
||||
if (rightNum === 0) {
|
||||
if (right === 0) {
|
||||
logger.warn(`⚠️ 0으로 나누기 시도`);
|
||||
return null;
|
||||
}
|
||||
return leftNum / rightNum;
|
||||
return left / right;
|
||||
case "%":
|
||||
if (rightNum === 0) {
|
||||
if (right === 0) {
|
||||
logger.warn(`⚠️ 0으로 나머지 연산 시도`);
|
||||
return null;
|
||||
}
|
||||
return leftNum % rightNum;
|
||||
return left % right;
|
||||
default:
|
||||
throw new Error(`지원하지 않는 연산자: ${arithmetic.operator}`);
|
||||
throw new Error(`지원하지 않는 연산자: ${operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,71 +187,68 @@ class TableCategoryValueService {
|
||||
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||
}
|
||||
|
||||
// 2. 카테고리 값 조회 (형제 메뉴 포함)
|
||||
// 2. 카테고리 값 조회 (메뉴 스코프 또는 형제 메뉴 포함)
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
const baseSelect = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
`;
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 카테고리 값 조회
|
||||
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
`;
|
||||
params = [tableName, columnName];
|
||||
logger.info("최고 관리자 카테고리 값 조회");
|
||||
// 최고 관리자: menuObjid가 있으면 해당 메뉴(및 형제 메뉴)의 값만 조회
|
||||
if (menuObjid && siblingObjids.length > 0) {
|
||||
query = baseSelect + ` AND menu_objid = ANY($3::numeric[])`;
|
||||
params = [tableName, columnName, siblingObjids];
|
||||
logger.info("최고 관리자 메뉴 스코프 카테고리 값 조회", { menuObjid, siblingObjids });
|
||||
} else if (menuObjid) {
|
||||
query = baseSelect + ` AND menu_objid = $3`;
|
||||
params = [tableName, columnName, menuObjid];
|
||||
logger.info("최고 관리자 단일 메뉴 카테고리 값 조회", { menuObjid });
|
||||
} else {
|
||||
// menuObjid 없으면 모든 값 조회 (중복 가능)
|
||||
query = baseSelect;
|
||||
params = [tableName, columnName];
|
||||
logger.info("최고 관리자 전체 카테고리 값 조회 (menuObjid 없음)");
|
||||
}
|
||||
} else {
|
||||
// 일반 회사: 자신의 카테고리 값만 조회
|
||||
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND company_code = $3
|
||||
`;
|
||||
params = [tableName, columnName, companyCode];
|
||||
logger.info("회사별 카테고리 값 조회", { companyCode });
|
||||
// 일반 회사: 자신의 회사 + menuObjid로 필터링
|
||||
if (menuObjid && siblingObjids.length > 0) {
|
||||
query = baseSelect + ` AND company_code = $3 AND menu_objid = ANY($4::numeric[])`;
|
||||
params = [tableName, columnName, companyCode, siblingObjids];
|
||||
logger.info("회사별 메뉴 스코프 카테고리 값 조회", { companyCode, menuObjid, siblingObjids });
|
||||
} else if (menuObjid) {
|
||||
query = baseSelect + ` AND company_code = $3 AND menu_objid = $4`;
|
||||
params = [tableName, columnName, companyCode, menuObjid];
|
||||
logger.info("회사별 단일 메뉴 카테고리 값 조회", { companyCode, menuObjid });
|
||||
} else {
|
||||
// menuObjid 없으면 회사 전체 조회 (중복 가능하지만 회사별로 제한)
|
||||
query = baseSelect + ` AND company_code = $3`;
|
||||
params = [tableName, columnName, companyCode];
|
||||
logger.info("회사별 카테고리 값 조회 (menuObjid 없음)", { companyCode });
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeInactive) {
|
||||
|
||||
Reference in New Issue
Block a user