테이블 데이터 필터링 기능 및 textarea컴포넌트 자동 매핑 삭제
This commit is contained in:
@@ -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 사용 권장`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user