2025-09-22 17:00:59 +09:00
|
|
|
/**
|
|
|
|
|
* DDL 실행 서비스
|
|
|
|
|
* 실제 PostgreSQL 테이블 및 컬럼 생성을 담당
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { PrismaClient } from "@prisma/client";
|
|
|
|
|
import {
|
|
|
|
|
CreateColumnDefinition,
|
|
|
|
|
DDLExecutionResult,
|
|
|
|
|
WEB_TYPE_TO_POSTGRES_MAP,
|
|
|
|
|
WebType,
|
|
|
|
|
} from "../types/ddl";
|
|
|
|
|
import { DDLSafetyValidator } from "./ddlSafetyValidator";
|
|
|
|
|
import { DDLAuditLogger } from "./ddlAuditLogger";
|
|
|
|
|
import { logger } from "../utils/logger";
|
|
|
|
|
import { cache, CacheKeys } from "../utils/cache";
|
|
|
|
|
|
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
|
|
|
|
export class DDLExecutionService {
|
|
|
|
|
/**
|
|
|
|
|
* 새 테이블 생성
|
|
|
|
|
*/
|
|
|
|
|
async createTable(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columns: CreateColumnDefinition[],
|
|
|
|
|
userCompanyCode: string,
|
|
|
|
|
userId: string,
|
|
|
|
|
description?: string
|
|
|
|
|
): Promise<DDLExecutionResult> {
|
|
|
|
|
// DDL 실행 시작 로그
|
|
|
|
|
await DDLAuditLogger.logDDLStart(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"CREATE_TABLE",
|
|
|
|
|
tableName,
|
|
|
|
|
{ columns, description }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. 권한 검증
|
|
|
|
|
this.validateSuperAdminPermission(userCompanyCode);
|
|
|
|
|
|
|
|
|
|
// 2. 안전성 검증
|
|
|
|
|
const validation = DDLSafetyValidator.validateTableCreation(
|
|
|
|
|
tableName,
|
|
|
|
|
columns
|
|
|
|
|
);
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
const errorMessage = `테이블 생성 검증 실패: ${validation.errors.join(", ")}`;
|
|
|
|
|
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"CREATE_TABLE",
|
|
|
|
|
tableName,
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "VALIDATION_FAILED",
|
|
|
|
|
details: validation.errors.join(", "),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 테이블 존재 여부 확인
|
|
|
|
|
const tableExists = await this.checkTableExists(tableName);
|
|
|
|
|
if (tableExists) {
|
|
|
|
|
const errorMessage = `테이블 '${tableName}'이 이미 존재합니다.`;
|
|
|
|
|
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"CREATE_TABLE",
|
|
|
|
|
tableName,
|
|
|
|
|
"TABLE_EXISTS",
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "TABLE_EXISTS",
|
|
|
|
|
details: errorMessage,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. DDL 쿼리 생성
|
|
|
|
|
const ddlQuery = this.generateCreateTableQuery(tableName, columns);
|
|
|
|
|
|
|
|
|
|
// 5. 트랜잭션으로 안전하게 실행
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
|
|
|
// 5-1. 테이블 생성
|
|
|
|
|
await tx.$executeRawUnsafe(ddlQuery);
|
|
|
|
|
|
|
|
|
|
// 5-2. 테이블 메타데이터 저장
|
|
|
|
|
await this.saveTableMetadata(tx, tableName, description);
|
|
|
|
|
|
|
|
|
|
// 5-3. 컬럼 메타데이터 저장
|
|
|
|
|
await this.saveColumnMetadata(tx, tableName, columns);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 6. 성공 로그 기록
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"CREATE_TABLE",
|
|
|
|
|
tableName,
|
|
|
|
|
ddlQuery,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info("테이블 생성 성공", {
|
|
|
|
|
tableName,
|
|
|
|
|
userId,
|
|
|
|
|
columnCount: columns.length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 테이블 생성 후 관련 캐시 무효화
|
|
|
|
|
this.invalidateTableCache(tableName);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `테이블 '${tableName}'이 성공적으로 생성되었습니다.`,
|
|
|
|
|
executedQuery: ddlQuery,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = `테이블 생성 실패: ${(error as Error).message}`;
|
|
|
|
|
|
|
|
|
|
// 실패 로그 기록
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"CREATE_TABLE",
|
|
|
|
|
tableName,
|
|
|
|
|
`FAILED: ${(error as Error).message}`,
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.error("테이블 생성 실패:", {
|
|
|
|
|
tableName,
|
|
|
|
|
userId,
|
|
|
|
|
error: (error as Error).message,
|
|
|
|
|
stack: (error as Error).stack,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "EXECUTION_FAILED",
|
|
|
|
|
details: (error as Error).message,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 기존 테이블에 컬럼 추가
|
|
|
|
|
*/
|
|
|
|
|
async addColumn(
|
|
|
|
|
tableName: string,
|
|
|
|
|
column: CreateColumnDefinition,
|
|
|
|
|
userCompanyCode: string,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<DDLExecutionResult> {
|
|
|
|
|
// DDL 실행 시작 로그
|
|
|
|
|
await DDLAuditLogger.logDDLStart(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
{ column }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. 권한 검증
|
|
|
|
|
this.validateSuperAdminPermission(userCompanyCode);
|
|
|
|
|
|
|
|
|
|
// 2. 안전성 검증
|
|
|
|
|
const validation = DDLSafetyValidator.validateColumnAddition(
|
|
|
|
|
tableName,
|
|
|
|
|
column
|
|
|
|
|
);
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
const errorMessage = `컬럼 추가 검증 실패: ${validation.errors.join(", ")}`;
|
|
|
|
|
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "VALIDATION_FAILED",
|
|
|
|
|
details: validation.errors.join(", "),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 테이블 존재 여부 확인
|
|
|
|
|
const tableExists = await this.checkTableExists(tableName);
|
|
|
|
|
if (!tableExists) {
|
|
|
|
|
const errorMessage = `테이블 '${tableName}'이 존재하지 않습니다.`;
|
|
|
|
|
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
"TABLE_NOT_EXISTS",
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "TABLE_NOT_EXISTS",
|
|
|
|
|
details: errorMessage,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 컬럼 존재 여부 확인
|
|
|
|
|
const columnExists = await this.checkColumnExists(tableName, column.name);
|
|
|
|
|
if (columnExists) {
|
|
|
|
|
const errorMessage = `컬럼 '${column.name}'이 이미 존재합니다.`;
|
|
|
|
|
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
"COLUMN_EXISTS",
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "COLUMN_EXISTS",
|
|
|
|
|
details: errorMessage,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. DDL 쿼리 생성
|
|
|
|
|
const ddlQuery = this.generateAddColumnQuery(tableName, column);
|
|
|
|
|
|
|
|
|
|
// 6. 트랜잭션으로 안전하게 실행
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
|
|
|
// 6-1. 컬럼 추가
|
|
|
|
|
await tx.$executeRawUnsafe(ddlQuery);
|
|
|
|
|
|
|
|
|
|
// 6-2. 컬럼 메타데이터 저장
|
|
|
|
|
await this.saveColumnMetadata(tx, tableName, [column]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 7. 성공 로그 기록
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
ddlQuery,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info("컬럼 추가 성공", {
|
|
|
|
|
tableName,
|
|
|
|
|
columnName: column.name,
|
|
|
|
|
webType: column.webType,
|
|
|
|
|
userId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 컬럼 추가 후 관련 캐시 무효화
|
|
|
|
|
this.invalidateTableCache(tableName);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `컬럼 '${column.name}'이 성공적으로 추가되었습니다.`,
|
|
|
|
|
executedQuery: ddlQuery,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = `컬럼 추가 실패: ${(error as Error).message}`;
|
|
|
|
|
|
|
|
|
|
// 실패 로그 기록
|
|
|
|
|
await DDLAuditLogger.logDDLExecution(
|
|
|
|
|
userId,
|
|
|
|
|
userCompanyCode,
|
|
|
|
|
"ADD_COLUMN",
|
|
|
|
|
tableName,
|
|
|
|
|
`FAILED: ${(error as Error).message}`,
|
|
|
|
|
false,
|
|
|
|
|
errorMessage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.error("컬럼 추가 실패:", {
|
|
|
|
|
tableName,
|
|
|
|
|
columnName: column.name,
|
|
|
|
|
userId,
|
|
|
|
|
error: (error as Error).message,
|
|
|
|
|
stack: (error as Error).stack,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
error: {
|
|
|
|
|
code: "EXECUTION_FAILED",
|
|
|
|
|
details: (error as Error).message,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* CREATE TABLE DDL 쿼리 생성
|
|
|
|
|
*/
|
|
|
|
|
private generateCreateTableQuery(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columns: CreateColumnDefinition[]
|
|
|
|
|
): string {
|
2025-09-23 10:40:21 +09:00
|
|
|
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 통일
|
2025-09-22 17:00:59 +09:00
|
|
|
const columnDefinitions = columns
|
|
|
|
|
.map((col) => {
|
2025-09-23 10:40:21 +09:00
|
|
|
// 입력 타입과 관계없이 모든 컬럼을 VARCHAR(500)로 생성
|
|
|
|
|
let definition = `"${col.name}" varchar(500)`;
|
2025-09-22 17:00:59 +09:00
|
|
|
|
|
|
|
|
if (!col.nullable) {
|
|
|
|
|
definition += " NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (col.defaultValue) {
|
|
|
|
|
definition += ` DEFAULT '${col.defaultValue}'`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return definition;
|
|
|
|
|
})
|
|
|
|
|
.join(",\n ");
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
// 기본 컬럼들 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
|
2025-09-22 17:00:59 +09:00
|
|
|
const baseColumns = `
|
2025-09-23 10:40:21 +09:00
|
|
|
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
2025-09-22 17:00:59 +09:00
|
|
|
"created_date" timestamp DEFAULT now(),
|
|
|
|
|
"updated_date" timestamp DEFAULT now(),
|
2025-09-23 10:40:21 +09:00
|
|
|
"writer" varchar(500),
|
|
|
|
|
"company_code" varchar(500)`;
|
2025-09-22 17:00:59 +09:00
|
|
|
|
|
|
|
|
// 최종 CREATE TABLE 쿼리
|
|
|
|
|
return `
|
|
|
|
|
CREATE TABLE "${tableName}" (${baseColumns},
|
|
|
|
|
${columnDefinitions}
|
|
|
|
|
);`.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ALTER TABLE ADD COLUMN DDL 쿼리 생성
|
|
|
|
|
*/
|
|
|
|
|
private generateAddColumnQuery(
|
|
|
|
|
tableName: string,
|
|
|
|
|
column: CreateColumnDefinition
|
|
|
|
|
): string {
|
2025-09-23 10:40:21 +09:00
|
|
|
// 새로 추가되는 컬럼도 VARCHAR(500)로 통일
|
|
|
|
|
let definition = `"${column.name}" varchar(500)`;
|
2025-09-22 17:00:59 +09:00
|
|
|
|
|
|
|
|
if (!column.nullable) {
|
|
|
|
|
definition += " NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (column.defaultValue) {
|
|
|
|
|
definition += ` DEFAULT '${column.defaultValue}'`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `ALTER TABLE "${tableName}" ADD COLUMN ${definition};`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-23 10:40:21 +09:00
|
|
|
* 입력타입을 PostgreSQL 타입으로 매핑 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
|
|
|
|
|
* 날짜 타입만 TIMESTAMP로, 나머지는 VARCHAR(500)로 통일
|
2025-09-22 17:00:59 +09:00
|
|
|
*/
|
2025-09-23 10:40:21 +09:00
|
|
|
private mapInputTypeToPostgresType(inputType?: string): string {
|
|
|
|
|
switch (inputType) {
|
|
|
|
|
case "date":
|
|
|
|
|
return "timestamp";
|
|
|
|
|
default:
|
|
|
|
|
// 날짜 외의 모든 타입은 VARCHAR(500)로 통일
|
|
|
|
|
return "varchar(500)";
|
2025-09-22 17:00:59 +09:00
|
|
|
}
|
2025-09-23 10:40:21 +09:00
|
|
|
}
|
2025-09-22 17:00:59 +09:00
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
/**
|
|
|
|
|
* 레거시 지원: 웹타입을 PostgreSQL 타입으로 매핑
|
|
|
|
|
* @deprecated 새로운 시스템에서는 mapInputTypeToPostgresType 사용
|
|
|
|
|
*/
|
|
|
|
|
private mapWebTypeToPostgresType(webType: WebType, length?: number): string {
|
|
|
|
|
// 레거시 지원을 위해 유지하되, VARCHAR(500)로 통일
|
|
|
|
|
logger.info(`레거시 웹타입 사용: ${webType} → varchar(500)로 변환`);
|
|
|
|
|
return "varchar(500)";
|
2025-09-22 17:00:59 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 메타데이터 저장
|
|
|
|
|
*/
|
|
|
|
|
private async saveTableMetadata(
|
|
|
|
|
tx: 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(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 메타데이터 저장
|
|
|
|
|
*/
|
|
|
|
|
private async saveColumnMetadata(
|
|
|
|
|
tx: 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(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
// 기본 컬럼들 정의 (모든 테이블에 자동으로 추가되는 시스템 컬럼)
|
|
|
|
|
const defaultColumns = [
|
|
|
|
|
{
|
|
|
|
|
name: "id",
|
|
|
|
|
label: "ID",
|
|
|
|
|
inputType: "text",
|
|
|
|
|
description: "기본키 (자동생성)",
|
|
|
|
|
order: -5,
|
|
|
|
|
isVisible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "created_date",
|
|
|
|
|
label: "생성일시",
|
|
|
|
|
inputType: "date",
|
|
|
|
|
description: "레코드 생성일시",
|
|
|
|
|
order: -4,
|
|
|
|
|
isVisible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "updated_date",
|
|
|
|
|
label: "수정일시",
|
|
|
|
|
inputType: "date",
|
|
|
|
|
description: "레코드 수정일시",
|
|
|
|
|
order: -3,
|
|
|
|
|
isVisible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "writer",
|
|
|
|
|
label: "작성자",
|
|
|
|
|
inputType: "text",
|
|
|
|
|
description: "레코드 작성자",
|
|
|
|
|
order: -2,
|
|
|
|
|
isVisible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "company_code",
|
|
|
|
|
label: "회사코드",
|
|
|
|
|
inputType: "text",
|
|
|
|
|
description: "회사 구분 코드",
|
|
|
|
|
order: -1,
|
|
|
|
|
isVisible: true,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 기본 컬럼들을 table_type_columns에 등록
|
|
|
|
|
for (const defaultCol of defaultColumns) {
|
|
|
|
|
await tx.$executeRaw`
|
|
|
|
|
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()
|
|
|
|
|
)
|
|
|
|
|
ON CONFLICT (table_name, column_name)
|
|
|
|
|
DO UPDATE SET
|
|
|
|
|
input_type = ${defaultCol.inputType},
|
|
|
|
|
display_order = ${defaultCol.order},
|
|
|
|
|
updated_date = now();
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 사용자 정의 컬럼들을 table_type_columns에 등록
|
|
|
|
|
for (let i = 0; i < columns.length; i++) {
|
|
|
|
|
const column = columns[i];
|
|
|
|
|
const inputType = this.convertWebTypeToInputType(
|
|
|
|
|
column.webType || "text"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await tx.$executeRaw`
|
|
|
|
|
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()
|
|
|
|
|
)
|
|
|
|
|
ON CONFLICT (table_name, column_name)
|
|
|
|
|
DO UPDATE SET
|
|
|
|
|
input_type = ${inputType},
|
|
|
|
|
detail_settings = ${JSON.stringify(column.detailSettings || {})},
|
|
|
|
|
display_order = ${i},
|
|
|
|
|
updated_date = now();
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 레거시 지원: 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(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 사용자 정의 컬럼들을 column_labels에 등록
|
2025-09-22 17:00:59 +09:00
|
|
|
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,
|
2025-09-23 10:40:21 +09:00
|
|
|
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
2025-09-22 17:00:59 +09:00
|
|
|
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,
|
2025-09-23 10:40:21 +09:00
|
|
|
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
2025-09-22 17:00:59 +09:00
|
|
|
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(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
/**
|
|
|
|
|
* 웹 타입을 입력 타입으로 변환
|
|
|
|
|
*/
|
|
|
|
|
private convertWebTypeToInputType(webType: string): string {
|
|
|
|
|
const webTypeToInputTypeMap: Record<string, string> = {
|
|
|
|
|
// 텍스트 관련
|
|
|
|
|
text: "text",
|
|
|
|
|
textarea: "text",
|
|
|
|
|
email: "text",
|
|
|
|
|
tel: "text",
|
|
|
|
|
url: "text",
|
|
|
|
|
password: "text",
|
|
|
|
|
|
|
|
|
|
// 숫자 관련
|
|
|
|
|
number: "number",
|
|
|
|
|
decimal: "number",
|
|
|
|
|
|
|
|
|
|
// 날짜 관련
|
|
|
|
|
date: "date",
|
|
|
|
|
datetime: "date",
|
|
|
|
|
time: "date",
|
|
|
|
|
|
|
|
|
|
// 선택 관련
|
|
|
|
|
select: "select",
|
|
|
|
|
dropdown: "select",
|
|
|
|
|
checkbox: "checkbox",
|
|
|
|
|
boolean: "checkbox",
|
|
|
|
|
radio: "radio",
|
|
|
|
|
|
|
|
|
|
// 참조 관련
|
|
|
|
|
code: "code",
|
|
|
|
|
entity: "entity",
|
|
|
|
|
|
|
|
|
|
// 기타
|
|
|
|
|
file: "text",
|
|
|
|
|
button: "text",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return webTypeToInputTypeMap[webType] || "text";
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 17:00:59 +09:00
|
|
|
/**
|
|
|
|
|
* 권한 검증 (슈퍼관리자 확인)
|
|
|
|
|
*/
|
|
|
|
|
private validateSuperAdminPermission(userCompanyCode: string): void {
|
|
|
|
|
if (userCompanyCode !== "*") {
|
|
|
|
|
throw new Error("최고 관리자 권한이 필요합니다.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 존재 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
private async checkTableExists(tableName: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const result = await prisma.$queryRawUnsafe(
|
|
|
|
|
`
|
|
|
|
|
SELECT EXISTS (
|
|
|
|
|
SELECT FROM information_schema.tables
|
|
|
|
|
WHERE table_schema = 'public'
|
|
|
|
|
AND table_name = $1
|
|
|
|
|
);
|
|
|
|
|
`,
|
|
|
|
|
tableName
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (result as any)[0]?.exists || false;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("테이블 존재 확인 오류:", error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 존재 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
private async checkColumnExists(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const result = await prisma.$queryRawUnsafe(
|
|
|
|
|
`
|
|
|
|
|
SELECT EXISTS (
|
|
|
|
|
SELECT FROM information_schema.columns
|
|
|
|
|
WHERE table_schema = 'public'
|
|
|
|
|
AND table_name = $1
|
|
|
|
|
AND column_name = $2
|
|
|
|
|
);
|
|
|
|
|
`,
|
|
|
|
|
tableName,
|
|
|
|
|
columnName
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (result as any)[0]?.exists || false;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("컬럼 존재 확인 오류:", error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 생성된 테이블 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
async getCreatedTableInfo(tableName: string): Promise<{
|
|
|
|
|
tableInfo: any;
|
|
|
|
|
columns: any[];
|
|
|
|
|
} | null> {
|
|
|
|
|
try {
|
|
|
|
|
// 테이블 정보 조회
|
|
|
|
|
const tableInfo = await prisma.table_labels.findUnique({
|
|
|
|
|
where: { table_name: tableName },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 컬럼 정보 조회
|
|
|
|
|
const columns = await prisma.column_labels.findMany({
|
|
|
|
|
where: { table_name: tableName },
|
|
|
|
|
orderBy: { display_order: "asc" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!tableInfo) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tableInfo,
|
|
|
|
|
columns,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("생성된 테이블 정보 조회 실패:", error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 관련 캐시 무효화
|
|
|
|
|
* DDL 작업 후 호출하여 캐시된 데이터를 클리어
|
|
|
|
|
*/
|
|
|
|
|
private invalidateTableCache(tableName: string): void {
|
|
|
|
|
try {
|
|
|
|
|
// 테이블 컬럼 관련 캐시 무효화
|
|
|
|
|
const columnCacheDeleted = cache.deleteByPattern(
|
|
|
|
|
`table_columns:${tableName}`
|
|
|
|
|
);
|
|
|
|
|
const countCacheDeleted = cache.deleteByPattern(
|
|
|
|
|
`table_column_count:${tableName}`
|
|
|
|
|
);
|
|
|
|
|
cache.delete("table_list");
|
|
|
|
|
|
|
|
|
|
const totalDeleted = columnCacheDeleted + countCacheDeleted + 1;
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
`테이블 캐시 무효화 완료: ${tableName}, 삭제된 키: ${totalDeleted}개`
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(`테이블 캐시 무효화 실패: ${tableName}`, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|