ERP-node/PHASE2_SCREEN_MANAGEMENT_MI...

12 KiB

🖥️ Phase 2.1: ScreenManagementService Raw Query 전환 계획

📋 개요

ScreenManagementService는 46개의 Prisma 호출이 있는 가장 복잡한 서비스입니다. 화면 정의, 레이아웃, 메뉴 할당, 템플릿 등 다양한 기능을 포함합니다.

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/screenManagementService.ts
파일 크기 1,700+ 라인
Prisma 호출 46개
복잡도 매우 높음
우선순위 🔴 최우선

🔍 Prisma 사용 현황 분석

1. 화면 정의 관리 (Screen Definitions) - 18개

// Line 53: 화면 코드 중복 확인
await prisma.screen_definitions.findFirst({ where: { screen_code, is_active: { not: "D" } } })

// Line 70: 화면 생성
await prisma.screen_definitions.create({ data: { ... } })

// Line 99: 화면 목록 조회 (페이징)
await prisma.screen_definitions.findMany({ where, skip, take, orderBy })

// Line 105: 화면 총 개수
await prisma.screen_definitions.count({ where })

// Line 166: 전체 화면 목록
await prisma.screen_definitions.findMany({ where })

// Line 178: 화면 코드로 조회
await prisma.screen_definitions.findFirst({ where: { screen_code } })

// Line 205: 화면 ID로 조회
await prisma.screen_definitions.findFirst({ where: { screen_id } })

// Line 221: 화면 존재 확인
await prisma.screen_definitions.findUnique({ where: { screen_id } })

// Line 236: 화면 업데이트
await prisma.screen_definitions.update({ where, data })

// Line 268: 화면 복사 - 원본 조회
await prisma.screen_definitions.findUnique({ where, include: { screen_layouts } })

// Line 292: 화면 순서 변경 - 전체 조회
await prisma.screen_definitions.findMany({ where })

// Line 486: 화면 템플릿 적용 - 존재 확인
await prisma.screen_definitions.findUnique({ where })

// Line 557: 화면 복사 - 존재 확인
await prisma.screen_definitions.findUnique({ where })

// Line 578: 화면 복사 - 중복 확인
await prisma.screen_definitions.findFirst({ where })

// Line 651: 화면 삭제 - 존재 확인
await prisma.screen_definitions.findUnique({ where })

// Line 672: 화면 삭제 (물리 삭제)
await prisma.screen_definitions.delete({ where })

// Line 700: 삭제된 화면 조회
await prisma.screen_definitions.findMany({ where: { is_active: "D" } })

// Line 706: 삭제된 화면 개수
await prisma.screen_definitions.count({ where })

// Line 763: 일괄 삭제 - 화면 조회
await prisma.screen_definitions.findMany({ where })

// Line 1083: 레이아웃 저장 - 화면 확인
await prisma.screen_definitions.findUnique({ where })

// Line 1181: 레이아웃 조회 - 화면 확인
await prisma.screen_definitions.findUnique({ where })

// Line 1655: 위젯 데이터 저장 - 화면 존재 확인
await prisma.screen_definitions.findMany({ where })

2. 레이아웃 관리 (Screen Layouts) - 4개

// Line 1096: 레이아웃 삭제
await prisma.screen_layouts.deleteMany({ where: { screen_id } })

// Line 1107: 레이아웃 생성 (단일)
await prisma.screen_layouts.create({ data })

// Line 1152: 레이아웃 생성 (다중)
await prisma.screen_layouts.create({ data })

// Line 1193: 레이아웃 조회
await prisma.screen_layouts.findMany({ where })

3. 템플릿 관리 (Screen Templates) - 2개

// Line 1303: 템플릿 목록 조회
await prisma.screen_templates.findMany({ where })

// Line 1317: 템플릿 생성
await prisma.screen_templates.create({ data })

4. 메뉴 할당 (Screen Menu Assignments) - 5개

// Line 446: 메뉴 할당 조회
await prisma.screen_menu_assignments.findMany({ where })

// Line 1346: 메뉴 할당 중복 확인
await prisma.screen_menu_assignments.findFirst({ where })

// Line 1358: 메뉴 할당 생성
await prisma.screen_menu_assignments.create({ data })

// Line 1376: 화면별 메뉴 할당 조회
await prisma.screen_menu_assignments.findMany({ where })

// Line 1401: 메뉴 할당 삭제
await prisma.screen_menu_assignments.deleteMany({ where })

5. 테이블 레이블 (Table Labels) - 3개

// Line 117: 테이블 레이블 조회 (페이징)
await prisma.table_labels.findMany({ where, skip, take })

// Line 713: 테이블 레이블 조회 (전체)
await prisma.table_labels.findMany({ where })

6. 컬럼 레이블 (Column Labels) - 2개

// Line 948: 웹타입 정보 조회
await prisma.column_labels.findMany({ where, select })

// Line 1456: 컬럼 레이블 UPSERT
await prisma.column_labels.upsert({ where, create, update })

7. Raw Query 사용 (이미 있음) - 6개

// Line 627: 화면 순서 변경 (일괄 업데이트)
await prisma.$executeRaw`UPDATE screen_definitions SET display_order = ...`

// Line 833: 테이블 목록 조회
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`

// Line 876: 테이블 존재 확인
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`

// Line 922: 테이블 컬럼 정보 조회
await prisma.$queryRaw<Array<ColumnInfo>>`SELECT column_name, data_type ...`

// Line 1418: 컬럼 정보 조회 (상세)
await prisma.$queryRaw`SELECT column_name, data_type ...`

8. 트랜잭션 사용 - 3개

// Line 521: 화면 템플릿 적용 트랜잭션
await prisma.$transaction(async (tx) => { ... })

// Line 593: 화면 복사 트랜잭션
await prisma.$transaction(async (tx) => { ... })

// Line 788: 일괄 삭제 트랜잭션
await prisma.$transaction(async (tx) => { ... })

// Line 1697: 위젯 데이터 저장 트랜잭션
await prisma.$transaction(async (tx) => { ... })

🛠️ 전환 전략

전략 1: 단계적 전환

  1. 1단계: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
  2. 2단계: 복잡한 조회 전환 (include, join)
  3. 3단계: 트랜잭션 전환
  4. 4단계: Raw Query 개선

전략 2: 함수별 전환 우선순위

🔴 최우선 (기본 CRUD)

  • createScreen() - Line 70
  • getScreensByCompany() - Line 99-105
  • getScreenByCode() - Line 178
  • getScreenById() - Line 205
  • updateScreen() - Line 236
  • deleteScreen() - Line 672

🟡 2순위 (레이아웃)

  • saveLayout() - Line 1096-1152
  • getLayout() - Line 1193
  • deleteLayout() - Line 1096

🟢 3순위 (템플릿 & 메뉴)

  • getTemplates() - Line 1303
  • createTemplate() - Line 1317
  • assignToMenu() - Line 1358
  • getMenuAssignments() - Line 1376
  • removeMenuAssignment() - Line 1401

🔵 4순위 (복잡한 기능)

  • copyScreen() - Line 593 (트랜잭션)
  • applyTemplate() - Line 521 (트랜잭션)
  • bulkDelete() - Line 788 (트랜잭션)
  • reorderScreens() - Line 627 (Raw Query)

📝 전환 예시

예시 1: createScreen() 전환

기존 Prisma 코드:

// Line 53: 중복 확인
const existingScreen = await prisma.screen_definitions.findFirst({
  where: {
    screen_code: screenData.screenCode,
    is_active: { not: "D" },
  },
});

// Line 70: 생성
const screen = await prisma.screen_definitions.create({
  data: {
    screen_name: screenData.screenName,
    screen_code: screenData.screenCode,
    table_name: screenData.tableName,
    company_code: screenData.companyCode,
    description: screenData.description,
    created_by: screenData.createdBy,
  },
});

새로운 Raw Query 코드:

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

// 중복 확인
const existingResult = await query<{ screen_id: number }>(
  `SELECT screen_id FROM screen_definitions
   WHERE screen_code = $1 AND is_active != 'D'
   LIMIT 1`,
  [screenData.screenCode]
);

if (existingResult.length > 0) {
  throw new Error("이미 존재하는 화면 코드입니다.");
}

// 생성
const [screen] = await query<ScreenDefinition>(
  `INSERT INTO screen_definitions (
    screen_name, screen_code, table_name, company_code, description, created_by
  ) VALUES ($1, $2, $3, $4, $5, $6)
  RETURNING *`,
  [
    screenData.screenName,
    screenData.screenCode,
    screenData.tableName,
    screenData.companyCode,
    screenData.description,
    screenData.createdBy,
  ]
);

예시 2: getScreensByCompany() 전환 (페이징)

기존 Prisma 코드:

const [screens, total] = await Promise.all([
  prisma.screen_definitions.findMany({
    where: whereClause,
    skip: (page - 1) * size,
    take: size,
    orderBy: { created_at: "desc" },
  }),
  prisma.screen_definitions.count({ where: whereClause }),
]);

새로운 Raw Query 코드:

const offset = (page - 1) * size;
const whereSQL = companyCode !== "*"
  ? "WHERE company_code = $1 AND is_active != 'D'"
  : "WHERE is_active != 'D'";
const params = companyCode !== "*" ? [companyCode, size, offset] : [size, offset];

const [screens, totalResult] = await Promise.all([
  query<ScreenDefinition>(
    `SELECT * FROM screen_definitions
     ${whereSQL}
     ORDER BY created_at DESC
     LIMIT $${params.length - 1} OFFSET $${params.length}`,
    params
  ),
  query<{ count: number }>(
    `SELECT COUNT(*) as count FROM screen_definitions ${whereSQL}`,
    companyCode !== "*" ? [companyCode] : []
  ),
]);

const total = totalResult[0]?.count || 0;

예시 3: 트랜잭션 전환

기존 Prisma 코드:

await prisma.$transaction(async (tx) => {
  const newScreen = await tx.screen_definitions.create({ data: { ... } });
  await tx.screen_layouts.createMany({ data: layouts });
});

새로운 Raw Query 코드:

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

await transaction(async (client) => {
  const [newScreen] = await client.query(
    `INSERT INTO screen_definitions (...) VALUES (...) RETURNING *`,
    [...]
  );

  for (const layout of layouts) {
    await client.query(
      `INSERT INTO screen_layouts (...) VALUES (...)`,
      [...]
    );
  }
});

🧪 테스트 계획

단위 테스트

describe("ScreenManagementService Raw Query 전환 테스트", () => {
  describe("createScreen", () => {
    test("화면 생성 성공", async () => { ... });
    test("중복 화면 코드 에러", async () => { ... });
  });

  describe("getScreensByCompany", () => {
    test("페이징 조회 성공", async () => { ... });
    test("회사별 필터링", async () => { ... });
  });

  describe("copyScreen", () => {
    test("화면 복사 성공 (트랜잭션)", async () => { ... });
    test("레이아웃 함께 복사", async () => { ... });
  });
});

통합 테스트

describe("화면 관리 통합 테스트", () => {
  test("화면 생성 → 조회 → 수정 → 삭제", async () => { ... });
  test("화면 복사 → 레이아웃 확인", async () => { ... });
  test("메뉴 할당 → 조회 → 해제", async () => { ... });
});

📋 체크리스트

1단계: 기본 CRUD (6개 함수)

  • createScreen() - 화면 생성
  • getScreensByCompany() - 화면 목록 (페이징)
  • getScreenByCode() - 화면 코드로 조회
  • getScreenById() - 화면 ID로 조회
  • updateScreen() - 화면 업데이트
  • deleteScreen() - 화면 삭제

2단계: 레이아웃 관리 (3개 함수)

  • saveLayout() - 레이아웃 저장
  • getLayout() - 레이아웃 조회
  • 레이아웃 삭제 로직

3단계: 템플릿 & 메뉴 (6개 함수)

  • getTemplates() - 템플릿 목록
  • createTemplate() - 템플릿 생성
  • assignToMenu() - 메뉴 할당
  • getMenuAssignments() - 메뉴 할당 조회
  • removeMenuAssignment() - 메뉴 할당 해제
  • 테이블 레이블 조회

4단계: 복잡한 기능 (4개 함수)

  • copyScreen() - 화면 복사 (트랜잭션)
  • applyTemplate() - 템플릿 적용 (트랜잭션)
  • bulkDelete() - 일괄 삭제 (트랜잭션)
  • reorderScreens() - 순서 변경 (Raw Query)

5단계: 테스트 & 검증

  • 단위 테스트 작성 (20개 이상)
  • 통합 테스트 작성 (5개 이상)
  • 성능 테스트
  • Prisma import 제거 확인

🎯 완료 기준

  • 46개 Prisma 호출 모두 Raw Query로 전환
  • 모든 단위 테스트 통과
  • 모든 통합 테스트 통과
  • 성능 저하 없음 (기존 대비 ±10% 이내)
  • 트랜잭션 정상 동작 확인
  • 에러 처리 및 롤백 정상 동작

작성일: 2025-09-30 예상 소요 시간: 2-3일 담당자: 백엔드 개발팀 우선순위: 🔴 최우선 (Phase 2.1)