ERP-node/PHASE2.2_TABLE_MANAGEMENT_M...

12 KiB

🗂️ Phase 2.2: TableManagementService Raw Query 전환 계획

📋 개요

TableManagementService는 33개의 Prisma 호출이 있습니다. 대부분(약 26개)은 $queryRaw를 사용하고 있어 SQL은 이미 작성되어 있지만, Prisma 클라이언트를 완전히 제거하려면 33개 모두를 db.tsquery 함수로 교체해야 합니다.

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/tableManagementService.ts
파일 크기 3,178 라인
Prisma 호출 33개 ($queryRaw: 26개, ORM: 7개)
현재 진행률 0/33 (0%) 전환 필요
전환 필요 33개 모두 전환 필요 (SQL은 이미 작성되어 있음)
복잡도 중간 (SQL 작성은 완료, query() 함수로 교체만 필요)
우선순위 🟡 중간 (Phase 2.2)

🎯 전환 목표

  • 33개 모든 Prisma 호출을 db.tsquery() 함수로 교체
    • 26개 $queryRawquery() 또는 queryOne()
    • 7개 ORM 메서드 → query() (SQL 새로 작성)
    • 1개 $transactiontransaction()
  • 트랜잭션 처리 정상 동작 확인
  • 모든 단위 테스트 통과
  • Prisma import 완전 제거

🔍 Prisma 사용 현황 분석

1. $queryRaw / $queryRawUnsafe 사용 (26개)

현재 상태: SQL은 이미 작성되어 있음
전환 작업: prisma.$queryRawquery() 함수로 교체만 하면 됨

// 기존
await prisma.$queryRaw`SELECT ...`;
await prisma.$queryRawUnsafe(sqlString, ...params);

// 전환 후
import { query } from "../database/db";
await query(`SELECT ...`);
await query(sqlString, params);

2. ORM 메서드 사용 (7개)

현재 상태: Prisma ORM 메서드 사용
전환 작업: SQL 작성 필요

1. table_labels 관리 (2개)

// Line 254: 테이블 라벨 UPSERT
await prisma.table_labels.upsert({
  where: { table_name: tableName },
  update: {},
  create: { table_name, table_label, description }
});

// Line 437: 테이블 라벨 조회
await prisma.table_labels.findUnique({
  where: { table_name: tableName },
  select: { table_name, table_label, description, ... }
});

2. column_labels 관리 (5개)

// Line 323: 컬럼 라벨 UPSERT
await prisma.column_labels.upsert({
  where: {
    table_name_column_name: {
      table_name: tableName,
      column_name: columnName
    }
  },
  update: { column_label, input_type, ... },
  create: { table_name, column_name, ... }
});

// Line 481: 컬럼 라벨 조회
await prisma.column_labels.findUnique({
  where: {
    table_name_column_name: {
      table_name: tableName,
      column_name: columnName
    }
  },
  select: { id, table_name, column_name, ... }
});

// Line 567: 컬럼 존재 확인
await prisma.column_labels.findFirst({
  where: { table_name, column_name }
});

// Line 586: 컬럼 라벨 업데이트
await prisma.column_labels.update({
  where: { id: existingColumn.id },
  data: { web_type, detail_settings, ... }
});

// Line 610: 컬럼 라벨 생성
await prisma.column_labels.create({
  data: { table_name, column_name, web_type, ... }
});

// Line 1003: 파일 타입 컬럼 조회
await prisma.column_labels.findMany({
  where: { table_name, web_type: 'file' },
  select: { column_name }
});

// Line 1382: 컬럼 웹타입 정보 조회
await prisma.column_labels.findFirst({
  where: { table_name, column_name },
  select: { web_type, code_category, ... }
});

// Line 2690: 컬럼 라벨 UPSERT (복제)
await prisma.column_labels.upsert({
  where: {
    table_name_column_name: { table_name, column_name }
  },
  update: { column_label, web_type, ... },
  create: { table_name, column_name, ... }
});

3. attach_file_info 관리 (2개)

// Line 914: 파일 정보 조회
await prisma.attach_file_info.findMany({
  where: { target_objid, doc_type, status: 'ACTIVE' },
  select: { objid, real_file_name, file_size, ... },
  orderBy: { regdate: 'desc' }
});

// Line 959: 파일 경로로 파일 정보 조회
await prisma.attach_file_info.findFirst({
  where: { file_path, status: 'ACTIVE' },
  select: { objid, real_file_name, ... }
});

4. 트랜잭션 (1개)

// Line 391: 전체 컬럼 설정 일괄 업데이트
await prisma.$transaction(async (tx) => {
  await this.insertTableIfNotExists(tableName);
  for (const columnSetting of columnSettings) {
    await this.updateColumnSettings(tableName, columnName, columnSetting);
  }
});

📝 전환 예시

예시 1: table_labels UPSERT 전환

기존 Prisma 코드:

await prisma.table_labels.upsert({
  where: { table_name: tableName },
  update: {},
  create: {
    table_name: tableName,
    table_label: tableName,
    description: "",
  },
});

새로운 Raw Query 코드:

import { query } from "../database/db";

await query(
  `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
   VALUES ($1, $2, $3, NOW(), NOW())
   ON CONFLICT (table_name) DO NOTHING`,
  [tableName, tableName, ""]
);

예시 2: column_labels UPSERT 전환

기존 Prisma 코드:

await prisma.column_labels.upsert({
  where: {
    table_name_column_name: {
      table_name: tableName,
      column_name: columnName,
    },
  },
  update: {
    column_label: settings.columnLabel,
    input_type: settings.inputType,
    detail_settings: settings.detailSettings,
    updated_date: new Date(),
  },
  create: {
    table_name: tableName,
    column_name: columnName,
    column_label: settings.columnLabel,
    input_type: settings.inputType,
    detail_settings: settings.detailSettings,
  },
});

새로운 Raw Query 코드:

await query(
  `INSERT INTO column_labels (
    table_name, column_name, column_label, input_type, detail_settings,
    code_category, code_value, reference_table, reference_column,
    display_column, display_order, is_visible, created_date, updated_date
  ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
  ON CONFLICT (table_name, column_name) 
  DO UPDATE SET
    column_label = EXCLUDED.column_label,
    input_type = EXCLUDED.input_type,
    detail_settings = EXCLUDED.detail_settings,
    code_category = EXCLUDED.code_category,
    code_value = EXCLUDED.code_value,
    reference_table = EXCLUDED.reference_table,
    reference_column = EXCLUDED.reference_column,
    display_column = EXCLUDED.display_column,
    display_order = EXCLUDED.display_order,
    is_visible = EXCLUDED.is_visible,
    updated_date = NOW()`,
  [
    tableName,
    columnName,
    settings.columnLabel,
    settings.inputType,
    settings.detailSettings,
    settings.codeCategory,
    settings.codeValue,
    settings.referenceTable,
    settings.referenceColumn,
    settings.displayColumn,
    settings.displayOrder || 0,
    settings.isVisible !== undefined ? settings.isVisible : true,
  ]
);

예시 3: 트랜잭션 전환

기존 Prisma 코드:

await prisma.$transaction(async (tx) => {
  await this.insertTableIfNotExists(tableName);
  for (const columnSetting of columnSettings) {
    await this.updateColumnSettings(tableName, columnName, columnSetting);
  }
});

새로운 Raw Query 코드:

import { transaction } from "../database/db";

await transaction(async (client) => {
  // 테이블 라벨 자동 추가
  await client.query(
    `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
     VALUES ($1, $2, $3, NOW(), NOW())
     ON CONFLICT (table_name) DO NOTHING`,
    [tableName, tableName, ""]
  );

  // 각 컬럼 설정 업데이트
  for (const columnSetting of columnSettings) {
    const columnName = columnSetting.columnName;
    if (columnName) {
      await client.query(
        `INSERT INTO column_labels (...)
         VALUES (...)
         ON CONFLICT (table_name, column_name) DO UPDATE SET ...`,
        [...]
      );
    }
  }
});

🧪 테스트 계획

단위 테스트 (10개)

describe("TableManagementService Raw Query 전환 테스트", () => {
  describe("insertTableIfNotExists", () => {
    test("테이블 라벨 UPSERT 성공", async () => { ... });
    test("중복 테이블 처리", async () => { ... });
  });

  describe("updateColumnSettings", () => {
    test("컬럼 설정 UPSERT 성공", async () => { ... });
    test("기존 컬럼 업데이트", async () => { ... });
  });

  describe("getTableLabels", () => {
    test("테이블 라벨 조회 성공", async () => { ... });
  });

  describe("getColumnLabels", () => {
    test("컬럼 라벨 조회 성공", async () => { ... });
  });

  describe("updateAllColumnSettings", () => {
    test("일괄 업데이트 성공 (트랜잭션)", async () => { ... });
    test("부분 실패 시 롤백", async () => { ... });
  });

  describe("getFileInfoByColumnAndTarget", () => {
    test("파일 정보 조회 성공", async () => { ... });
  });
});

통합 테스트 (5개 시나리오)

describe("테이블 관리 통합 테스트", () => {
  test("테이블 라벨 생성 → 조회 → 수정", async () => { ... });
  test("컬럼 라벨 생성 → 조회 → 수정", async () => { ... });
  test("컬럼 일괄 설정 업데이트", async () => { ... });
  test("파일 정보 조회 및 보강", async () => { ... });
  test("트랜잭션 롤백 테스트", async () => { ... });
});

📋 체크리스트

1단계: table_labels 전환 (2개 함수) 진행 예정

  • insertTableIfNotExists() - UPSERT
  • getTableLabels() - 조회

2단계: column_labels 전환 (5개 함수) 진행 예정

  • updateColumnSettings() - UPSERT
  • getColumnLabels() - 조회
  • updateColumnWebType() - findFirst + update/create
  • getColumnWebTypeInfo() - findFirst
  • updateColumnLabel() - UPSERT (복제)

3단계: attach_file_info 전환 (2개 함수) 진행 예정

  • getFileInfoByColumnAndTarget() - findMany
  • getFileInfoByPath() - findFirst

4단계: 트랜잭션 전환 (1개 함수) 진행 예정

  • updateAllColumnSettings() - 트랜잭션

5단계: 테스트 & 검증 진행 예정

  • 단위 테스트 작성 (10개)
  • 통합 테스트 작성 (5개 시나리오)
  • Prisma import 완전 제거 확인
  • 성능 테스트

🎯 완료 기준

  • 33개 모든 Prisma 호출을 Raw Query로 전환 완료
    • 26개 $queryRawquery() 함수로 교체
    • 7개 ORM 메서드 → query() 함수로 전환 (SQL 작성)
  • 모든 TypeScript 컴파일 오류 해결
  • 트랜잭션 정상 동작 확인
  • 에러 처리 및 롤백 정상 동작
  • 모든 단위 테스트 통과 (10개)
  • 모든 통합 테스트 작성 완료 (5개 시나리오)
  • import prisma 완전 제거 및 import { query, transaction } from "../database/db" 사용
  • 성능 저하 없음 (기존 대비 ±10% 이내)

💡 특이사항

SQL은 이미 대부분 작성되어 있음

이 서비스는 이미 79%가 $queryRaw를 사용하고 있어, SQL 작성은 완료되었습니다:

  • information_schema 조회: SQL 작성 완료 ($queryRaw 사용 중)
  • 동적 테이블 쿼리: SQL 작성 완료 ($queryRawUnsafe 사용 중)
  • DDL 실행: SQL 작성 완료 ($executeRaw 사용 중)
  • 전환 작업: prisma.$queryRawquery() 함수로 단순 교체만 필요
  • CRUD 작업: 7개만 SQL 새로 작성 필요

UPSERT 패턴 중요

대부분의 전환이 UPSERT 패턴이므로 PostgreSQL의 ON CONFLICT 구문을 활용합니다.


작성일: 2025-09-30 예상 소요 시간: 1-1.5일 (SQL은 79% 작성 완료, 함수 교체 작업 필요) 담당자: 백엔드 개발팀 우선순위: 🟡 중간 (Phase 2.2) 상태: 진행 예정 특이사항: SQL은 대부분 작성되어 있어 prisma.$queryRawquery() 단순 교체 작업이 주요 작업