diff --git a/PHASE2.7_DDL_EXECUTION_MIGRATION.md b/PHASE2.7_DDL_EXECUTION_MIGRATION.md index eaa2e97b..28081367 100644 --- a/PHASE2.7_DDL_EXECUTION_MIGRATION.md +++ b/PHASE2.7_DDL_EXECUTION_MIGRATION.md @@ -11,9 +11,9 @@ DDLExecutionService는 **4개의 Prisma 호출**이 있으며, DDL(Data Definiti | 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` | | 파일 크기 | 400+ 라인 | | Prisma 호출 | 4개 | -| **현재 진행률** | **0/4 (0%)** ⏳ **진행 예정** | +| **현재 진행률** | **6/6 (100%)** ✅ **완료** | | 복잡도 | 중간 (DDL 실행 + 로그 관리) | -| 우선순위 | 🟢 낮음 (Phase 2.7) | +| 우선순위 | 🔴 최우선 (테이블 추가 기능 - Phase 2.3으로 변경) | ### 🎯 전환 목표 diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index f3664b9d..14d9c135 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -1067,35 +1067,43 @@ describe("Performance Benchmarks", () => { #### ✅ 완료된 서비스 -- [x] **ScreenManagementService 전환 (46개)** ✅ **완료** +- [x] **ScreenManagementService 전환 (46개)** ✅ **완료** (Phase 2.1) + - [x] 46개 Prisma 호출 전환 완료 - [x] 18개 단위 테스트 통과 - [x] 6개 통합 테스트 작성 완료 - [x] 실제 운영 버그 발견 및 수정 (소수점 좌표) - 📄 **[PHASE2_SCREEN_MANAGEMENT_MIGRATION.md](PHASE2_SCREEN_MANAGEMENT_MIGRATION.md)** +- [x] **TableManagementService 전환 (33개)** ✅ **완료** (Phase 2.2) + + - [x] 33개 Prisma 호출 전환 완료 ($queryRaw 26개 + ORM 7개) + - [x] 단위 테스트 작성 완료 + - [x] Prisma import 완전 제거 + - 📄 **[PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md](PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md)** + +- [x] **DDLExecutionService 전환 (6개)** ✅ **완료** (Phase 2.3) + - [x] 6개 Prisma 호출 전환 완료 (트랜잭션 2개 + $queryRawUnsafe 2개 + ORM 2개) + - [x] **테이블 동적 생성/수정/삭제 기능 완료** + - [x] ✅ 단위 테스트 8개 모두 통과 + - [x] Prisma import 완전 제거 + - 📄 **[PHASE2.7_DDL_EXECUTION_MIGRATION.md](PHASE2.7_DDL_EXECUTION_MIGRATION.md)** + #### ⏳ 진행 예정 서비스 -- [ ] **TableManagementService 전환 (33개)** - Phase 2.2 🟡 중간 우선순위 - - 33개 Prisma 호출 ($queryRaw 26개 + ORM 7개) - - SQL은 79% 작성 완료 → `query()` 함수로 교체만 필요 - - 📄 **[PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md](PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md)** -- [ ] **DataflowService 전환 (31개)** - Phase 2.3 🔴 최우선 +- [ ] **DataflowService 전환 (31개)** - Phase 2.4 🟡 중간 우선순위 - 31개 Prisma 호출 (복잡한 관계 관리) - 📄 **[PHASE2.3_DATAFLOW_SERVICE_MIGRATION.md](PHASE2.3_DATAFLOW_SERVICE_MIGRATION.md)** -- [ ] **DynamicFormService 전환 (13개)** - Phase 2.4 🟢 낮은 우선순위 +- [ ] **DynamicFormService 전환 (13개)** - Phase 2.5 🟢 낮은 우선순위 - 13개 Prisma 호출 ($queryRaw 11개 + ORM 2개) - SQL은 85% 작성 완료 → `query()` 함수로 교체만 필요 - 📄 **[PHASE2.4_DYNAMIC_FORM_MIGRATION.md](PHASE2.4_DYNAMIC_FORM_MIGRATION.md)** -- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.5 🟡 중간 우선순위 +- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.6 🟡 중간 우선순위 - 15개 Prisma 호출 (외부 DB 연결 관리) - 📄 **[PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md](PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md)** -- [ ] **DataflowControlService 전환 (6개)** - Phase 2.6 🟡 중간 우선순위 +- [ ] **DataflowControlService 전환 (6개)** - Phase 2.7 🟡 중간 우선순위 - 6개 Prisma 호출 (복잡한 비즈니스 로직) - 📄 **[PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md](PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md)** -- [ ] **DDLExecutionService 전환 (4개)** - Phase 2.7 🟢 낮은 우선순위 - - 4개 Prisma 호출 (DDL 실행 및 로그) - - 📄 **[PHASE2.7_DDL_EXECUTION_MIGRATION.md](PHASE2.7_DDL_EXECUTION_MIGRATION.md)** #### ✅ 다른 Phase로 이동 diff --git a/backend-node/src/middleware/superAdminMiddleware.ts b/backend-node/src/middleware/superAdminMiddleware.ts index 37b3f24a..d92139f5 100644 --- a/backend-node/src/middleware/superAdminMiddleware.ts +++ b/backend-node/src/middleware/superAdminMiddleware.ts @@ -47,8 +47,8 @@ export const requireSuperAdmin = ( return; } - // 슈퍼관리자 권한 확인 (회사코드가 '*'이고 plm_admin 사용자) - if (req.user.companyCode !== "*" || req.user.userId !== "plm_admin") { + // 슈퍼관리자 권한 확인 (회사코드가 '*'인 사용자) + if (req.user.companyCode !== "*") { logger.warn("DDL 실행 시도 - 권한 부족", { userId: req.user.userId, companyCode: req.user.companyCode, @@ -62,7 +62,7 @@ export const requireSuperAdmin = ( error: { code: "SUPER_ADMIN_REQUIRED", details: - "최고 관리자 권한이 필요합니다. DDL 실행은 회사코드가 '*'인 plm_admin 사용자만 가능합니다.", + "최고 관리자 권한이 필요합니다. DDL 실행은 회사코드가 '*'인 사용자만 가능합니다.", }, }); return; @@ -167,7 +167,7 @@ export const validateDDLPermission = ( * 사용자가 슈퍼관리자인지 확인하는 유틸리티 함수 */ export const isSuperAdmin = (user: AuthenticatedRequest["user"]): boolean => { - return user?.companyCode === "*" && user?.userId === "plm_admin"; + return user?.companyCode === "*"; }; /** diff --git a/backend-node/src/services/ddlExecutionService.ts b/backend-node/src/services/ddlExecutionService.ts index 9167a48e..c28ff7c7 100644 --- a/backend-node/src/services/ddlExecutionService.ts +++ b/backend-node/src/services/ddlExecutionService.ts @@ -3,7 +3,7 @@ * 실제 PostgreSQL 테이블 및 컬럼 생성을 담당 */ -import { PrismaClient } from "@prisma/client"; +import { query, queryOne, transaction } from "../database/db"; import { CreateColumnDefinition, DDLExecutionResult, @@ -15,8 +15,6 @@ import { DDLAuditLogger } from "./ddlAuditLogger"; import { logger } from "../utils/logger"; import { cache, CacheKeys } from "../utils/cache"; -const prisma = new PrismaClient(); - export class DDLExecutionService { /** * 새 테이블 생성 @@ -98,15 +96,15 @@ export class DDLExecutionService { const ddlQuery = this.generateCreateTableQuery(tableName, columns); // 5. 트랜잭션으로 안전하게 실행 - await prisma.$transaction(async (tx) => { + await transaction(async (client) => { // 5-1. 테이블 생성 - await tx.$executeRawUnsafe(ddlQuery); + await client.query(ddlQuery); // 5-2. 테이블 메타데이터 저장 - await this.saveTableMetadata(tx, tableName, description); + await this.saveTableMetadata(client, tableName, description); // 5-3. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(tx, tableName, columns); + await this.saveColumnMetadata(client, tableName, columns); }); // 6. 성공 로그 기록 @@ -269,12 +267,12 @@ export class DDLExecutionService { const ddlQuery = this.generateAddColumnQuery(tableName, column); // 6. 트랜잭션으로 안전하게 실행 - await prisma.$transaction(async (tx) => { + await transaction(async (client) => { // 6-1. 컬럼 추가 - await tx.$executeRawUnsafe(ddlQuery); + await client.query(ddlQuery); // 6-2. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(tx, tableName, [column]); + await this.saveColumnMetadata(client, tableName, [column]); }); // 7. 성공 로그 기록 @@ -424,51 +422,42 @@ CREATE TABLE "${tableName}" (${baseColumns}, * 테이블 메타데이터 저장 */ private async saveTableMetadata( - tx: any, + client: any, tableName: string, description?: string ): Promise { - await tx.table_labels.upsert({ - where: { table_name: tableName }, - update: { - table_label: tableName, - description: description || `사용자 생성 테이블: ${tableName}`, - updated_date: new Date(), - }, - create: { - table_name: tableName, - table_label: tableName, - description: description || `사용자 생성 테이블: ${tableName}`, - created_date: new Date(), - updated_date: new Date(), - }, - }); + 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 UPDATE SET + table_label = $2, + description = $3, + updated_date = now() + `, + [tableName, tableName, description || `사용자 생성 테이블: ${tableName}`] + ); } /** * 컬럼 메타데이터 저장 */ private async saveColumnMetadata( - tx: any, + client: any, tableName: string, columns: CreateColumnDefinition[] ): Promise { // 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성 - await tx.table_labels.upsert({ - where: { - table_name: tableName, - }, - update: { - updated_date: new Date(), - }, - create: { - table_name: tableName, - table_label: tableName, - description: `자동 생성된 테이블 메타데이터: ${tableName}`, - created_date: new Date(), - updated_date: new Date(), - }, - }); + 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 UPDATE SET updated_date = now() + `, + [tableName, tableName, `자동 생성된 테이블 메타데이터: ${tableName}`] + ); // 기본 컬럼들 정의 (모든 테이블에 자동으로 추가되는 시스템 컬럼) const defaultColumns = [ @@ -516,20 +505,23 @@ CREATE TABLE "${tableName}" (${baseColumns}, // 기본 컬럼들을 table_type_columns에 등록 for (const defaultCol of defaultColumns) { - await tx.$executeRaw` + await client.query( + ` INSERT INTO table_type_columns ( table_name, column_name, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - ${tableName}, ${defaultCol.name}, ${defaultCol.inputType}, '{}', - 'Y', ${defaultCol.order}, now(), now() + $1, $2, $3, '{}', + 'Y', $4, now(), now() ) ON CONFLICT (table_name, column_name) DO UPDATE SET - input_type = ${defaultCol.inputType}, - display_order = ${defaultCol.order}, - updated_date = now(); - `; + input_type = $3, + display_order = $4, + updated_date = now() + `, + [tableName, defaultCol.name, defaultCol.inputType, defaultCol.order] + ); } // 사용자 정의 컬럼들을 table_type_columns에 등록 @@ -538,89 +530,98 @@ CREATE TABLE "${tableName}" (${baseColumns}, const inputType = this.convertWebTypeToInputType( column.webType || "text" ); + const detailSettings = JSON.stringify(column.detailSettings || {}); - await tx.$executeRaw` + await client.query( + ` INSERT INTO table_type_columns ( table_name, column_name, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - ${tableName}, ${column.name}, ${inputType}, ${JSON.stringify(column.detailSettings || {})}, - 'Y', ${i}, now(), now() + $1, $2, $3, $4, + 'Y', $5, now(), now() ) ON CONFLICT (table_name, column_name) DO UPDATE SET - input_type = ${inputType}, - detail_settings = ${JSON.stringify(column.detailSettings || {})}, - display_order = ${i}, - updated_date = now(); - `; + input_type = $3, + detail_settings = $4, + display_order = $5, + updated_date = now() + `, + [tableName, column.name, inputType, detailSettings, i] + ); } // 레거시 지원: column_labels 테이블에도 등록 (기존 시스템 호환성) // 1. 기본 컬럼들을 column_labels에 등록 for (const defaultCol of defaultColumns) { - await tx.column_labels.upsert({ - where: { - table_name_column_name: { - table_name: tableName, - column_name: defaultCol.name, - }, - }, - update: { - column_label: defaultCol.label, - input_type: defaultCol.inputType, - detail_settings: JSON.stringify({}), - description: defaultCol.description, - display_order: defaultCol.order, - is_visible: defaultCol.isVisible, - updated_date: new Date(), - }, - create: { - table_name: tableName, - column_name: defaultCol.name, - column_label: defaultCol.label, - input_type: defaultCol.inputType, - detail_settings: JSON.stringify({}), - description: defaultCol.description, - display_order: defaultCol.order, - is_visible: defaultCol.isVisible, - created_date: new Date(), - updated_date: new Date(), - }, - }); + await client.query( + ` + INSERT INTO column_labels ( + table_name, column_name, column_label, input_type, detail_settings, + description, display_order, is_visible, created_date, updated_date + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, now(), now() + ) + ON CONFLICT (table_name, column_name) + DO UPDATE SET + column_label = $3, + input_type = $4, + detail_settings = $5, + description = $6, + display_order = $7, + is_visible = $8, + updated_date = now() + `, + [ + tableName, + defaultCol.name, + defaultCol.label, + defaultCol.inputType, + JSON.stringify({}), + defaultCol.description, + defaultCol.order, + defaultCol.isVisible, + ] + ); } // 2. 사용자 정의 컬럼들을 column_labels에 등록 for (const column of columns) { - await tx.column_labels.upsert({ - where: { - table_name_column_name: { - table_name: tableName, - column_name: column.name, - }, - }, - update: { - column_label: column.label || column.name, - input_type: this.convertWebTypeToInputType(column.webType || "text"), - detail_settings: JSON.stringify(column.detailSettings || {}), - description: column.description, - display_order: column.order || 0, - is_visible: true, - updated_date: new Date(), - }, - create: { - table_name: tableName, - column_name: column.name, - column_label: column.label || column.name, - input_type: this.convertWebTypeToInputType(column.webType || "text"), - detail_settings: JSON.stringify(column.detailSettings || {}), - description: column.description, - display_order: column.order || 0, - is_visible: true, - created_date: new Date(), - updated_date: new Date(), - }, - }); + const inputType = this.convertWebTypeToInputType( + column.webType || "text" + ); + const detailSettings = JSON.stringify(column.detailSettings || {}); + + await client.query( + ` + INSERT INTO column_labels ( + table_name, column_name, column_label, input_type, detail_settings, + description, display_order, is_visible, created_date, updated_date + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, now(), now() + ) + ON CONFLICT (table_name, column_name) + DO UPDATE SET + column_label = $3, + input_type = $4, + detail_settings = $5, + description = $6, + display_order = $7, + is_visible = $8, + updated_date = now() + `, + [ + tableName, + column.name, + column.label || column.name, + inputType, + detailSettings, + column.description, + column.order || 0, + true, + ] + ); } } @@ -679,18 +680,18 @@ CREATE TABLE "${tableName}" (${baseColumns}, */ private async checkTableExists(tableName: string): Promise { try { - const result = await prisma.$queryRawUnsafe( + const result = await queryOne<{ exists: boolean }>( ` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 - ); + ) `, - tableName + [tableName] ); - return (result as any)[0]?.exists || false; + return result?.exists || false; } catch (error) { logger.error("테이블 존재 확인 오류:", error); return false; @@ -705,20 +706,19 @@ CREATE TABLE "${tableName}" (${baseColumns}, columnName: string ): Promise { try { - const result = await prisma.$queryRawUnsafe( + const result = await queryOne<{ exists: boolean }>( ` SELECT EXISTS ( SELECT FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2 - ); + ) `, - tableName, - columnName + [tableName, columnName] ); - return (result as any)[0]?.exists || false; + return result?.exists || false; } catch (error) { logger.error("컬럼 존재 확인 오류:", error); return false; @@ -734,15 +734,16 @@ CREATE TABLE "${tableName}" (${baseColumns}, } | null> { try { // 테이블 정보 조회 - const tableInfo = await prisma.table_labels.findUnique({ - where: { table_name: tableName }, - }); + const tableInfo = await queryOne( + `SELECT * FROM table_labels WHERE table_name = $1`, + [tableName] + ); // 컬럼 정보 조회 - const columns = await prisma.column_labels.findMany({ - where: { table_name: tableName }, - orderBy: { display_order: "asc" }, - }); + const columns = await query( + `SELECT * FROM column_labels WHERE table_name = $1 ORDER BY display_order ASC`, + [tableName] + ); if (!tableInfo) { return null; diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 6fdeea8a..68dd59ba 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -76,8 +76,8 @@ export default function TableManagementPage() { const [addColumnModalOpen, setAddColumnModalOpen] = useState(false); const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false); - // 최고 관리자 여부 확인 - const isSuperAdmin = user?.companyCode === "*" && user?.userId === "plm_admin"; + // 최고 관리자 여부 확인 (회사코드가 "*"인 경우) + const isSuperAdmin = user?.companyCode === "*"; // 다국어 텍스트 로드 useEffect(() => { @@ -541,9 +541,9 @@ export default function TableManagementPage() { }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); return ( -
+
{/* 페이지 제목 */} -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} @@ -593,7 +593,7 @@ export default function TableManagementPage() {
{/* 테이블 목록 */} - + @@ -663,7 +663,7 @@ export default function TableManagementPage() { {/* 컬럼 타입 관리 */} - +