Merge branch 'mhkim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -561,6 +561,34 @@ export class EntityJoinController {
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 테이블 컬럼의 고유값 목록 조회 (헤더 필터 드롭다운용)
|
||||
* GET /api/table-management/tables/:tableName/column-values/:columnName
|
||||
*/
|
||||
async getColumnUniqueValues(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
|
||||
const data = await tableManagementService.getColumnDistinctValues(
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`컬럼 고유값 조회 실패: ${req.params.tableName}.${req.params.columnName}`, error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "컬럼 고유값 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const entityJoinController = new EntityJoinController();
|
||||
|
||||
@@ -55,6 +55,15 @@ router.get(
|
||||
entityJoinController.getTableDataWithJoins.bind(entityJoinController)
|
||||
);
|
||||
|
||||
/**
|
||||
* 테이블 컬럼의 고유값 목록 조회 (헤더 필터 드롭다운용)
|
||||
* GET /api/table-management/tables/:tableName/column-values/:columnName
|
||||
*/
|
||||
router.get(
|
||||
"/tables/:tableName/column-values/:columnName",
|
||||
entityJoinController.getColumnUniqueValues.bind(entityJoinController)
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 🎯 Entity 조인 설정 관리
|
||||
// ========================================
|
||||
|
||||
@@ -211,7 +211,8 @@ class TableCategoryValueService {
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
updated_by AS "updatedBy",
|
||||
path
|
||||
FROM category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
@@ -1441,7 +1442,7 @@ class TableCategoryValueService {
|
||||
// is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
|
||||
if (companyCode === "*") {
|
||||
query = `
|
||||
SELECT DISTINCT value_code, value_label
|
||||
SELECT DISTINCT value_code, value_label, path
|
||||
FROM category_values
|
||||
WHERE value_code IN (${placeholders1})
|
||||
`;
|
||||
@@ -1449,7 +1450,7 @@ class TableCategoryValueService {
|
||||
} else {
|
||||
const companyIdx = n + 1;
|
||||
query = `
|
||||
SELECT DISTINCT value_code, value_label
|
||||
SELECT DISTINCT value_code, value_label, path
|
||||
FROM category_values
|
||||
WHERE value_code IN (${placeholders1})
|
||||
AND (company_code = $${companyIdx} OR company_code = '*')
|
||||
@@ -1460,10 +1461,15 @@ class TableCategoryValueService {
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
// { [code]: label } 형태로 변환 (중복 시 첫 번째 결과 우선)
|
||||
// path가 있고 '/'를 포함하면(depth>1) 전체 경로를 ' > ' 구분자로 표시
|
||||
const labels: Record<string, string> = {};
|
||||
for (const row of result.rows) {
|
||||
if (!labels[row.value_code]) {
|
||||
labels[row.value_code] = row.value_label;
|
||||
if (row.path && row.path.includes('/')) {
|
||||
labels[row.value_code] = row.path.replace(/\//g, ' > ');
|
||||
} else {
|
||||
labels[row.value_code] = row.value_label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3412,6 +3412,31 @@ export class TableManagementService {
|
||||
case "is_not_null":
|
||||
filterConditions.push(`${safeColumn} IS NOT NULL`);
|
||||
break;
|
||||
case "not_contains":
|
||||
filterConditions.push(
|
||||
`${safeColumn}::text NOT LIKE '%${String(value).replace(/'/g, "''")}%'`
|
||||
);
|
||||
break;
|
||||
case "greater_than":
|
||||
filterConditions.push(
|
||||
`(${safeColumn})::numeric > ${parseFloat(String(value))}`
|
||||
);
|
||||
break;
|
||||
case "less_than":
|
||||
filterConditions.push(
|
||||
`(${safeColumn})::numeric < ${parseFloat(String(value))}`
|
||||
);
|
||||
break;
|
||||
case "greater_or_equal":
|
||||
filterConditions.push(
|
||||
`(${safeColumn})::numeric >= ${parseFloat(String(value))}`
|
||||
);
|
||||
break;
|
||||
case "less_or_equal":
|
||||
filterConditions.push(
|
||||
`(${safeColumn})::numeric <= ${parseFloat(String(value))}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3428,6 +3453,89 @@ export class TableManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 filterGroups 처리 (런타임 필터 빌더 - 그룹별 AND/OR 지원)
|
||||
if (
|
||||
options.dataFilter &&
|
||||
options.dataFilter.filterGroups &&
|
||||
options.dataFilter.filterGroups.length > 0
|
||||
) {
|
||||
const groupConditions: string[] = [];
|
||||
|
||||
for (const group of options.dataFilter.filterGroups) {
|
||||
if (!group.conditions || group.conditions.length === 0) continue;
|
||||
|
||||
const conditions: string[] = [];
|
||||
|
||||
for (const condition of group.conditions) {
|
||||
const { columnName, operator, value } = condition;
|
||||
if (!columnName) continue;
|
||||
|
||||
const safeCol = `main."${columnName}"`;
|
||||
|
||||
switch (operator) {
|
||||
case "equals":
|
||||
conditions.push(`${safeCol}::text = '${String(value).replace(/'/g, "''")}'`);
|
||||
break;
|
||||
case "not_equals":
|
||||
conditions.push(`${safeCol}::text != '${String(value).replace(/'/g, "''")}'`);
|
||||
break;
|
||||
case "contains":
|
||||
conditions.push(`${safeCol}::text LIKE '%${String(value).replace(/'/g, "''")}%'`);
|
||||
break;
|
||||
case "not_contains":
|
||||
conditions.push(`${safeCol}::text NOT LIKE '%${String(value).replace(/'/g, "''")}%'`);
|
||||
break;
|
||||
case "starts_with":
|
||||
conditions.push(`${safeCol}::text LIKE '${String(value).replace(/'/g, "''")}%'`);
|
||||
break;
|
||||
case "ends_with":
|
||||
conditions.push(`${safeCol}::text LIKE '%${String(value).replace(/'/g, "''")}'`);
|
||||
break;
|
||||
case "greater_than":
|
||||
conditions.push(`(${safeCol})::numeric > ${parseFloat(String(value))}`);
|
||||
break;
|
||||
case "less_than":
|
||||
conditions.push(`(${safeCol})::numeric < ${parseFloat(String(value))}`);
|
||||
break;
|
||||
case "greater_or_equal":
|
||||
conditions.push(`(${safeCol})::numeric >= ${parseFloat(String(value))}`);
|
||||
break;
|
||||
case "less_or_equal":
|
||||
conditions.push(`(${safeCol})::numeric <= ${parseFloat(String(value))}`);
|
||||
break;
|
||||
case "is_null":
|
||||
conditions.push(`(${safeCol} IS NULL OR ${safeCol}::text = '')`);
|
||||
break;
|
||||
case "is_not_null":
|
||||
conditions.push(`(${safeCol} IS NOT NULL AND ${safeCol}::text != '')`);
|
||||
break;
|
||||
case "in": {
|
||||
const inArr = Array.isArray(value) ? value : [String(value)];
|
||||
if (inArr.length > 0) {
|
||||
const vals = inArr.map((v) => `'${String(v).replace(/'/g, "''")}'`).join(", ");
|
||||
conditions.push(`${safeCol}::text IN (${vals})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
const logic = group.logic === "OR" ? " OR " : " AND ";
|
||||
groupConditions.push(`(${conditions.join(logic)})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupConditions.length > 0) {
|
||||
const groupWhere = groupConditions.join(" AND ");
|
||||
whereClause = whereClause
|
||||
? `${whereClause} AND ${groupWhere}`
|
||||
: groupWhere;
|
||||
|
||||
logger.info(`🔍 필터 그룹 적용 (Entity 조인): ${groupWhere}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 제외 필터 적용 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
if (options.excludeFilter && options.excludeFilter.enabled) {
|
||||
const {
|
||||
@@ -5391,4 +5499,40 @@ export class TableManagementService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼의 고유값 목록 조회 (헤더 필터 드롭다운용)
|
||||
*/
|
||||
async getColumnDistinctValues(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
companyCode?: string
|
||||
): Promise<{ value: string; label: string }[]> {
|
||||
try {
|
||||
// 테이블명/컬럼명 안전성 검증 (영문, 숫자, 언더스코어만 허용)
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName) || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
|
||||
logger.warn(`잘못된 테이블/컬럼명: ${tableName}.${columnName}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
let sql = `SELECT DISTINCT "${columnName}"::text as value FROM "${tableName}" WHERE "${columnName}" IS NOT NULL AND "${columnName}"::text != ''`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (companyCode) {
|
||||
params.push(companyCode);
|
||||
sql += ` AND "company_code" = $${params.length}`;
|
||||
}
|
||||
|
||||
sql += ` ORDER BY value LIMIT 500`;
|
||||
|
||||
const rows = await query<{ value: string }>(sql, params);
|
||||
return rows.map((row) => ({
|
||||
value: row.value,
|
||||
label: row.value,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`컬럼 고유값 조회 실패: ${tableName}.${columnName}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user