테이블 데이터 필터링 기능 및 textarea컴포넌트 자동 매핑 삭제

This commit is contained in:
kjs
2025-11-13 17:06:41 +09:00
parent a828f54663
commit 296ee3e825
17 changed files with 941 additions and 98 deletions

View File

@@ -128,7 +128,8 @@ export class TableManagementService {
);
// 캐시 키 생성 (companyCode 포함)
const cacheKey = CacheKeys.TABLE_COLUMNS(tableName, page, size) + `_${companyCode}`;
const cacheKey =
CacheKeys.TABLE_COLUMNS(tableName, page, size) + `_${companyCode}`;
const countCacheKey = CacheKeys.TABLE_COLUMN_COUNT(tableName);
// 캐시에서 먼저 확인
@@ -162,9 +163,9 @@ export class TableManagementService {
// 페이지네이션 적용한 컬럼 조회
const offset = (page - 1) * size;
// 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기
const rawColumns = companyCode
const rawColumns = companyCode
? await query<any>(
`SELECT
c.column_name as "columnName",
@@ -260,8 +261,11 @@ export class TableManagementService {
let categoryMappings: Map<string, number[]> = new Map();
if (mappingTableExists && companyCode) {
logger.info("📥 getColumnList: 카테고리 매핑 조회 시작", { tableName, companyCode });
logger.info("📥 getColumnList: 카테고리 매핑 조회 시작", {
tableName,
companyCode,
});
const mappings = await query<any>(
`SELECT
logical_column_name as "columnName",
@@ -272,11 +276,11 @@ export class TableManagementService {
[tableName, companyCode]
);
logger.info("✅ getColumnList: 카테고리 매핑 조회 완료", {
tableName,
companyCode,
logger.info("✅ getColumnList: 카테고리 매핑 조회 완료", {
tableName,
companyCode,
mappingCount: mappings.length,
mappings: mappings
mappings: mappings,
});
mappings.forEach((m: any) => {
@@ -288,7 +292,7 @@ export class TableManagementService {
logger.info("✅ getColumnList: categoryMappings Map 생성 완료", {
size: categoryMappings.size,
entries: Array.from(categoryMappings.entries())
entries: Array.from(categoryMappings.entries()),
});
}
@@ -300,19 +304,27 @@ export class TableManagementService {
numericPrecision: column.numericPrecision
? Number(column.numericPrecision)
: null,
numericScale: column.numericScale ? Number(column.numericScale) : null,
displayOrder: column.displayOrder ? Number(column.displayOrder) : null,
// 자동 매핑: webType이 기본값('text')인 경우 DB 타입에 따라 자동 추론
webType:
column.webType === "text"
? this.inferWebType(column.dataType)
: column.webType,
numericScale: column.numericScale
? Number(column.numericScale)
: null,
displayOrder: column.displayOrder
? Number(column.displayOrder)
: null,
// webType은 사용자가 명시적으로 설정한 값을 그대로 사용
// (자동 추론은 column_labels에 없는 경우에만 SQL 쿼리의 COALESCE에서 처리됨)
webType: column.webType,
};
// 카테고리 타입인 경우 categoryMenus 추가
if (column.inputType === "category" && categoryMappings.has(column.columnName)) {
if (
column.inputType === "category" &&
categoryMappings.has(column.columnName)
) {
const menus = categoryMappings.get(column.columnName);
logger.info(`✅ getColumnList: 컬럼 ${column.columnName}에 카테고리 메뉴 추가`, { menus });
logger.info(
`✅ getColumnList: 컬럼 ${column.columnName}에 카테고리 메뉴 추가`,
{ menus }
);
return {
...baseColumn,
categoryMenus: menus,
@@ -417,7 +429,9 @@ export class TableManagementService {
companyCode: string // 🔥 회사 코드 추가
): Promise<void> {
try {
logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode}`);
logger.info(
`컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode}`
);
// 테이블이 table_labels에 없으면 자동 추가
await this.insertTableIfNotExists(tableName);
@@ -463,17 +477,22 @@ export class TableManagementService {
// detailSettings가 문자열이면 파싱, 객체면 그대로 사용
let parsedDetailSettings: Record<string, any> | undefined = undefined;
if (settings.detailSettings) {
if (typeof settings.detailSettings === 'string') {
if (typeof settings.detailSettings === "string") {
try {
parsedDetailSettings = JSON.parse(settings.detailSettings);
} catch (e) {
logger.warn(`detailSettings 파싱 실패, 그대로 사용: ${settings.detailSettings}`);
logger.warn(
`detailSettings 파싱 실패, 그대로 사용: ${settings.detailSettings}`
);
}
} else if (typeof settings.detailSettings === 'object') {
parsedDetailSettings = settings.detailSettings as Record<string, any>;
} else if (typeof settings.detailSettings === "object") {
parsedDetailSettings = settings.detailSettings as Record<
string,
any
>;
}
}
await this.updateColumnInputType(
tableName,
columnName,
@@ -486,7 +505,7 @@ export class TableManagementService {
// 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
cache.deleteByPattern(`table_columns:${tableName}:`);
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
} catch (error) {
logger.error(
@@ -1133,7 +1152,7 @@ export class TableManagementService {
if (typeof value === "object" && value !== null && "value" in value) {
actualValue = value.value;
operator = value.operator || "contains";
logger.info("🔍 필터 객체 처리:", {
columnName,
originalValue: value,
@@ -1180,11 +1199,19 @@ export class TableManagementService {
switch (webType) {
case "date":
case "datetime":
return this.buildDateRangeCondition(columnName, actualValue, paramIndex);
return this.buildDateRangeCondition(
columnName,
actualValue,
paramIndex
);
case "number":
case "decimal":
return this.buildNumberRangeCondition(columnName, actualValue, paramIndex);
return this.buildNumberRangeCondition(
columnName,
actualValue,
paramIndex
);
case "code":
return await this.buildCodeSearchCondition(
@@ -1220,7 +1247,7 @@ export class TableManagementService {
if (typeof value === "object" && value !== null && "value" in value) {
fallbackValue = value.value;
}
return {
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
values: [`%${fallbackValue}%`],
@@ -1583,6 +1610,7 @@ export class TableManagementService {
sortBy?: string;
sortOrder?: string;
companyCode?: string;
dataFilter?: any; // 🆕 DataFilterConfig
}
): Promise<{
data: any[];
@@ -1592,7 +1620,15 @@ export class TableManagementService {
totalPages: number;
}> {
try {
const { page, size, search = {}, sortBy, sortOrder = "asc", companyCode } = options;
const {
page,
size,
search = {},
sortBy,
sortOrder = "asc",
companyCode,
dataFilter,
} = options;
const offset = (page - 1) * size;
logger.info(`테이블 데이터 조회: ${tableName}`, options);
@@ -1611,7 +1647,9 @@ export class TableManagementService {
whereConditions.push(`company_code = $${paramIndex}`);
searchValues.push(companyCode);
paramIndex++;
logger.info(`🔒 멀티테넌시 필터 추가 (기본 조회): company_code = ${companyCode}`);
logger.info(
`🔒 멀티테넌시 필터 추가 (기본 조회): company_code = ${companyCode}`
);
}
if (search && Object.keys(search).length > 0) {
@@ -1649,6 +1687,29 @@ export class TableManagementService {
}
}
// 🆕 데이터 필터 적용
if (
dataFilter &&
dataFilter.enabled &&
dataFilter.filters &&
dataFilter.filters.length > 0
) {
const {
buildDataFilterWhereClause,
} = require("../utils/dataFilterUtil");
const { whereClause: filterWhere, params: filterParams } =
buildDataFilterWhereClause(dataFilter, paramIndex);
if (filterWhere) {
whereConditions.push(filterWhere);
searchValues.push(...filterParams);
paramIndex += filterParams.length;
logger.info(`🔍 데이터 필터 적용: ${filterWhere}`);
logger.info(`🔍 필터 파라미터:`, filterParams);
}
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
@@ -1680,7 +1741,9 @@ export class TableManagementService {
`;
logger.info(`🔍 실행할 SQL: ${dataQuery}`);
logger.info(`🔍 파라미터: ${JSON.stringify([...searchValues, size, offset])}`);
logger.info(
`🔍 파라미터: ${JSON.stringify([...searchValues, size, offset])}`
);
let data = await query<any>(dataQuery, [...searchValues, size, offset]);
@@ -2152,6 +2215,7 @@ export class TableManagementService {
joinAlias: string;
}>;
screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
dataFilter?: any; // 🆕 데이터 필터
}
): Promise<EntityJoinResponse> {
const startTime = Date.now();
@@ -2311,18 +2375,99 @@ export class TableManagementService {
const selectColumns = columns.data.map((col: any) => col.column_name);
// WHERE 절 구성
let whereClause = await this.buildWhereClause(
tableName,
options.search
);
let whereClause = await this.buildWhereClause(tableName, options.search);
// 멀티테넌시 필터 추가 (company_code)
if (options.companyCode) {
const companyFilter = `main.company_code = '${options.companyCode.replace(/'/g, "''")}'`;
whereClause = whereClause
? `${whereClause} AND ${companyFilter}`
whereClause = whereClause
? `${whereClause} AND ${companyFilter}`
: companyFilter;
logger.info(`🔒 멀티테넌시 필터 추가 (Entity 조인): company_code = ${options.companyCode}`);
logger.info(
`🔒 멀티테넌시 필터 추가 (Entity 조인): company_code = ${options.companyCode}`
);
}
// 🆕 데이터 필터 적용 (Entity 조인) - 파라미터 바인딩 없이 직접 값 삽입
if (
options.dataFilter &&
options.dataFilter.enabled &&
options.dataFilter.filters &&
options.dataFilter.filters.length > 0
) {
const filterConditions: string[] = [];
for (const filter of options.dataFilter.filters) {
const { columnName, operator, value } = filter;
if (!columnName || value === undefined || value === null) {
continue;
}
const safeColumn = `main."${columnName}"`;
switch (operator) {
case "equals":
filterConditions.push(
`${safeColumn} = '${String(value).replace(/'/g, "''")}'`
);
break;
case "not_equals":
filterConditions.push(
`${safeColumn} != '${String(value).replace(/'/g, "''")}'`
);
break;
case "in":
if (Array.isArray(value) && value.length > 0) {
const values = value
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
filterConditions.push(`${safeColumn} IN (${values})`);
}
break;
case "not_in":
if (Array.isArray(value) && value.length > 0) {
const values = value
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
filterConditions.push(`${safeColumn} NOT IN (${values})`);
}
break;
case "contains":
filterConditions.push(
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}%'`
);
break;
case "starts_with":
filterConditions.push(
`${safeColumn} LIKE '${String(value).replace(/'/g, "''")}%'`
);
break;
case "ends_with":
filterConditions.push(
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}'`
);
break;
case "is_null":
filterConditions.push(`${safeColumn} IS NULL`);
break;
case "is_not_null":
filterConditions.push(`${safeColumn} IS NOT NULL`);
break;
}
}
if (filterConditions.length > 0) {
const logicalOperator =
options.dataFilter.matchType === "any" ? " OR " : " AND ";
const filterWhere = `(${filterConditions.join(logicalOperator)})`;
whereClause = whereClause
? `${whereClause} AND ${filterWhere}`
: filterWhere;
logger.info(`🔍 데이터 필터 적용 (Entity 조인): ${filterWhere}`);
}
}
// ORDER BY 절 구성
@@ -2412,8 +2557,10 @@ export class TableManagementService {
query(dataQuery),
query(countQuery),
]);
logger.info(`✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`);
logger.info(
`✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`
);
const data = Array.isArray(dataResult) ? dataResult : [];
const total =
@@ -2643,11 +2790,17 @@ export class TableManagementService {
);
}
basicResult = await this.getTableData(tableName, { ...fallbackOptions, companyCode: options.companyCode });
basicResult = await this.getTableData(tableName, {
...fallbackOptions,
companyCode: options.companyCode,
});
}
} else {
// Entity 조인 컬럼 검색이 없는 경우 기존 캐시 방식 사용
basicResult = await this.getTableData(tableName, { ...options, companyCode: options.companyCode });
basicResult = await this.getTableData(tableName, {
...options,
companyCode: options.companyCode,
});
}
// Entity 값들을 캐시에서 룩업하여 변환
@@ -2921,13 +3074,20 @@ export class TableManagementService {
// 모든 조인이 캐시 가능한 경우: 기본 쿼리 + 캐시 룩업
else {
// whereClause에서 company_code 추출 (멀티테넌시 필터)
const companyCodeMatch = whereClause.match(/main\.company_code\s*=\s*'([^']+)'/);
const companyCodeMatch = whereClause.match(
/main\.company_code\s*=\s*'([^']+)'/
);
const companyCode = companyCodeMatch ? companyCodeMatch[1] : undefined;
return await this.executeCachedLookup(
tableName,
cacheableJoins,
{ page: Math.floor(offset / limit) + 1, size: limit, search: {}, companyCode },
{
page: Math.floor(offset / limit) + 1,
size: limit,
search: {},
companyCode,
},
startTime
);
}
@@ -2949,7 +3109,7 @@ export class TableManagementService {
for (const config of joinConfigs) {
// table_column_category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
if (config.referenceTable === 'table_column_category_values') {
if (config.referenceTable === "table_column_category_values") {
dbJoins.push(config);
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
continue;
@@ -3227,7 +3387,7 @@ export class TableManagementService {
let categoryMappings: Map<string, number[]> = new Map();
if (mappingTableExists) {
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
const mappings = await query<any>(
`SELECT
logical_column_name as "columnName",
@@ -3238,11 +3398,11 @@ export class TableManagementService {
[tableName, companyCode]
);
logger.info("카테고리 매핑 조회 완료", {
tableName,
companyCode,
logger.info("카테고리 매핑 조회 완료", {
tableName,
companyCode,
mappingCount: mappings.length,
mappings: mappings
mappings: mappings,
});
mappings.forEach((m: any) => {
@@ -3254,7 +3414,7 @@ export class TableManagementService {
logger.info("categoryMappings Map 생성 완료", {
size: categoryMappings.size,
entries: Array.from(categoryMappings.entries())
entries: Array.from(categoryMappings.entries()),
});
} else {
logger.warn("category_column_mapping 테이블이 존재하지 않음");
@@ -3276,9 +3436,14 @@ export class TableManagementService {
};
// 카테고리 타입인 경우 categoryMenus 추가
if (col.inputType === "category" && categoryMappings.has(col.columnName)) {
if (
col.inputType === "category" &&
categoryMappings.has(col.columnName)
) {
const menus = categoryMappings.get(col.columnName);
logger.info(`✅ 컬럼 ${col.columnName}에 카테고리 메뉴 추가`, { menus });
logger.info(`✅ 컬럼 ${col.columnName}에 카테고리 메뉴 추가`, {
menus,
});
return {
...baseInfo,
categoryMenus: menus,
@@ -3309,7 +3474,10 @@ export class TableManagementService {
* 레거시 지원: 컬럼 웹타입 정보 조회
* @deprecated getColumnInputTypes 사용 권장
*/
async getColumnWebTypes(tableName: string, companyCode: string): Promise<ColumnTypeInfo[]> {
async getColumnWebTypes(
tableName: string,
companyCode: string
): Promise<ColumnTypeInfo[]> {
logger.warn(
`레거시 메서드 사용: getColumnWebTypes → getColumnInputTypes 사용 권장`
);