카테고리 기능 구현

This commit is contained in:
kjs
2025-11-05 15:23:57 +09:00
parent f4fd1184cd
commit 573a300a4a
35 changed files with 9577 additions and 131 deletions

View File

@@ -1,3 +1,18 @@
/**
* 동적 데이터 서비스
*
* 주요 특징:
* 1. 화이트리스트 제거 - 모든 테이블에 동적으로 접근 가능
* 2. 블랙리스트 방식 - 시스템 중요 테이블만 접근 금지
* 3. 자동 회사별 필터링 - company_code 컬럼 자동 감지 및 필터 적용
* 4. SQL 인젝션 방지 - 정규식 기반 테이블명/컬럼명 검증
*
* 보안:
* - 테이블명은 영문, 숫자, 언더스코어만 허용
* - 시스템 테이블(pg_*, information_schema 등) 접근 금지
* - company_code 컬럼이 있는 테이블은 자동으로 회사별 격리
* - 최고 관리자(company_code = "*")만 전체 데이터 조회 가능
*/
import { query, queryOne } from "../database/db";
interface GetTableDataParams {
@@ -17,65 +32,72 @@ interface ServiceResponse<T> {
}
/**
* 안전한 테이블 목록 (화이트리스트)
* 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",
"work_history", // 작업 이력 테이블
"delivery_status", // 배송 현황 테이블
const BLOCKED_TABLES = [
"pg_catalog",
"pg_statistic",
"pg_database",
"pg_user",
"information_schema",
"session_tokens", // 세션 토큰 테이블
"password_history", // 패스워드 이력
];
/**
* 회사별 필터링이 필요한 테이블 목록
* 테이블 이름 검증 정규식
* SQL 인젝션 방지: 영문, 숫자, 언더스코어만 허용
*/
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",
];
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
class DataService {
/**
* 테이블 접근 검증 (공통 메서드)
*/
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 };
}
/**
* 테이블 데이터 조회
*/
@@ -92,23 +114,10 @@ class DataService {
} = 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",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
// 동적 SQL 쿼리 생성
@@ -119,13 +128,14 @@ class DataService {
// WHERE 조건 생성
const whereConditions: string[] = [];
// 회사별 필터링 추가
if (COMPANY_FILTERED_TABLES.includes(tableName) && userCompany) {
// 슈퍼관리자(*)가 아닌 경우에만 회사 필터 적용
if (userCompany !== "*") {
// 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우)
if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code");
if (hasCompanyCode) {
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(userCompany);
paramIndex++;
console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`);
}
}
@@ -213,13 +223,10 @@ class DataService {
*/
async getTableColumns(tableName: string): Promise<ServiceResponse<any[]>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(tableName)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
error: "TABLE_NOT_ALLOWED",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
const columns = await this.getTableColumnsSimple(tableName);
@@ -276,6 +283,31 @@ class DataService {
}
}
/**
* 특정 컬럼 존재 여부 확인
*/
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;
}
}
/**
* 테이블 컬럼 정보 조회 (간단 버전)
*/
@@ -324,13 +356,10 @@ class DataService {
id: string | number
): Promise<ServiceResponse<any>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(tableName)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
error: "TABLE_NOT_ALLOWED",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
// Primary Key 컬럼 찾기
@@ -383,21 +412,16 @@ class DataService {
leftValue?: string | number
): Promise<ServiceResponse<any[]>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(leftTable)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${leftTable}`,
error: "TABLE_NOT_ALLOWED",
};
// 왼쪽 테이블 접근 검증
const leftValidation = await this.validateTableAccess(leftTable);
if (!leftValidation.valid) {
return leftValidation.error!;
}
if (!ALLOWED_TABLES.includes(rightTable)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${rightTable}`,
error: "TABLE_NOT_ALLOWED",
};
// 오른쪽 테이블 접근 검증
const rightValidation = await this.validateTableAccess(rightTable);
if (!rightValidation.valid) {
return rightValidation.error!;
}
let queryText = `
@@ -440,13 +464,10 @@ class DataService {
data: Record<string, any>
): Promise<ServiceResponse<any>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(tableName)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
error: "TABLE_NOT_ALLOWED",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
const columns = Object.keys(data);
@@ -485,13 +506,10 @@ class DataService {
data: Record<string, any>
): Promise<ServiceResponse<any>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(tableName)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
error: "TABLE_NOT_ALLOWED",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
// Primary Key 컬럼 찾기
@@ -554,13 +572,10 @@ class DataService {
id: string | number
): Promise<ServiceResponse<void>> {
try {
// 테이블명 화이트리스트 검증
if (!ALLOWED_TABLES.includes(tableName)) {
return {
success: false,
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
error: "TABLE_NOT_ALLOWED",
};
// 테이블 접근 검증
const validation = await this.validateTableAccess(tableName);
if (!validation.valid) {
return validation.error!;
}
// Primary Key 컬럼 찾기