Files
vexplor/동적_테이블_접근_시스템_개선_완료.md
kjs e9268b3f00 feat: 선택항목 상세입력 컴포넌트 그룹별 독립 입력 구조로 개선
- 데이터 구조 변경: ItemData.details → ItemData.fieldGroups (그룹별 관리)
- 각 필드 그룹마다 독립적으로 여러 항목 추가/수정/삭제 가능
- renderFieldsByGroup: 그룹별 입력 항목 목록 + 편집 + 추가 버튼 구현
- renderGridLayout/renderCardLayout: 품목별 그룹 카드 표시로 단순화
- handleFieldChange: groupId 파라미터 추가 (itemId, groupId, entryId, fieldName, value)
- handleAddGroupEntry, handleRemoveGroupEntry, handleEditGroupEntry 핸들러 추가
- buttonActions handleBatchSave: fieldGroups 구조 처리하도록 수정
- 원본 데이터 표시 버그 수정: modalData의 중첩 구조 처리

사용 예:
- 품목 1
  - 그룹 1 (거래처 정보): 3개 항목 입력 가능
  - 그룹 2 (단가 정보): 5개 항목 입력 가능
- 각 항목 클릭 → 수정 가능
- 저장 시 모든 입력 항목이 개별 레코드로 저장됨
2025-11-18 09:56:49 +09:00

9.1 KiB

동적 테이블 접근 시스템 개선 완료

작성일: 2025-01-04
목적: 화이트리스트 제거 및 동적 테이블 접근 시스템 구축


문제 상황

기존 시스템의 문제점

// ❌ 기존 방식: 하드코딩된 화이트리스트
const ALLOWED_TABLES = [
  "company_mng",
  "user_info",
  "dept_info",
  "item_info",  // 매번 수동으로 추가해야 함!
  // ... 계속 추가해야 함
];

// 문제:
// 1. 새 테이블 생성 시마다 코드 수정 필요
// 2. 동적 테이블 생성 기능과 충돌
// 3. 유지보수 어려움
// 4. 확장성 부족

발생한 에러

GET /api/data/item_info?page=1&size=100&userLang=KR
-> 400 Bad Request
-> 접근이 허용되지 않은 테이블입니다: item_info

개선된 시스템

1. 블랙리스트 방식으로 전환

/**
 * 접근 금지 테이블 목록 (블랙리스트)
 * 시스템 중요 테이블 및 보안상 접근 금지할 테이블만 명시
 */
const BLOCKED_TABLES = [
  "pg_catalog",
  "pg_statistic",
  "pg_database",
  "pg_user",
  "information_schema",
  "session_tokens",     // 세션 토큰 테이블
  "password_history",   // 패스워드 이력
];

// ✅ 장점:
// - 금지할 테이블만 명시 (시스템 테이블)
// - 비즈니스 테이블은 자유롭게 추가 가능
// - 코드 수정 불필요

2. 테이블명 검증 강화

/**
 * 테이블 이름 검증 정규식
 * SQL 인젝션 방지: 영문, 숫자, 언더스코어만 허용
 */
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

// 검증 순서:
// 1. 정규식으로 형식 검증 (SQL 인젝션 방지)
// 2. 블랙리스트 확인 (시스템 테이블 차단)
// 3. 테이블 존재 여부 확인 (실제 존재하는 테이블만)

3. 자동 회사별 필터링

// ✅ 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}`);
  }
}

// 동작 방식:
// - company_code 컬럼이 있으면 자동으로 필터링 적용
// - 최고 관리자(company_code = "*")는 전체 데이터 조회 가능
// - 일반 사용자는 자기 회사 데이터만 조회

4. 공통 검증 메서드

/**
 * 테이블 접근 검증 (공통 메서드)
 */
private async validateTableAccess(
  tableName: string
): Promise<{ valid: boolean; error?: ServiceResponse<any> }> {
  // 1. 테이블명 형식 검증 (SQL 인젝션 방지)
  if (!TABLE_NAME_REGEX.test(tableName)) {
    return { valid: false, error: { /* ... */ } };
  }

  // 2. 블랙리스트 검증
  if (BLOCKED_TABLES.includes(tableName)) {
    return { valid: false, error: { /* ... */ } };
  }

  // 3. 테이블 존재 여부 확인
  const tableExists = await this.checkTableExists(tableName);
  if (!tableExists) {
    return { valid: false, error: { /* ... */ } };
  }

  return { valid: true };
}

// 모든 메서드에서 재사용:
// - getTableData()
// - getTableColumns()
// - getRecordDetail()
// - createRecord()
// - updateRecord()
// - deleteRecord()
// - getJoinedData()

개선 효과

Before (화이트리스트 방식)

// 1. item_info 테이블 생성
CREATE TABLE item_info (...);

// 2. 백엔드 코드 수정 필요 ❌
const ALLOWED_TABLES = [
  // ...기존 테이블들
  "item_info",  // 수동으로 추가!
];

const COMPANY_FILTERED_TABLES = [
  // ...기존 테이블들
  "item_info",  // 또 추가!
];

// 3. 서버 재시작 필요
// 4. 테스트

After (블랙리스트 방식)

// 1. item_info 테이블 생성
CREATE TABLE item_info (
  id SERIAL PRIMARY KEY,
  company_code VARCHAR(20) NOT NULL,  --  컬럼만 있으면 자동 필터링!
  name VARCHAR(100),
  ...
);

// 2. 코드 수정 불필요 ✅
// 3. 서버 재시작 불필요 ✅
// 4. 즉시 사용 가능 ✅

보안 강화

1. SQL 인젝션 방지

// ❌ 위험한 테이블명
"user_info; DROP TABLE users; --"  -> 정규식 검증 실패
"../../etc/passwd"                  -> 정규식 검증 실패
"pg_user"                           -> 블랙리스트 차단

// ✅ 안전한 테이블명
"user_info"      -> 통과
"item_info"      -> 통과
"order_mng_001"  -> 통과

2. 시스템 테이블 보호

const BLOCKED_TABLES = [
  "pg_catalog",         // PostgreSQL 카탈로그
  "pg_statistic",       // 통계 정보
  "pg_database",        // 데이터베이스 목록
  "pg_user",            // 사용자 정보
  "information_schema", // 스키마 정보
  "session_tokens",     // 세션 토큰
  "password_history",   // 패스워드 이력
];

3. 멀티테넌시 자동 적용

// 테이블에 company_code 컬럼이 있으면 자동으로:

// 일반 사용자 (company_code = "COMPANY_A")
SELECT * FROM item_info WHERE company_code = 'COMPANY_A';

// 최고 관리자 (company_code = "*")
SELECT * FROM item_info;  -- 모든 회사 데이터 조회 가능

사용 예시

1. 새 테이블 생성

-- 회사별 데이터 격리가 필요한 테이블
CREATE TABLE product_catalog (
  id SERIAL PRIMARY KEY,
  company_code VARCHAR(20) NOT NULL,  -- 자동 필터링 활성화
  product_name VARCHAR(100),
  price DECIMAL(10, 2),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 전역 공통 테이블 (회사별 격리 불필요)
CREATE TABLE global_settings (
  id SERIAL PRIMARY KEY,
  setting_key VARCHAR(50),
  setting_value TEXT
);

2. API 호출

// 프론트엔드에서 그냥 호출하면 끝!
const response = await apiClient.get("/api/data/product_catalog", {
  params: { page: 1, size: 100 }
});

// 백엔드에서 자동으로:
// 1. 테이블 존재 확인 ✓
// 2. company_code 컬럼 확인 ✓
// 3. 회사별 필터링 적용 ✓
// 4. 데이터 반환 ✓

3. 동적 테이블 생성 (DDL API 연동)

// 1. DDL API로 테이블 생성
POST /api/ddl/tables
{
  "tableName": "customer_feedback",
  "columns": [
    { "name": "company_code", "type": "VARCHAR(20)", "nullable": false },
    { "name": "feedback_text", "type": "TEXT" },
    { "name": "rating", "type": "INTEGER" }
  ]
}

// 2. 즉시 데이터 조회 가능 (코드 수정 없음)
GET /api/data/customer_feedback

변경된 파일

backend-node/src/services/dataService.ts

변경 사항:

  • 제거: ALLOWED_TABLES 화이트리스트
  • 제거: COMPANY_FILTERED_TABLES 하드코딩
  • 추가: BLOCKED_TABLES 블랙리스트
  • 추가: TABLE_NAME_REGEX 정규식 검증
  • 추가: validateTableAccess() 공통 검증 메서드
  • 추가: checkColumnExists() 컬럼 존재 확인 메서드
  • 개선: 자동 회사별 필터링 로직

테스트 체크리스트

기본 기능

  • 기존 테이블 조회 정상 작동
  • 새로운 테이블 조회 정상 작동
  • 존재하지 않는 테이블 접근 시 적절한 에러
  • 블랙리스트 테이블 접근 시 차단

보안

  • SQL 인젝션 시도 차단
  • 시스템 테이블 접근 차단
  • 회사별 데이터 격리 정상 작동
  • 최고 관리자 전체 데이터 조회 가능

성능

  • company_code 컬럼 존재 여부 확인 성능 (캐싱 가능)
  • 테이블 존재 여부 확인 성능
  • 정규식 검증 성능 (충분히 빠름)

향후 개선 사항

1. 컬럼 존재 여부 캐싱

// 성능 최적화: 컬럼 정보 캐싱
private columnCache = new Map<string, Set<string>>();

private async checkColumnExists(
  tableName: string,
  columnName: string
): Promise<boolean> {
  // 캐시 확인
  if (this.columnCache.has(tableName)) {
    return this.columnCache.get(tableName)!.has(columnName);
  }
  
  // 테이블의 모든 컬럼 조회 및 캐싱
  const columns = await this.getTableColumnsSimple(tableName);
  const columnSet = new Set(columns.map(c => c.column_name));
  this.columnCache.set(tableName, columnSet);
  
  return columnSet.has(columnName);
}

2. 블랙리스트 패턴 매칭

// pg_* 형태의 패턴 지원
const BLOCKED_TABLE_PATTERNS = [
  /^pg_/,              // pg_로 시작하는 모든 테이블
  /^information_/,     // information_으로 시작
  /_password$/,        // _password로 끝나는 테이블
];

3. 테이블별 접근 권한 시스템

// 향후: 사용자 역할별 테이블 접근 권한
interface TablePermission {
  tableName: string;
  roles: string[];      // ["ADMIN", "USER", "VIEWER"]
  operations: string[]; // ["read", "write", "delete"]
}

결론

동적 테이블 접근 시스템 구축 완료

  • 화이트리스트 제거로 유지보수 부담 해소
  • 블랙리스트 방식으로 보안 유지
  • 자동 회사별 필터링으로 멀티테넌시 보장
  • 새 테이블 추가 시 코드 수정 불필요

이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!