16 KiB
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단계: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
- 2단계: 복잡한 조회 전환 (include, join)
- 3단계: 트랜잭션 전환
- 4단계: Raw Query 개선
전략 2: 함수별 전환 우선순위
🔴 최우선 (기본 CRUD)
createScreen()- Line 70getScreensByCompany()- Line 99-105getScreenByCode()- Line 178getScreenById()- Line 205updateScreen()- Line 236deleteScreen()- Line 672
🟡 2순위 (레이아웃)
saveLayout()- Line 1096-1152getLayout()- Line 1193deleteLayout()- Line 1096
🟢 3순위 (템플릿 & 메뉴)
getTemplates()- Line 1303createTemplate()- Line 1317assignToMenu()- Line 1358getMenuAssignments()- Line 1376removeMenuAssignment()- 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) 상태: ✅ 완료