phase 2.3 테이블 및 컬럼 동적생성기능 변경
This commit is contained in:
@@ -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 === "*";
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user