ERP-node/backend-node/src/services/dataService.ts

638 lines
18 KiB
TypeScript
Raw Normal View History

2025-11-05 15:23:57 +09:00
/**
*
*
* :
* 1. -
* 2. -
* 3. - company_code
* 4. SQL - /
*
* :
* - , ,
* - (pg_*, information_schema )
* - company_code
* - (company_code = "*")
*/
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;
}
/**
2025-11-05 15:23:57 +09:00
* ()
*
2025-09-12 16:47:02 +09:00
*/
2025-11-05 15:23:57 +09:00
const BLOCKED_TABLES = [
"pg_catalog",
"pg_statistic",
"pg_database",
"pg_user",
"information_schema",
"session_tokens", // 세션 토큰 테이블
"password_history", // 패스워드 이력
2025-09-12 16:47:02 +09:00
];
/**
2025-11-05 15:23:57 +09:00
*
* SQL 방지: 영문, ,
2025-09-12 16:47:02 +09:00
*/
2025-11-05 15:23:57 +09:00
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
2025-09-12 16:47:02 +09:00
class DataService {
2025-11-05 15:23:57 +09:00
/**
* ( )
*/
private async validateTableAccess(
tableName: string
): Promise<{ valid: boolean; error?: ServiceResponse<any> }> {
// 1. 테이블명 형식 검증 (SQL 인젝션 방지)
if (!TABLE_NAME_REGEX.test(tableName)) {
return {
valid: false,
error: {
success: false,
message: `유효하지 않은 테이블명입니다: ${tableName}`,
error: "INVALID_TABLE_NAME",
},
};
}
// 2. 블랙리스트 검증
if (BLOCKED_TABLES.includes(tableName)) {
return {
valid: false,
error: {
success: false,
message: `접근이 금지된 테이블입니다: ${tableName}`,
error: "TABLE_ACCESS_DENIED",
},
};
}
// 3. 테이블 존재 여부 확인
const tableExists = await this.checkTableExists(tableName);
if (!tableExists) {
return {
valid: false,
error: {
success: false,
message: `테이블을 찾을 수 없습니다: ${tableName}`,
error: "TABLE_NOT_FOUND",
},
};
}
return { valid: true };
}
2025-09-12 16:47:02 +09:00
/**
*
*/
async getTableData(
params: GetTableDataParams
): Promise<ServiceResponse<any[]>> {
const {
tableName,
limit = 10,
offset = 0,
orderBy,
filters = {},
userCompany,
} = params;
try {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-09-12 16:47:02 +09:00
}
// 동적 SQL 쿼리 생성
let sql = `SELECT * FROM "${tableName}"`;
2025-09-12 16:47:02 +09:00
const queryParams: any[] = [];
let paramIndex = 1;
// WHERE 조건 생성
const whereConditions: string[] = [];
2025-11-05 15:23:57 +09:00
// 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우)
if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code");
if (hasCompanyCode) {
2025-09-12 16:47:02 +09:00
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(userCompany);
paramIndex++;
2025-11-05 15:23:57 +09:00
console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`);
2025-09-12 16:47:02 +09:00
}
}
// 사용자 정의 필터 추가
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) {
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";
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) {
sql += ` ORDER BY "${availableDateColumn}" DESC`;
2025-09-12 16:47:02 +09:00
}
}
// LIMIT과 OFFSET 추가
sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
2025-09-12 16:47:02 +09:00
queryParams.push(limit, offset);
console.log("🔍 실행할 쿼리:", sql);
2025-09-12 16:47:02 +09:00
console.log("📊 쿼리 파라미터:", queryParams);
// 쿼리 실행
const result = await query<any>(sql, queryParams);
2025-09-12 16:47:02 +09:00
return {
success: true,
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 {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-09-12 16:47:02 +09:00
}
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 {
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
)`,
[tableName]
2025-09-12 16:47:02 +09:00
);
return result[0]?.exists || false;
2025-09-12 16:47:02 +09:00
} catch (error) {
console.error("테이블 존재 확인 오류:", error);
return false;
}
}
2025-11-05 15:23:57 +09:00
/**
*
*/
private async checkColumnExists(
tableName: string,
columnName: string
): Promise<boolean> {
try {
const result = await query<{ exists: boolean }>(
`SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
AND column_name = $2
)`,
[tableName, columnName]
);
return result[0]?.exists || false;
} catch (error) {
console.error("컬럼 존재 확인 오류:", error);
return false;
}
}
2025-09-12 16:47:02 +09:00
/**
* ( )
*/
private async getTableColumnsSimple(tableName: string): Promise<any[]> {
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'
ORDER BY ordinal_position`,
[tableName]
2025-09-12 16:47:02 +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 테이블에서 라벨 조회
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
LIMIT 1`,
[tableName, columnName]
2025-09-12 16:47:02 +09:00
);
return result[0]?.label_ko || null;
2025-09-12 16:47:02 +09:00
} catch (error) {
// column_labels 테이블이 없거나 오류가 발생하면 null 반환
return null;
}
}
2025-10-15 17:25:38 +09:00
/**
*
*/
async getRecordDetail(
tableName: string,
id: string | number
): Promise<ServiceResponse<any>> {
try {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-10-15 17:25:38 +09:00
}
// Primary Key 컬럼 찾기
const pkResult = await query<{ attname: string }>(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
let pkColumn = "id"; // 기본값
if (pkResult.length > 0) {
pkColumn = pkResult[0].attname;
}
const queryText = `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = $1`;
const result = await query<any>(queryText, [id]);
if (result.length === 0) {
return {
success: false,
message: "레코드를 찾을 수 없습니다.",
error: "RECORD_NOT_FOUND",
};
}
return {
success: true,
data: result[0],
};
} catch (error) {
console.error(`레코드 상세 조회 오류 (${tableName}/${id}):`, error);
return {
success: false,
message: "레코드 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
*
*/
async getJoinedData(
leftTable: string,
rightTable: string,
leftColumn: string,
rightColumn: string,
leftValue?: string | number,
userCompany?: string
2025-10-15 17:25:38 +09:00
): Promise<ServiceResponse<any[]>> {
try {
2025-11-05 15:23:57 +09:00
// 왼쪽 테이블 접근 검증
const leftValidation = await this.validateTableAccess(leftTable);
if (!leftValidation.valid) {
return leftValidation.error!;
2025-10-15 17:25:38 +09:00
}
2025-11-05 15:23:57 +09:00
// 오른쪽 테이블 접근 검증
const rightValidation = await this.validateTableAccess(rightTable);
if (!rightValidation.valid) {
return rightValidation.error!;
2025-10-15 17:25:38 +09:00
}
let queryText = `
SELECT DISTINCT r.*
2025-10-15 17:25:38 +09:00
FROM "${rightTable}" r
INNER JOIN "${leftTable}" l
ON l."${leftColumn}" = r."${rightColumn}"
`;
const values: any[] = [];
const whereConditions: string[] = [];
let paramIndex = 1;
// 좌측 값 필터링
2025-10-15 17:25:38 +09:00
if (leftValue !== undefined && leftValue !== null) {
whereConditions.push(`l."${leftColumn}" = $${paramIndex}`);
2025-10-15 17:25:38 +09:00
values.push(leftValue);
paramIndex++;
}
// 우측 테이블 회사별 필터링 (company_code 컬럼이 있는 경우)
if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code");
if (hasCompanyCode) {
whereConditions.push(`r.company_code = $${paramIndex}`);
values.push(userCompany);
paramIndex++;
console.log(`🏢 우측 패널 회사별 필터링 적용: ${rightTable}.company_code = ${userCompany}`);
}
2025-10-15 17:25:38 +09:00
}
// WHERE 절 추가
if (whereConditions.length > 0) {
queryText += ` WHERE ${whereConditions.join(" AND ")}`;
}
console.log("🔍 조인 쿼리 실행:", queryText);
console.log("📊 조인 쿼리 파라미터:", values);
2025-10-15 17:25:38 +09:00
const result = await query<any>(queryText, values);
return {
success: true,
data: result,
};
} catch (error) {
console.error(
`조인 데이터 조회 오류 (${leftTable}${rightTable}):`,
error
);
return {
success: false,
message: "조인 데이터 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
*
*/
async createRecord(
tableName: string,
data: Record<string, any>
): Promise<ServiceResponse<any>> {
try {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-10-15 17:25:38 +09:00
}
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
const columnNames = columns.map((col) => `"${col}"`).join(", ");
const queryText = `
INSERT INTO "${tableName}" (${columnNames})
VALUES (${placeholders})
RETURNING *
`;
const result = await query<any>(queryText, values);
return {
success: true,
data: result[0],
};
} catch (error) {
console.error(`레코드 생성 오류 (${tableName}):`, error);
return {
success: false,
message: "레코드 생성 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
*
*/
async updateRecord(
tableName: string,
id: string | number,
data: Record<string, any>
): Promise<ServiceResponse<any>> {
try {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-10-15 17:25:38 +09:00
}
// Primary Key 컬럼 찾기
const pkResult = await query<{ attname: string }>(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
let pkColumn = "id";
if (pkResult.length > 0) {
pkColumn = pkResult[0].attname;
}
const columns = Object.keys(data);
const values = Object.values(data);
const setClause = columns
.map((col, index) => `"${col}" = $${index + 1}`)
.join(", ");
const queryText = `
UPDATE "${tableName}"
SET ${setClause}
WHERE "${pkColumn}" = $${values.length + 1}
RETURNING *
`;
values.push(id);
const result = await query<any>(queryText, values);
if (result.length === 0) {
return {
success: false,
message: "레코드를 찾을 수 없습니다.",
error: "RECORD_NOT_FOUND",
};
}
return {
success: true,
data: result[0],
};
} catch (error) {
console.error(`레코드 수정 오류 (${tableName}/${id}):`, error);
return {
success: false,
message: "레코드 수정 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
*
*/
async deleteRecord(
tableName: string,
id: string | number
): Promise<ServiceResponse<void>> {
try {
2025-11-05 15:23:57 +09:00
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
2025-10-15 17:25:38 +09:00
}
// Primary Key 컬럼 찾기
const pkResult = await query<{ attname: string }>(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
let pkColumn = "id";
if (pkResult.length > 0) {
pkColumn = pkResult[0].attname;
}
const queryText = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
await query<any>(queryText, [id]);
return {
success: true,
};
} catch (error) {
console.error(`레코드 삭제 오류 (${tableName}/${id}):`, error);
return {
success: false,
message: "레코드 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
2025-09-12 16:47:02 +09:00
}
export const dataService = new DataService();