ERP-node/PHASE3.16_DATA_MANAGEMENT_S...

13 KiB

📋 Phase 3.16: Data Management Services Raw Query 전환 계획

📋 개요

데이터 관리 관련 서비스들은 총 18개의 Prisma 호출이 있으며, 동적 폼, 데이터 매핑, 데이터 서비스, 관리자 기능을 담당합니다.

📊 기본 정보

항목 내용
대상 서비스 4개 (EnhancedDynamicForm, DataMapping, Data, Admin)
파일 위치 backend-node/src/services/{enhanced,data,admin}*.ts
총 파일 크기 2,062 라인
Prisma 호출 0개 (전환 완료)
현재 진행률 18/18 (100%) 전환 완료
복잡도 중간 (동적 쿼리, JSON 필드, 관리자 기능)
우선순위 🟡 중간 (Phase 3.16)
상태 완료

전환 완료 내역

전환된 Prisma 호출 (18개)

1. EnhancedDynamicFormService (6개)

  • validateTableExists() - $queryRawUnsafe → query
  • getTableColumns() - $queryRawUnsafe → query
  • getColumnWebTypes() - $queryRawUnsafe → query
  • getPrimaryKeys() - $queryRawUnsafe → query
  • performInsert() - $queryRawUnsafe → query
  • performUpdate() - $queryRawUnsafe → query

2. DataMappingService (5개)

  • getSourceData() - $queryRawUnsafe → query
  • executeInsert() - $executeRawUnsafe → query
  • executeUpsert() - $executeRawUnsafe → query
  • executeUpdate() - $executeRawUnsafe → query
  • disconnect() - 제거 (Raw Query는 disconnect 불필요)

3. DataService (4개)

  • getTableData() - $queryRawUnsafe → query
  • checkTableExists() - $queryRawUnsafe → query
  • getTableColumnsSimple() - $queryRawUnsafe → query
  • getColumnLabel() - $queryRawUnsafe → query

4. AdminService (3개)

  • getAdminMenuList() - $queryRaw → query (WITH RECURSIVE)
  • getUserMenuList() - $queryRaw → query (WITH RECURSIVE)
  • getMenuInfo() - findUnique → query (JOIN)

주요 기술적 해결 사항

  1. 변수명 충돌 해결

    • dataService.ts에서 query 변수 → sql 변수로 변경
    • query() 함수와 로컬 변수 충돌 방지
  2. WITH RECURSIVE 쿼리 전환

    • Prisma의 $queryRaw 템플릿 리터럴 → 일반 문자열
    • ${userLang}$1 파라미터 바인딩
  3. JOIN 쿼리 전환

    • Prisma의 include 옵션 → LEFT JOIN 쿼리
    • 관계 데이터를 단일 쿼리로 조회
  4. 동적 쿼리 생성

    • 동적 WHERE 조건 구성
    • SQL 인젝션 방지 (컬럼명 검증)
    • 동적 ORDER BY 처리

컴파일 상태

TypeScript 컴파일 성공
Linter 오류 없음


🔍 서비스별 상세 분석

1. EnhancedDynamicFormService (6개 호출, 786 라인)

주요 기능:

  • 고급 동적 폼 관리
  • 폼 검증 규칙
  • 조건부 필드 표시
  • 폼 템플릿 관리

예상 Prisma 호출:

  • getEnhancedForms() - 고급 폼 목록 조회
  • getEnhancedForm() - 고급 폼 단건 조회
  • createEnhancedForm() - 고급 폼 생성
  • updateEnhancedForm() - 고급 폼 수정
  • deleteEnhancedForm() - 고급 폼 삭제
  • getFormValidationRules() - 검증 규칙 조회

기술적 고려사항:

  • JSON 필드 (validation_rules, conditional_logic, field_config)
  • 복잡한 검증 규칙
  • 동적 필드 생성
  • 조건부 표시 로직

2. DataMappingService (5개 호출, 575 라인)

주요 기능:

  • 데이터 매핑 설정 관리
  • 소스-타겟 필드 매핑
  • 데이터 변환 규칙
  • 매핑 실행

예상 Prisma 호출:

  • getDataMappings() - 매핑 설정 목록 조회
  • getDataMapping() - 매핑 설정 단건 조회
  • createDataMapping() - 매핑 설정 생성
  • updateDataMapping() - 매핑 설정 수정
  • deleteDataMapping() - 매핑 설정 삭제

기술적 고려사항:

  • JSON 필드 (field_mappings, transformation_rules)
  • 복잡한 변환 로직
  • 매핑 검증
  • 실행 이력 추적

3. DataService (4개 호출, 327 라인)

주요 기능:

  • 동적 데이터 조회
  • 데이터 필터링
  • 데이터 정렬
  • 데이터 집계

예상 Prisma 호출:

  • getDataByTable() - 테이블별 데이터 조회
  • getDataById() - 데이터 단건 조회
  • executeCustomQuery() - 커스텀 쿼리 실행
  • getDataStatistics() - 데이터 통계 조회

기술적 고려사항:

  • 동적 테이블 쿼리
  • SQL 인젝션 방지
  • 동적 WHERE 조건
  • 집계 쿼리

4. AdminService (3개 호출, 374 라인)

주요 기능:

  • 관리자 메뉴 관리
  • 시스템 설정
  • 사용자 관리
  • 로그 조회

예상 Prisma 호출:

  • getAdminMenus() - 관리자 메뉴 조회
  • getSystemSettings() - 시스템 설정 조회
  • updateSystemSettings() - 시스템 설정 업데이트

기술적 고려사항:

  • 메뉴 계층 구조
  • 권한 기반 필터링
  • JSON 설정 필드
  • 캐싱

💡 통합 전환 전략

Phase 1: 단순 CRUD 전환 (12개)

EnhancedDynamicFormService (6개) + DataMappingService (5개) + AdminService (1개)

  • 기본 CRUD 기능
  • JSON 필드 처리

Phase 2: 동적 쿼리 전환 (4개)

DataService (4개)

  • 동적 테이블 쿼리
  • 보안 검증

Phase 3: 고급 기능 전환 (2개)

AdminService (2개)

  • 시스템 설정
  • 캐싱

💻 전환 예시

예시 1: 고급 폼 생성 (JSON 필드)

변경 전:

const form = await prisma.enhanced_forms.create({
  data: {
    form_code: formCode,
    form_name: formName,
    validation_rules: validationRules, // JSON
    conditional_logic: conditionalLogic, // JSON
    field_config: fieldConfig, // JSON
    company_code: companyCode,
  },
});

변경 후:

const form = await queryOne<any>(
  `INSERT INTO enhanced_forms 
   (form_code, form_name, validation_rules, conditional_logic, 
    field_config, company_code, created_at, updated_at)
   VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
   RETURNING *`,
  [
    formCode,
    formName,
    JSON.stringify(validationRules),
    JSON.stringify(conditionalLogic),
    JSON.stringify(fieldConfig),
    companyCode,
  ]
);

예시 2: 데이터 매핑 조회

변경 전:

const mappings = await prisma.data_mappings.findMany({
  where: {
    source_table: sourceTable,
    target_table: targetTable,
    is_active: true,
  },
  include: {
    source_columns: true,
    target_columns: true,
  },
});

변경 후:

const mappings = await query<any>(
  `SELECT 
     dm.*,
     json_agg(DISTINCT jsonb_build_object(
       'column_id', sc.column_id,
       'column_name', sc.column_name
     )) FILTER (WHERE sc.column_id IS NOT NULL) as source_columns,
     json_agg(DISTINCT jsonb_build_object(
       'column_id', tc.column_id,
       'column_name', tc.column_name
     )) FILTER (WHERE tc.column_id IS NOT NULL) as target_columns
   FROM data_mappings dm
   LEFT JOIN columns sc ON dm.mapping_id = sc.mapping_id AND sc.type = 'source'
   LEFT JOIN columns tc ON dm.mapping_id = tc.mapping_id AND tc.type = 'target'
   WHERE dm.source_table = $1 
   AND dm.target_table = $2 
   AND dm.is_active = $3
   GROUP BY dm.mapping_id`,
  [sourceTable, targetTable, true]
);

예시 3: 동적 테이블 쿼리 (DataService)

변경 전:

// Prisma로는 동적 테이블 쿼리 불가능
// 이미 $queryRawUnsafe 사용 중일 가능성
const data = await prisma.$queryRawUnsafe(
  `SELECT * FROM ${tableName} WHERE ${whereClause}`,
  ...params
);

변경 후:

// SQL 인젝션 방지를 위한 테이블명 검증
const validTableName = validateTableName(tableName);

const data = await query<any>(
  `SELECT * FROM ${validTableName} WHERE ${whereClause}`,
  params
);

예시 4: 관리자 메뉴 조회 (계층 구조)

변경 전:

const menus = await prisma.admin_menus.findMany({
  where: { is_active: true },
  orderBy: { sort_order: "asc" },
  include: {
    children: {
      orderBy: { sort_order: "asc" },
    },
  },
});

변경 후:

// 재귀 CTE를 사용한 계층 쿼리
const menus = await query<any>(
  `WITH RECURSIVE menu_tree AS (
     SELECT *, 0 as level, ARRAY[menu_id] as path
     FROM admin_menus
     WHERE parent_id IS NULL AND is_active = $1
     
     UNION ALL
     
     SELECT m.*, mt.level + 1, mt.path || m.menu_id
     FROM admin_menus m
     JOIN menu_tree mt ON m.parent_id = mt.menu_id
     WHERE m.is_active = $1
   )
   SELECT * FROM menu_tree
   ORDER BY path, sort_order`,
  [true]
);

🔧 기술적 고려사항

1. JSON 필드 처리

// 복잡한 JSON 구조
interface ValidationRules {
  required?: string[];
  min?: Record<string, number>;
  max?: Record<string, number>;
  pattern?: Record<string, string>;
  custom?: Array<{ field: string; rule: string }>;
}

// 저장 시
JSON.stringify(validationRules);

// 조회 후
const parsed =
  typeof row.validation_rules === "string"
    ? JSON.parse(row.validation_rules)
    : row.validation_rules;

2. 동적 테이블 쿼리 보안

// 테이블명 화이트리스트
const ALLOWED_TABLES = ["users", "products", "orders"];

function validateTableName(tableName: string): string {
  if (!ALLOWED_TABLES.includes(tableName)) {
    throw new Error("Invalid table name");
  }
  return tableName;
}

// 컬럼명 검증
function validateColumnName(columnName: string): string {
  if (!/^[a-z_][a-z0-9_]*$/i.test(columnName)) {
    throw new Error("Invalid column name");
  }
  return columnName;
}

3. 재귀 CTE (계층 구조)

WITH RECURSIVE hierarchy AS (
  -- 최상위 노드
  SELECT * FROM table WHERE parent_id IS NULL

  UNION ALL

  -- 하위 노드
  SELECT t.* FROM table t
  JOIN hierarchy h ON t.parent_id = h.id
)
SELECT * FROM hierarchy

4. JSON 집계 (관계 데이터)

SELECT
  parent.*,
  COALESCE(
    json_agg(
      jsonb_build_object('id', child.id, 'name', child.name)
    ) FILTER (WHERE child.id IS NOT NULL),
    '[]'
  ) as children
FROM parent
LEFT JOIN child ON parent.id = child.parent_id
GROUP BY parent.id

📝 전환 체크리스트

EnhancedDynamicFormService (6개)

  • getEnhancedForms() - 목록 조회
  • getEnhancedForm() - 단건 조회
  • createEnhancedForm() - 생성 (JSON 필드)
  • updateEnhancedForm() - 수정 (JSON 필드)
  • deleteEnhancedForm() - 삭제
  • getFormValidationRules() - 검증 규칙 조회

DataMappingService (5개)

  • getDataMappings() - 목록 조회
  • getDataMapping() - 단건 조회
  • createDataMapping() - 생성
  • updateDataMapping() - 수정
  • deleteDataMapping() - 삭제

DataService (4개)

  • getDataByTable() - 동적 테이블 조회
  • getDataById() - 단건 조회
  • executeCustomQuery() - 커스텀 쿼리
  • getDataStatistics() - 통계 조회

AdminService (3개)

  • getAdminMenus() - 메뉴 조회 (재귀 CTE)
  • getSystemSettings() - 시스템 설정 조회
  • updateSystemSettings() - 시스템 설정 업데이트

공통 작업

  • import 문 수정 (모든 서비스)
  • Prisma import 완전 제거
  • JSON 필드 처리 확인
  • 보안 검증 (SQL 인젝션)

🧪 테스트 계획

단위 테스트 (18개)

  • 각 Prisma 호출별 1개씩

통합 테스트 (6개)

  • EnhancedDynamicFormService: 폼 생성 및 검증 테스트 (2개)
  • DataMappingService: 매핑 설정 및 실행 테스트 (2개)
  • DataService: 동적 쿼리 및 보안 테스트 (1개)
  • AdminService: 메뉴 계층 구조 테스트 (1개)

보안 테스트

  • SQL 인젝션 방지 테스트
  • 테이블명 검증 테스트
  • 컬럼명 검증 테스트

🎯 예상 난이도 및 소요 시간

  • 난이도: (높음)
    • JSON 필드 처리
    • 동적 쿼리 보안
    • 재귀 CTE
    • JSON 집계
  • 예상 소요 시간: 2.5~3시간
    • Phase 1 (기본 CRUD): 1시간
    • Phase 2 (동적 쿼리): 1시간
    • Phase 3 (고급 기능): 0.5시간
    • 테스트 및 문서화: 0.5시간

⚠️ 주의사항

보안 필수 체크리스트

  1. 동적 테이블명은 반드시 화이트리스트 검증
  2. 동적 컬럼명은 정규식으로 검증
  3. WHERE 절 파라미터는 반드시 바인딩
  4. JSON 필드는 파싱 에러 처리
  5. 재귀 쿼리는 깊이 제한 설정

성능 최적화

  • JSON 필드 인덱싱 (GIN 인덱스)
  • 재귀 쿼리 깊이 제한
  • 집계 쿼리 최적화
  • 필요시 캐싱 적용

상태: 대기 중
특이사항: JSON 필드, 동적 쿼리, 재귀 CTE, 보안 검증 포함
⚠️ 주의: 동적 쿼리는 SQL 인젝션 방지가 매우 중요!