2025-10-01 12:27:32 +09:00
|
|
|
import { query, queryOne } from "../database/db";
|
2025-09-12 16:47:02 +09:00
|
|
|
|
|
|
|
|
interface GetTableDataParams {
|
|
|
|
|
tableName: string;
|
|
|
|
|
limit?: number;
|
|
|
|
|
offset?: number;
|
|
|
|
|
orderBy?: string;
|
|
|
|
|
filters?: Record<string, string>;
|
|
|
|
|
userCompany?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ServiceResponse<T> {
|
|
|
|
|
success: boolean;
|
|
|
|
|
data?: T;
|
|
|
|
|
message?: string;
|
|
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 안전한 테이블명 목록 (화이트리스트)
|
|
|
|
|
* SQL 인젝션 방지를 위해 허용된 테이블만 접근 가능
|
|
|
|
|
*/
|
|
|
|
|
const ALLOWED_TABLES = [
|
|
|
|
|
"company_mng",
|
|
|
|
|
"user_info",
|
|
|
|
|
"dept_info",
|
|
|
|
|
"code_info",
|
|
|
|
|
"code_category",
|
|
|
|
|
"menu_info",
|
|
|
|
|
"approval",
|
|
|
|
|
"approval_kind",
|
|
|
|
|
"board",
|
|
|
|
|
"comm_code",
|
|
|
|
|
"product_mng",
|
|
|
|
|
"part_mng",
|
|
|
|
|
"material_mng",
|
|
|
|
|
"order_mng_master",
|
|
|
|
|
"inventory_mng",
|
|
|
|
|
"contract_mgmt",
|
|
|
|
|
"project_mgmt",
|
|
|
|
|
"screen_definitions",
|
|
|
|
|
"screen_layouts",
|
|
|
|
|
"layout_standards",
|
|
|
|
|
"component_standards",
|
|
|
|
|
"web_type_standards",
|
|
|
|
|
"button_action_standards",
|
|
|
|
|
"template_standards",
|
|
|
|
|
"grid_standards",
|
|
|
|
|
"style_templates",
|
|
|
|
|
"multi_lang_key_master",
|
|
|
|
|
"multi_lang_text",
|
|
|
|
|
"language_master",
|
|
|
|
|
"table_labels",
|
|
|
|
|
"column_labels",
|
|
|
|
|
"dynamic_form_data",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 회사별 필터링이 필요한 테이블 목록
|
|
|
|
|
*/
|
|
|
|
|
const COMPANY_FILTERED_TABLES = [
|
|
|
|
|
"company_mng",
|
|
|
|
|
"user_info",
|
|
|
|
|
"dept_info",
|
|
|
|
|
"approval",
|
|
|
|
|
"board",
|
|
|
|
|
"product_mng",
|
|
|
|
|
"part_mng",
|
|
|
|
|
"material_mng",
|
|
|
|
|
"order_mng_master",
|
|
|
|
|
"inventory_mng",
|
|
|
|
|
"contract_mgmt",
|
|
|
|
|
"project_mgmt",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
class DataService {
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 데이터 조회
|
|
|
|
|
*/
|
|
|
|
|
async getTableData(
|
|
|
|
|
params: GetTableDataParams
|
|
|
|
|
): Promise<ServiceResponse<any[]>> {
|
|
|
|
|
const {
|
|
|
|
|
tableName,
|
|
|
|
|
limit = 10,
|
|
|
|
|
offset = 0,
|
|
|
|
|
orderBy,
|
|
|
|
|
filters = {},
|
|
|
|
|
userCompany,
|
|
|
|
|
} = params;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 테이블명 화이트리스트 검증
|
|
|
|
|
if (!ALLOWED_TABLES.includes(tableName)) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
|
|
|
|
error: "TABLE_NOT_ALLOWED",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 테이블 존재 여부 확인
|
|
|
|
|
const tableExists = await this.checkTableExists(tableName);
|
|
|
|
|
if (!tableExists) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `테이블을 찾을 수 없습니다: ${tableName}`,
|
|
|
|
|
error: "TABLE_NOT_FOUND",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 동적 SQL 쿼리 생성
|
2025-10-01 12:27:32 +09:00
|
|
|
let sql = `SELECT * FROM "${tableName}"`;
|
2025-09-12 16:47:02 +09:00
|
|
|
const queryParams: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
// WHERE 조건 생성
|
|
|
|
|
const whereConditions: string[] = [];
|
|
|
|
|
|
|
|
|
|
// 회사별 필터링 추가
|
|
|
|
|
if (COMPANY_FILTERED_TABLES.includes(tableName) && userCompany) {
|
|
|
|
|
// 슈퍼관리자(*)가 아닌 경우에만 회사 필터 적용
|
|
|
|
|
if (userCompany !== "*") {
|
|
|
|
|
whereConditions.push(`company_code = $${paramIndex}`);
|
|
|
|
|
queryParams.push(userCompany);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 사용자 정의 필터 추가
|
|
|
|
|
for (const [key, value] of Object.entries(filters)) {
|
|
|
|
|
if (
|
|
|
|
|
value &&
|
|
|
|
|
key !== "limit" &&
|
|
|
|
|
key !== "offset" &&
|
|
|
|
|
key !== "orderBy" &&
|
|
|
|
|
key !== "userLang"
|
|
|
|
|
) {
|
|
|
|
|
// 컬럼명 검증 (SQL 인젝션 방지)
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
|
|
|
continue; // 유효하지 않은 컬럼명은 무시
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
whereConditions.push(`"${key}" ILIKE $${paramIndex}`);
|
|
|
|
|
queryParams.push(`%${value}%`);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WHERE 절 추가
|
|
|
|
|
if (whereConditions.length > 0) {
|
2025-10-01 12:27:32 +09:00
|
|
|
sql += ` WHERE ${whereConditions.join(" AND ")}`;
|
2025-09-12 16:47:02 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ORDER BY 절 추가
|
|
|
|
|
if (orderBy) {
|
|
|
|
|
// ORDER BY 검증 (SQL 인젝션 방지)
|
|
|
|
|
const orderParts = orderBy.split(" ");
|
|
|
|
|
const columnName = orderParts[0];
|
|
|
|
|
const direction = orderParts[1]?.toUpperCase();
|
|
|
|
|
|
|
|
|
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
|
|
|
|
|
const validDirection = direction === "DESC" ? "DESC" : "ASC";
|
2025-10-01 12:27:32 +09:00
|
|
|
sql += ` ORDER BY "${columnName}" ${validDirection}`;
|
2025-09-12 16:47:02 +09:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 기본 정렬: 최신순 (가능한 컬럼 시도)
|
|
|
|
|
const dateColumns = [
|
|
|
|
|
"created_date",
|
|
|
|
|
"regdate",
|
|
|
|
|
"reg_date",
|
|
|
|
|
"updated_date",
|
|
|
|
|
"upd_date",
|
|
|
|
|
];
|
|
|
|
|
const tableColumns = await this.getTableColumnsSimple(tableName);
|
|
|
|
|
const availableDateColumn = dateColumns.find((col) =>
|
|
|
|
|
tableColumns.some((tableCol) => tableCol.column_name === col)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (availableDateColumn) {
|
2025-10-01 12:27:32 +09:00
|
|
|
sql += ` ORDER BY "${availableDateColumn}" DESC`;
|
2025-09-12 16:47:02 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LIMIT과 OFFSET 추가
|
2025-10-01 12:27:32 +09:00
|
|
|
sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
2025-09-12 16:47:02 +09:00
|
|
|
queryParams.push(limit, offset);
|
|
|
|
|
|
2025-10-01 12:27:32 +09:00
|
|
|
console.log("🔍 실행할 쿼리:", sql);
|
2025-09-12 16:47:02 +09:00
|
|
|
console.log("📊 쿼리 파라미터:", queryParams);
|
|
|
|
|
|
|
|
|
|
// 쿼리 실행
|
2025-10-01 12:27:32 +09:00
|
|
|
const result = await query<any>(sql, queryParams);
|
2025-09-12 16:47:02 +09:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
2025-10-01 12:27:32 +09:00
|
|
|
data: result,
|
2025-09-12 16:47:02 +09:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`데이터 조회 오류 (${tableName}):`, error);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: "데이터 조회 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 컬럼 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
async getTableColumns(tableName: string): Promise<ServiceResponse<any[]>> {
|
|
|
|
|
try {
|
|
|
|
|
// 테이블명 화이트리스트 검증
|
|
|
|
|
if (!ALLOWED_TABLES.includes(tableName)) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
|
|
|
|
error: "TABLE_NOT_ALLOWED",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columns = await this.getTableColumnsSimple(tableName);
|
|
|
|
|
|
|
|
|
|
// 컬럼 라벨 정보 추가
|
|
|
|
|
const columnsWithLabels = await Promise.all(
|
|
|
|
|
columns.map(async (column) => {
|
|
|
|
|
const label = await this.getColumnLabel(
|
|
|
|
|
tableName,
|
|
|
|
|
column.column_name
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
columnName: column.column_name,
|
|
|
|
|
columnLabel: label || column.column_name,
|
|
|
|
|
dataType: column.data_type,
|
|
|
|
|
isNullable: column.is_nullable === "YES",
|
|
|
|
|
defaultValue: column.column_default,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: columnsWithLabels,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`컬럼 정보 조회 오류 (${tableName}):`, error);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 존재 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
private async checkTableExists(tableName: string): Promise<boolean> {
|
|
|
|
|
try {
|
2025-10-01 12:27:32 +09:00
|
|
|
const result = await query<{ exists: boolean }>(
|
|
|
|
|
`SELECT EXISTS (
|
2025-09-12 16:47:02 +09:00
|
|
|
SELECT FROM information_schema.tables
|
|
|
|
|
WHERE table_schema = 'public'
|
|
|
|
|
AND table_name = $1
|
2025-10-01 12:27:32 +09:00
|
|
|
)`,
|
|
|
|
|
[tableName]
|
2025-09-12 16:47:02 +09:00
|
|
|
);
|
|
|
|
|
|
2025-10-01 12:27:32 +09:00
|
|
|
return result[0]?.exists || false;
|
2025-09-12 16:47:02 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("테이블 존재 확인 오류:", error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 컬럼 정보 조회 (간단 버전)
|
|
|
|
|
*/
|
|
|
|
|
private async getTableColumnsSimple(tableName: string): Promise<any[]> {
|
2025-10-01 12:27:32 +09:00
|
|
|
const result = await query<any>(
|
|
|
|
|
`SELECT column_name, data_type, is_nullable, column_default
|
2025-09-12 16:47:02 +09:00
|
|
|
FROM information_schema.columns
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
AND table_schema = 'public'
|
2025-10-01 12:27:32 +09:00
|
|
|
ORDER BY ordinal_position`,
|
|
|
|
|
[tableName]
|
2025-09-12 16:47:02 +09:00
|
|
|
);
|
|
|
|
|
|
2025-10-01 12:27:32 +09:00
|
|
|
return result;
|
2025-09-12 16:47:02 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 라벨 조회
|
|
|
|
|
*/
|
|
|
|
|
private async getColumnLabel(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
try {
|
|
|
|
|
// column_labels 테이블에서 라벨 조회
|
2025-10-01 12:27:32 +09:00
|
|
|
const result = await query<{ label_ko: string }>(
|
|
|
|
|
`SELECT label_ko
|
2025-09-12 16:47:02 +09:00
|
|
|
FROM column_labels
|
|
|
|
|
WHERE table_name = $1 AND column_name = $2
|
2025-10-01 12:27:32 +09:00
|
|
|
LIMIT 1`,
|
|
|
|
|
[tableName, columnName]
|
2025-09-12 16:47:02 +09:00
|
|
|
);
|
|
|
|
|
|
2025-10-01 12:27:32 +09:00
|
|
|
return result[0]?.label_ko || null;
|
2025-09-12 16:47:02 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
// column_labels 테이블이 없거나 오류가 발생하면 null 반환
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const dataService = new DataService();
|