ERP-node/PHASE2_SCREEN_MANAGEMENT_MI...

16 KiB

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

📋 개요

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

📊 기본 정보

항목 내용
파일 위치 backend-node/src/services/screenManagementService.ts
파일 크기 1,700+ 라인
Prisma 호출 46개
현재 진행률 46/46 (100%) 완료
복잡도 매우 높음
우선순위 🔴 최우선

🎯 전환 현황 (2025-09-30 업데이트)

  • Stage 1 완료: 기본 CRUD (8개 함수) - Commit: 13c1bc4, 0e8d1d4
  • Stage 2 완료: 레이아웃 관리 (2개 함수, 4 Prisma 호출) - Commit: 67dced7
  • Stage 3 완료: 템플릿 & 메뉴 관리 (5개 함수) - Commit: 74351e8
  • Stage 4 완료: 복잡한 기능 (트랜잭션) - 모든 46개 Prisma 호출 전환 완료

🔍 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 (8개 함수) 완료

  • createScreen() - 화면 생성
  • getScreensByCompany() - 화면 목록 (페이징)
  • getScreenByCode() - 화면 코드로 조회
  • getScreenById() - 화면 ID로 조회
  • updateScreen() - 화면 업데이트
  • deleteScreen() - 화면 삭제
  • getScreens() - 전체 화면 목록 조회
  • getScreen() - 회사 코드 필터링 포함 조회

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

  • saveLayout() - 레이아웃 저장 (메타데이터 + 컴포넌트)
  • getLayout() - 레이아웃 조회
  • 레이아웃 삭제 로직 (saveLayout 내부에 포함)

3단계: 템플릿 & 메뉴 (5개 함수) 완료

  • getTemplatesByCompany() - 템플릿 목록
  • createTemplate() - 템플릿 생성
  • assignScreenToMenu() - 메뉴 할당
  • getScreensByMenu() - 메뉴별 화면 조회
  • unassignScreenFromMenu() - 메뉴 할당 해제
  • 테이블 레이블 조회 (getScreensByCompany 내부에 포함됨)

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

  • copyScreen() - 화면 복사 (트랜잭션)
  • generateScreenCode() - 화면 코드 자동 생성
  • checkScreenDependencies() - 화면 의존성 체크 (메뉴 할당 포함)
  • 모든 유틸리티 메서드 Raw Query 전환

5단계: 테스트 & 검증 완료

  • 단위 테스트 작성 (18개 테스트 통과)
    • createScreen, updateScreen, deleteScreen
    • getScreensByCompany, getScreenById
    • saveLayout, getLayout
    • getTemplatesByCompany, assignScreenToMenu
    • copyScreen, generateScreenCode
    • getTableColumns
  • 통합 테스트 작성 (6개 시나리오)
    • 화면 생명주기 테스트 (생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제)
    • 화면 복사 및 레이아웃 테스트
    • 테이블 정보 조회 테스트
    • 일괄 작업 테스트
    • 화면 코드 자동 생성 테스트
  • Prisma import 완전 제거 확인
  • 성능 테스트 (추후 실행 예정)

🎯 완료 기준

  • 46개 Prisma 호출 모두 Raw Query로 전환 완료
  • 모든 TypeScript 컴파일 오류 해결
  • 트랜잭션 정상 동작 확인
  • 에러 처리 및 롤백 정상 동작
  • 모든 단위 테스트 통과 (18개)
  • 모든 통합 테스트 작성 완료 (6개 시나리오)
  • Prisma import 완전 제거
  • 성능 저하 없음 (기존 대비 ±10% 이내) - 추후 측정 예정

📊 테스트 결과

단위 테스트 (18개)

✅ createScreen - 화면 생성 (2개 테스트)
✅ getScreensByCompany - 화면 목록 페이징 (2개 테스트)
✅ updateScreen - 화면 업데이트 (2개 테스트)
✅ deleteScreen - 화면 삭제 (2개 테스트)
✅ saveLayout - 레이아웃 저장 (2개 테스트)
   - 기본 저장, 소수점 좌표 반올림 처리
✅ getLayout - 레이아웃 조회 (1개 테스트)
✅ getTemplatesByCompany - 템플릿 목록 (1개 테스트)
✅ assignScreenToMenu - 메뉴 할당 (2개 테스트)
✅ copyScreen - 화면 복사 (1개 테스트)
✅ generateScreenCode - 화면 코드 자동 생성 (2개 테스트)
✅ getTableColumns - 테이블 컬럼 정보 (1개 테스트)

Test Suites: 1 passed
Tests:       18 passed
Time:        1.922s

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

✅ 화면 생명주기 테스트
   - 생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제
✅ 화면 복사 및 레이아웃 테스트
   - 화면 복사 → 레이아웃 저장 → 레이아웃 확인 → 레이아웃 수정
✅ 테이블 정보 조회 테스트
   - 테이블 목록 조회 → 특정 테이블 정보 조회
✅ 일괄 작업 테스트
   - 여러 화면 생성 → 일괄 삭제
✅ 화면 코드 자동 생성 테스트
   - 순차적 화면 코드 생성 검증
✅ 메뉴 할당 테스트 (skip - 실제 메뉴 데이터 필요)

🐛 버그 수정 및 개선사항

실제 운영 환경에서 발견된 이슈

1. 소수점 좌표 저장 오류 (해결 완료)

문제:

invalid input syntax for type integer: "1602.666666666667"
  • position_x, position_y, width, height 컬럼이 integer 타입
  • 격자 계산 시 소수점 값이 발생하여 저장 실패

해결:

Math.round(component.position.x), // 정수로 반올림
Math.round(component.position.y),
Math.round(component.size.width),
Math.round(component.size.height),

테스트 추가:

  • 소수점 좌표 저장 테스트 케이스 추가
  • 반올림 처리 검증

영향 범위:

  • saveLayout() 함수
  • copyScreen() 함수 (레이아웃 복사 시)

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