phase 2.3 테이블 및 컬럼 동적생성기능 변경

This commit is contained in:
kjs
2025-09-30 18:28:54 +09:00
parent c8c05f1c0d
commit 3c06d35374
5 changed files with 164 additions and 155 deletions

View File

@@ -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 === "*";
};
/**

View File

@@ -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<void> {
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<void> {
// 먼저 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<boolean> {
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<boolean> {
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;