테이블 추가기능 수정사항

This commit is contained in:
kjs
2025-09-23 10:40:21 +09:00
parent 474cc33aee
commit e653effac0
19 changed files with 1931 additions and 201 deletions

View File

@@ -342,14 +342,11 @@ export class DDLExecutionService {
tableName: string,
columns: CreateColumnDefinition[]
): string {
// 사용자 정의 컬럼들
// 사용자 정의 컬럼들 - 모두 VARCHAR(500)로 통일
const columnDefinitions = columns
.map((col) => {
const postgresType = this.mapWebTypeToPostgresType(
col.webType,
col.length
);
let definition = `"${col.name}" ${postgresType}`;
// 입력 타입과 관계없이 모든 컬럼을 VARCHAR(500)로 생성
let definition = `"${col.name}" varchar(500)`;
if (!col.nullable) {
definition += " NOT NULL";
@@ -363,13 +360,13 @@ export class DDLExecutionService {
})
.join(",\n ");
// 기본 컬럼들 (시스템 필수 컬럼)
// 기본 컬럼들 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
const baseColumns = `
"id" serial PRIMARY KEY,
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
"created_date" timestamp DEFAULT now(),
"updated_date" timestamp DEFAULT now(),
"writer" varchar(100),
"company_code" varchar(50) DEFAULT '*'`;
"writer" varchar(500),
"company_code" varchar(500)`;
// 최종 CREATE TABLE 쿼리
return `
@@ -385,11 +382,8 @@ CREATE TABLE "${tableName}" (${baseColumns},
tableName: string,
column: CreateColumnDefinition
): string {
const postgresType = this.mapWebTypeToPostgresType(
column.webType,
column.length
);
let definition = `"${column.name}" ${postgresType}`;
// 새로 추가되는 컬럼도 VARCHAR(500)로 통일
let definition = `"${column.name}" varchar(500)`;
if (!column.nullable) {
definition += " NOT NULL";
@@ -403,23 +397,27 @@ CREATE TABLE "${tableName}" (${baseColumns},
}
/**
* 타입을 PostgreSQL 타입으로 매핑
* 입력타입을 PostgreSQL 타입으로 매핑 (날짜는 TIMESTAMP, 나머지는 VARCHAR)
* 날짜 타입만 TIMESTAMP로, 나머지는 VARCHAR(500)로 통일
*/
private mapInputTypeToPostgresType(inputType?: string): string {
switch (inputType) {
case "date":
return "timestamp";
default:
// 날짜 외의 모든 타입은 VARCHAR(500)로 통일
return "varchar(500)";
}
}
/**
* 레거시 지원: 웹타입을 PostgreSQL 타입으로 매핑
* @deprecated 새로운 시스템에서는 mapInputTypeToPostgresType 사용
*/
private mapWebTypeToPostgresType(webType: WebType, length?: number): string {
const mapping = WEB_TYPE_TO_POSTGRES_MAP[webType];
if (!mapping) {
logger.warn(`알 수 없는 웹타입: ${webType}, text로 대체`);
return "text";
}
if (mapping.supportsLength && length && length > 0) {
if (mapping.postgresType === "varchar") {
return `varchar(${length})`;
}
}
return mapping.postgresType;
// 레거시 지원을 위해 유지하되, VARCHAR(500)로 통일
logger.info(`레거시 웹타입 사용: ${webType} → varchar(500)로 변환`);
return "varchar(500)";
}
/**
@@ -472,6 +470,127 @@ CREATE TABLE "${tableName}" (${baseColumns},
},
});
// 기본 컬럼들 정의 (모든 테이블에 자동으로 추가되는 시스템 컬럼)
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에 등록
for (const column of columns) {
await tx.column_labels.upsert({
where: {
@@ -482,7 +601,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
},
update: {
column_label: column.label || column.name,
web_type: column.webType,
input_type: this.convertWebTypeToInputType(column.webType || "text"),
detail_settings: JSON.stringify(column.detailSettings || {}),
description: column.description,
display_order: column.order || 0,
@@ -493,7 +612,7 @@ CREATE TABLE "${tableName}" (${baseColumns},
table_name: tableName,
column_name: column.name,
column_label: column.label || column.name,
web_type: column.webType,
input_type: this.convertWebTypeToInputType(column.webType || "text"),
detail_settings: JSON.stringify(column.detailSettings || {}),
description: column.description,
display_order: column.order || 0,
@@ -505,6 +624,47 @@ CREATE TABLE "${tableName}" (${baseColumns},
}
}
/**
* 웹 타입을 입력 타입으로 변환
*/
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";
}
/**
* 권한 검증 (슈퍼관리자 확인)
*/

View File

@@ -256,11 +256,11 @@ export class DDLSafetyValidator {
if (column.length !== undefined) {
if (
!["text", "code", "email", "tel", "select", "radio"].includes(
column.webType
column.webType || "text"
)
) {
warnings.push(
`${prefix}${column.webType} 타입에서는 길이 설정이 무시됩니다.`
`${prefix}${column.webType || "text"} 타입에서는 길이 설정이 무시됩니다.`
);
} else if (column.length <= 0 || column.length > 65535) {
errors.push(`${prefix}길이는 1 이상 65535 이하여야 합니다.`);

View File

@@ -0,0 +1,282 @@
/**
* 입력 타입 처리 서비스
* VARCHAR 통일 방식에서 입력 타입별 형변환 및 검증 처리
*/
import { InputType } from "../types/input-types";
import { logger } from "../utils/logger";
export interface ValidationResult {
isValid: boolean;
message?: string;
convertedValue?: string;
}
export class InputTypeService {
/**
* 데이터 저장 전 형변환 (화면 입력값 → DB 저장값)
* 모든 값을 VARCHAR(500)에 저장하기 위해 문자열로 변환
*/
static convertForStorage(value: any, inputType: InputType): string {
if (value === null || value === undefined) {
return "";
}
try {
switch (inputType) {
case "text":
case "select":
case "radio":
return String(value).trim();
case "number":
if (value === "" || value === null || value === undefined) {
return "0";
}
const num = parseFloat(String(value));
return isNaN(num) ? "0" : String(num);
case "date":
if (!value || value === "") {
return "";
}
const date = new Date(value);
if (isNaN(date.getTime())) {
logger.warn(`Invalid date value: ${value}`);
return "";
}
return date.toISOString().split("T")[0]; // YYYY-MM-DD 형식
case "checkbox":
// 다양한 형태의 true 값을 "Y"로, 나머지는 "N"으로 변환
const truthyValues = ["true", "1", "Y", "yes", "on", true, 1];
return truthyValues.includes(value) ? "Y" : "N";
case "code":
case "entity":
return String(value || "").trim();
default:
return String(value);
}
} catch (error) {
logger.error(`Error converting value for storage: ${error}`, {
value,
inputType,
});
return String(value || "");
}
}
/**
* 화면 표시용 형변환 (DB 저장값 → 화면 표시값)
* VARCHAR에서 읽어온 문자열을 적절한 타입으로 변환
*/
static convertForDisplay(value: string, inputType: InputType): any {
if (!value && value !== "0") {
// 빈 값 처리
switch (inputType) {
case "number":
return 0;
case "checkbox":
return false;
default:
return "";
}
}
try {
switch (inputType) {
case "text":
case "select":
case "radio":
case "code":
case "entity":
return value;
case "number":
const num = parseFloat(value);
return isNaN(num) ? 0 : num;
case "date":
// YYYY-MM-DD 형식 그대로 반환 (HTML date input 호환)
return value;
case "checkbox":
return value === "Y" || value === "true" || value === "1";
default:
return value;
}
} catch (error) {
logger.error(`Error converting value for display: ${error}`, {
value,
inputType,
});
return value;
}
}
/**
* 입력값 검증
* 저장 전에 값이 해당 입력 타입에 적합한지 검증
*/
static validate(value: any, inputType: InputType): ValidationResult {
// 빈 값은 일반적으로 허용 (필수 여부는 별도 검증)
if (!value && value !== 0 && value !== false) {
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
}
try {
switch (inputType) {
case "text":
case "select":
case "radio":
case "code":
case "entity":
const strValue = String(value).trim();
if (strValue.length > 500) {
return {
isValid: false,
message: "입력값이 너무 깁니다. (최대 500자)",
};
}
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
case "number":
const num = parseFloat(String(value));
if (isNaN(num)) {
return {
isValid: false,
message: "숫자 형식이 올바르지 않습니다.",
};
}
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
case "date":
if (!value) {
return { isValid: true, convertedValue: "" };
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return {
isValid: false,
message: "날짜 형식이 올바르지 않습니다.",
};
}
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
case "checkbox":
// 체크박스는 모든 값을 허용 (Y/N으로 변환)
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
default:
return {
isValid: true,
convertedValue: this.convertForStorage(value, inputType),
};
}
} catch (error) {
logger.error(`Error validating value: ${error}`, { value, inputType });
return {
isValid: false,
message: "값 검증 중 오류가 발생했습니다.",
};
}
}
/**
* 배치 데이터 변환 (여러 필드를 한번에 처리)
*/
static convertBatchForStorage(
data: Record<string, any>,
columnTypes: Record<string, InputType>
): Record<string, string> {
const converted: Record<string, string> = {};
for (const [columnName, value] of Object.entries(data)) {
const inputType = columnTypes[columnName];
if (inputType) {
converted[columnName] = this.convertForStorage(value, inputType);
} else {
// 입력 타입이 정의되지 않은 경우 기본적으로 text로 처리
converted[columnName] = this.convertForStorage(value, "text");
}
}
return converted;
}
/**
* 배치 데이터 표시용 변환
*/
static convertBatchForDisplay(
data: Record<string, string>,
columnTypes: Record<string, InputType>
): Record<string, any> {
const converted: Record<string, any> = {};
for (const [columnName, value] of Object.entries(data)) {
const inputType = columnTypes[columnName];
if (inputType) {
converted[columnName] = this.convertForDisplay(value, inputType);
} else {
// 입력 타입이 정의되지 않은 경우 문자열 그대로 반환
converted[columnName] = value;
}
}
return converted;
}
/**
* 배치 데이터 검증
*/
static validateBatch(
data: Record<string, any>,
columnTypes: Record<string, InputType>
): {
isValid: boolean;
errors: Record<string, string>;
convertedData: Record<string, string>;
} {
const errors: Record<string, string> = {};
const convertedData: Record<string, string> = {};
for (const [columnName, value] of Object.entries(data)) {
const inputType = columnTypes[columnName];
if (inputType) {
const result = this.validate(value, inputType);
if (!result.isValid) {
errors[columnName] = result.message || "검증 실패";
} else {
convertedData[columnName] = result.convertedValue || "";
}
} else {
// 입력 타입이 정의되지 않은 경우 기본적으로 text로 처리
convertedData[columnName] = this.convertForStorage(value, "text");
}
}
return {
isValid: Object.keys(errors).length === 0,
errors,
convertedData,
};
}
}

View File

@@ -329,7 +329,7 @@ export class TableManagementService {
},
update: {
column_label: settings.columnLabel,
web_type: settings.webType,
input_type: settings.inputType,
detail_settings: settings.detailSettings,
code_category: settings.codeCategory,
code_value: settings.codeValue,
@@ -345,7 +345,7 @@ export class TableManagementService {
table_name: tableName,
column_name: columnName,
column_label: settings.columnLabel,
web_type: settings.webType,
input_type: settings.inputType,
detail_settings: settings.detailSettings,
code_category: settings.codeCategory,
code_value: settings.codeValue,
@@ -626,7 +626,123 @@ export class TableManagementService {
}
/**
* 웹 타입별 기본 상세 설정 생성
* 컬럼 입력 타입 설정 (새로운 시스템)
*/
async updateColumnInputType(
tableName: string,
columnName: string,
inputType: string,
detailSettings?: Record<string, any>
): Promise<void> {
try {
logger.info(
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType}`
);
// 입력 타입별 기본 상세 설정 생성
const defaultDetailSettings =
this.generateDefaultInputTypeSettings(inputType);
// 사용자 정의 설정과 기본 설정 병합
const finalDetailSettings = {
...defaultDetailSettings,
...detailSettings,
};
// table_type_columns 테이블에서 업데이트
await prisma.$executeRaw`
INSERT INTO table_type_columns (
table_name, column_name, input_type, detail_settings,
is_nullable, display_order, created_date, updated_date
) VALUES (
${tableName}, ${columnName}, ${inputType}, ${JSON.stringify(finalDetailSettings)},
'Y', 0, now(), now()
)
ON CONFLICT (table_name, column_name)
DO UPDATE SET
input_type = ${inputType},
detail_settings = ${JSON.stringify(finalDetailSettings)},
updated_date = now();
`;
logger.info(
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}`
);
} catch (error) {
logger.error(
`컬럼 입력 타입 설정 실패: ${tableName}.${columnName}`,
error
);
throw new Error(
`컬럼 입력 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* 입력 타입별 기본 상세 설정 생성
*/
private generateDefaultInputTypeSettings(
inputType: string
): Record<string, any> {
switch (inputType) {
case "text":
return {
maxLength: 500,
placeholder: "텍스트를 입력하세요",
};
case "number":
return {
min: 0,
step: 1,
placeholder: "숫자를 입력하세요",
};
case "date":
return {
format: "YYYY-MM-DD",
placeholder: "날짜를 선택하세요",
};
case "code":
return {
placeholder: "코드를 선택하세요",
searchable: true,
};
case "entity":
return {
placeholder: "항목을 선택하세요",
searchable: true,
};
case "select":
return {
placeholder: "선택하세요",
searchable: false,
};
case "checkbox":
return {
defaultChecked: false,
trueValue: "Y",
falseValue: "N",
};
case "radio":
return {
inline: false,
};
default:
return {};
}
}
/**
* 웹 타입별 기본 상세 설정 생성 (레거시 지원)
* @deprecated generateDefaultInputTypeSettings 사용 권장
*/
private generateDefaultDetailSettings(webType: string): Record<string, any> {
switch (webType) {
@@ -2363,22 +2479,21 @@ export class TableManagementService {
}
/**
* 컬럼 타입 정보 조회 (화면관리 연동용)
* 컬럼 입력타입 정보 조회 (화면관리 연동용)
*/
async getColumnWebTypes(tableName: string): Promise<ColumnTypeInfo[]> {
async getColumnInputTypes(tableName: string): Promise<ColumnTypeInfo[]> {
try {
logger.info(`컬럼 타입 정보 조회: ${tableName}`);
logger.info(`컬럼 입력타입 정보 조회: ${tableName}`);
// table_type_columns에서 타입 정보 조회
const rawWebTypes = await prisma.$queryRaw<any[]>`
// table_type_columns에서 입력타입 정보 조회
const rawInputTypes = await prisma.$queryRaw<any[]>`
SELECT
ttc.column_name as "columnName",
ttc.column_name as "displayName",
COALESCE(ttc.web_type, 'text') as "webType",
COALESCE(ttc.input_type, 'text') as "inputType",
COALESCE(ttc.detail_settings, '{}') as "detailSettings",
ttc.is_nullable as "isNullable",
ic.data_type as "dataType",
ic.udt_name as "dbType"
ic.data_type as "dataType"
FROM table_type_columns ttc
LEFT JOIN information_schema.columns ic
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
@@ -2386,14 +2501,12 @@ export class TableManagementService {
ORDER BY ttc.display_order, ttc.column_name
`;
const webTypes: ColumnTypeInfo[] = rawWebTypes.map((col) => ({
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({
tableName: tableName,
columnName: col.columnName,
displayName: col.displayName,
dataType: col.dataType || "text",
dbType: col.dbType || "text",
webType: col.webType,
inputType: "direct",
dataType: col.dataType || "varchar",
inputType: col.inputType,
detailSettings: col.detailSettings,
description: "", // 필수 필드 추가
isNullable: col.isNullable,
@@ -2403,15 +2516,26 @@ export class TableManagementService {
}));
logger.info(
`컬럼 타입 정보 조회 완료: ${tableName}, ${webTypes.length}개 컬럼`
`컬럼 입력타입 정보 조회 완료: ${tableName}, ${inputTypes.length}개 컬럼`
);
return webTypes;
return inputTypes;
} catch (error) {
logger.error(`컬럼 타입 정보 조회 실패: ${tableName}`, error);
logger.error(`컬럼 입력타입 정보 조회 실패: ${tableName}`, error);
throw error;
}
}
/**
* 레거시 지원: 컬럼 웹타입 정보 조회
* @deprecated getColumnInputTypes 사용 권장
*/
async getColumnWebTypes(tableName: string): Promise<ColumnTypeInfo[]> {
logger.warn(
`레거시 메서드 사용: getColumnWebTypes → getColumnInputTypes 사용 권장`
);
return this.getColumnInputTypes(tableName);
}
/**
* 데이터베이스 연결 상태 확인
*/