776 lines
20 KiB
TypeScript
776 lines
20 KiB
TypeScript
/**
|
|
* 개선된 동적 폼 서비스
|
|
* 타입 안전성과 검증 강화
|
|
*/
|
|
|
|
import { query, queryOne } from "../database/db";
|
|
import {
|
|
WebType,
|
|
DynamicWebType,
|
|
normalizeWebType,
|
|
isValidWebType,
|
|
WEB_TYPE_TO_POSTGRES_CONVERTER,
|
|
WEB_TYPE_VALIDATION_PATTERNS,
|
|
} from "../types/unified-web-types";
|
|
import { DataflowControlService } from "./dataflowControlService";
|
|
|
|
// 테이블 컬럼 정보
|
|
export interface TableColumn {
|
|
column_name: string;
|
|
data_type: string;
|
|
is_nullable: string;
|
|
column_default: any;
|
|
character_maximum_length?: number;
|
|
numeric_precision?: number;
|
|
numeric_scale?: number;
|
|
}
|
|
|
|
// 컬럼 웹타입 정보
|
|
export interface ColumnWebTypeInfo {
|
|
columnName: string;
|
|
webType: WebType;
|
|
isRequired: boolean;
|
|
validationRules?: Record<string, any>;
|
|
defaultValue?: any;
|
|
}
|
|
|
|
// 폼 데이터 검증 결과
|
|
export interface FormValidationResult {
|
|
isValid: boolean;
|
|
errors: FormValidationError[];
|
|
warnings: FormValidationWarning[];
|
|
transformedData: Record<string, any>;
|
|
}
|
|
|
|
export interface FormValidationError {
|
|
field: string;
|
|
code: string;
|
|
message: string;
|
|
value?: any;
|
|
}
|
|
|
|
export interface FormValidationWarning {
|
|
field: string;
|
|
code: string;
|
|
message: string;
|
|
suggestion?: string;
|
|
}
|
|
|
|
// 저장 결과
|
|
export interface FormDataResult {
|
|
success: boolean;
|
|
message: string;
|
|
data?: any;
|
|
affectedRows?: number;
|
|
insertedId?: any;
|
|
validationResult?: FormValidationResult;
|
|
}
|
|
|
|
export class EnhancedDynamicFormService {
|
|
private dataflowControlService = new DataflowControlService();
|
|
private columnCache = new Map<string, TableColumn[]>();
|
|
private webTypeCache = new Map<string, ColumnWebTypeInfo[]>();
|
|
|
|
/**
|
|
* 폼 데이터 저장 (메인 메서드)
|
|
*/
|
|
async saveFormData(
|
|
screenId: number,
|
|
tableName: string,
|
|
data: Record<string, any>
|
|
): Promise<FormDataResult> {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
console.log(`🚀 개선된 폼 저장 시작: ${tableName}`, {
|
|
screenId,
|
|
dataKeys: Object.keys(data),
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
// 1. 테이블 존재 여부 확인
|
|
const tableExists = await this.validateTableExists(tableName);
|
|
if (!tableExists) {
|
|
return {
|
|
success: false,
|
|
message: `테이블 '${tableName}'이 존재하지 않습니다.`,
|
|
};
|
|
}
|
|
|
|
// 2. 스키마 정보 로드
|
|
const [tableColumns, columnWebTypes] = await Promise.all([
|
|
this.getTableColumns(tableName),
|
|
this.getColumnWebTypes(tableName),
|
|
]);
|
|
|
|
// 3. 폼 데이터 검증
|
|
const validationResult = await this.validateFormData(
|
|
data,
|
|
tableColumns,
|
|
columnWebTypes,
|
|
tableName
|
|
);
|
|
|
|
if (!validationResult.isValid) {
|
|
console.error("❌ 폼 데이터 검증 실패:", validationResult.errors);
|
|
return {
|
|
success: false,
|
|
message: this.formatValidationErrors(validationResult.errors),
|
|
validationResult,
|
|
};
|
|
}
|
|
|
|
// 4. 데이터 저장 수행
|
|
const saveResult = await this.performDataSave(
|
|
tableName,
|
|
validationResult.transformedData,
|
|
tableColumns
|
|
);
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`✅ 폼 저장 완료: ${duration}ms`);
|
|
|
|
return {
|
|
success: true,
|
|
message: "데이터가 성공적으로 저장되었습니다.",
|
|
data: saveResult.data,
|
|
affectedRows: saveResult.affectedRows,
|
|
insertedId: saveResult.insertedId,
|
|
validationResult,
|
|
};
|
|
} catch (error: any) {
|
|
console.error("❌ 폼 저장 중 오류:", error);
|
|
|
|
return {
|
|
success: false,
|
|
message: this.formatErrorMessage(error),
|
|
data: { error: error.message, stack: error.stack },
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 존재 여부 확인
|
|
*/
|
|
private async validateTableExists(tableName: string): Promise<boolean> {
|
|
try {
|
|
const result = await query<{ exists: boolean }>(
|
|
`SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_name = $1
|
|
) as exists`,
|
|
[tableName]
|
|
);
|
|
|
|
return result[0]?.exists || false;
|
|
} catch (error) {
|
|
console.error(`❌ 테이블 존재 여부 확인 실패: ${tableName}`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 컬럼 정보 조회 (캐시 포함)
|
|
*/
|
|
private async getTableColumns(tableName: string): Promise<TableColumn[]> {
|
|
// 캐시 확인
|
|
const cached = this.columnCache.get(tableName);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
try {
|
|
const columns = await query<TableColumn>(
|
|
`SELECT
|
|
column_name,
|
|
data_type,
|
|
is_nullable,
|
|
column_default,
|
|
character_maximum_length,
|
|
numeric_precision,
|
|
numeric_scale
|
|
FROM information_schema.columns
|
|
WHERE table_name = $1
|
|
ORDER BY ordinal_position`,
|
|
[tableName]
|
|
);
|
|
|
|
// 캐시 저장 (10분)
|
|
this.columnCache.set(tableName, columns);
|
|
setTimeout(() => this.columnCache.delete(tableName), 10 * 60 * 1000);
|
|
|
|
return columns;
|
|
} catch (error) {
|
|
console.error(`❌ 테이블 컬럼 정보 조회 실패: ${tableName}`, error);
|
|
throw new Error(`테이블 컬럼 정보를 조회할 수 없습니다: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 웹타입 정보 조회
|
|
*/
|
|
private async getColumnWebTypes(
|
|
tableName: string
|
|
): Promise<ColumnWebTypeInfo[]> {
|
|
// 캐시 확인
|
|
const cached = this.webTypeCache.get(tableName);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
try {
|
|
// table_type_columns에서 웹타입 정보 조회
|
|
const webTypeData = await query<{
|
|
column_name: string;
|
|
web_type: string;
|
|
is_nullable: string;
|
|
detail_settings: any;
|
|
}>(
|
|
`SELECT
|
|
column_name,
|
|
web_type,
|
|
is_nullable,
|
|
detail_settings
|
|
FROM table_type_columns
|
|
WHERE table_name = $1`,
|
|
[tableName]
|
|
);
|
|
|
|
const columnWebTypes: ColumnWebTypeInfo[] = webTypeData.map((row) => ({
|
|
columnName: row.column_name,
|
|
webType: normalizeWebType(row.web_type || "text"),
|
|
isRequired: row.is_nullable === "N",
|
|
validationRules: this.parseDetailSettings(row.detail_settings),
|
|
defaultValue: null,
|
|
}));
|
|
|
|
// 캐시 저장 (10분)
|
|
this.webTypeCache.set(tableName, columnWebTypes);
|
|
setTimeout(() => this.webTypeCache.delete(tableName), 10 * 60 * 1000);
|
|
|
|
return columnWebTypes;
|
|
} catch (error) {
|
|
console.error(`❌ 컬럼 웹타입 정보 조회 실패: ${tableName}`, error);
|
|
// 실패 시 빈 배열 반환 (기본 검증만 수행)
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 상세 설정 파싱
|
|
*/
|
|
private parseDetailSettings(
|
|
detailSettings: string | null
|
|
): Record<string, any> {
|
|
if (!detailSettings) return {};
|
|
|
|
try {
|
|
return JSON.parse(detailSettings);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 폼 데이터 검증
|
|
*/
|
|
private async validateFormData(
|
|
data: Record<string, any>,
|
|
tableColumns: TableColumn[],
|
|
columnWebTypes: ColumnWebTypeInfo[],
|
|
tableName: string
|
|
): Promise<FormValidationResult> {
|
|
const errors: FormValidationError[] = [];
|
|
const warnings: FormValidationWarning[] = [];
|
|
const transformedData: Record<string, any> = {};
|
|
|
|
const columnMap = new Map(
|
|
tableColumns.map((col) => [col.column_name, col])
|
|
);
|
|
const webTypeMap = new Map(columnWebTypes.map((wt) => [wt.columnName, wt]));
|
|
|
|
console.log(`📋 폼 데이터 검증 시작: ${tableName}`, {
|
|
inputFields: Object.keys(data).length,
|
|
tableColumns: tableColumns.length,
|
|
webTypeColumns: columnWebTypes.length,
|
|
});
|
|
|
|
// 입력된 각 필드 검증
|
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
const column = columnMap.get(fieldName);
|
|
const webTypeInfo = webTypeMap.get(fieldName);
|
|
|
|
// 1. 컬럼 존재 여부 확인
|
|
if (!column) {
|
|
errors.push({
|
|
field: fieldName,
|
|
code: "COLUMN_NOT_EXISTS",
|
|
message: `테이블 '${tableName}'에 '${fieldName}' 컬럼이 존재하지 않습니다.`,
|
|
value,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// 2. 필수값 검증
|
|
if (webTypeInfo?.isRequired && this.isEmptyValue(value)) {
|
|
errors.push({
|
|
field: fieldName,
|
|
code: "REQUIRED_FIELD",
|
|
message: `'${fieldName}'은(는) 필수 입력 항목입니다.`,
|
|
value,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// 3. 웹타입별 검증 및 변환
|
|
if (webTypeInfo?.webType) {
|
|
const validationResult = this.validateFieldByWebType(
|
|
fieldName,
|
|
value,
|
|
webTypeInfo.webType,
|
|
webTypeInfo.validationRules
|
|
);
|
|
|
|
if (!validationResult.isValid) {
|
|
errors.push(validationResult.error!);
|
|
continue;
|
|
}
|
|
|
|
transformedData[fieldName] = validationResult.transformedValue;
|
|
} else {
|
|
// 웹타입 정보가 없는 경우 DB 타입 기반 변환
|
|
transformedData[fieldName] = this.convertValueForPostgreSQL(
|
|
value,
|
|
column.data_type
|
|
);
|
|
}
|
|
|
|
// 4. DB 제약조건 검증
|
|
const constraintValidation = this.validateDatabaseConstraints(
|
|
fieldName,
|
|
transformedData[fieldName],
|
|
column
|
|
);
|
|
|
|
if (!constraintValidation.isValid) {
|
|
errors.push(constraintValidation.error!);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 필수 컬럼 누락 확인
|
|
const requiredColumns = columnWebTypes.filter((wt) => wt.isRequired);
|
|
for (const requiredCol of requiredColumns) {
|
|
if (
|
|
!(requiredCol.columnName in data) ||
|
|
this.isEmptyValue(data[requiredCol.columnName])
|
|
) {
|
|
if (!errors.some((e) => e.field === requiredCol.columnName)) {
|
|
errors.push({
|
|
field: requiredCol.columnName,
|
|
code: "MISSING_REQUIRED_FIELD",
|
|
message: `필수 입력 항목 '${requiredCol.columnName}'이 누락되었습니다.`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`📋 폼 데이터 검증 완료:`, {
|
|
errors: errors.length,
|
|
warnings: warnings.length,
|
|
transformedFields: Object.keys(transformedData).length,
|
|
});
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
transformedData,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 웹타입별 필드 검증
|
|
*/
|
|
private validateFieldByWebType(
|
|
fieldName: string,
|
|
value: any,
|
|
webType: WebType,
|
|
validationRules?: Record<string, any>
|
|
): { isValid: boolean; error?: FormValidationError; transformedValue?: any } {
|
|
// 빈 값 처리
|
|
if (this.isEmptyValue(value)) {
|
|
return { isValid: true, transformedValue: null };
|
|
}
|
|
|
|
// 웹타입 유효성 확인
|
|
if (!isValidWebType(webType)) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "INVALID_WEB_TYPE",
|
|
message: `'${fieldName}'의 웹타입 '${webType}'이 올바르지 않습니다.`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
|
|
// 패턴 검증
|
|
const pattern = WEB_TYPE_VALIDATION_PATTERNS[webType];
|
|
if (pattern && !pattern.test(String(value))) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "INVALID_FORMAT",
|
|
message: `'${fieldName}'의 형식이 올바르지 않습니다.`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
|
|
// 값 변환
|
|
try {
|
|
const converter = WEB_TYPE_TO_POSTGRES_CONVERTER[webType];
|
|
const transformedValue = converter ? converter(value) : value;
|
|
|
|
return { isValid: true, transformedValue };
|
|
} catch (error) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "CONVERSION_ERROR",
|
|
message: `'${fieldName}' 값 변환 중 오류가 발생했습니다: ${error}`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 데이터베이스 제약조건 검증
|
|
*/
|
|
private validateDatabaseConstraints(
|
|
fieldName: string,
|
|
value: any,
|
|
column: TableColumn
|
|
): { isValid: boolean; error?: FormValidationError } {
|
|
// NULL 제약조건
|
|
if (
|
|
column.is_nullable === "NO" &&
|
|
(value === null || value === undefined)
|
|
) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "NOT_NULL_VIOLATION",
|
|
message: `'${fieldName}'에는 NULL 값을 입력할 수 없습니다.`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
|
|
// 문자열 길이 제약조건
|
|
if (column.character_maximum_length && typeof value === "string") {
|
|
if (value.length > column.character_maximum_length) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "STRING_TOO_LONG",
|
|
message: `'${fieldName}'의 길이는 최대 ${column.character_maximum_length}자까지 입력할 수 있습니다.`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// 숫자 정밀도 검증
|
|
if (column.numeric_precision && typeof value === "number") {
|
|
const totalDigits = Math.abs(value).toString().replace(".", "").length;
|
|
if (totalDigits > column.numeric_precision) {
|
|
return {
|
|
isValid: false,
|
|
error: {
|
|
field: fieldName,
|
|
code: "NUMERIC_OVERFLOW",
|
|
message: `'${fieldName}'의 숫자 자릿수가 허용 범위를 초과했습니다.`,
|
|
value,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
return { isValid: true };
|
|
}
|
|
|
|
/**
|
|
* 빈 값 확인
|
|
*/
|
|
private isEmptyValue(value: any): boolean {
|
|
return (
|
|
value === null ||
|
|
value === undefined ||
|
|
value === "" ||
|
|
(Array.isArray(value) && value.length === 0)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 데이터 저장 수행
|
|
*/
|
|
private async performDataSave(
|
|
tableName: string,
|
|
data: Record<string, any>,
|
|
tableColumns: TableColumn[]
|
|
): Promise<{ data?: any; affectedRows: number; insertedId?: any }> {
|
|
try {
|
|
// Primary Key 확인
|
|
const primaryKeys = await this.getPrimaryKeys(tableName);
|
|
const hasExistingRecord =
|
|
primaryKeys.length > 0 &&
|
|
primaryKeys.every((pk) => data[pk] !== undefined && data[pk] !== null);
|
|
|
|
if (hasExistingRecord) {
|
|
// UPDATE 수행
|
|
return await this.performUpdate(tableName, data, primaryKeys);
|
|
} else {
|
|
// INSERT 수행
|
|
return await this.performInsert(tableName, data);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 데이터 저장 실패: ${tableName}`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Primary Key 조회
|
|
*/
|
|
private async getPrimaryKeys(tableName: string): Promise<string[]> {
|
|
try {
|
|
const result = await query<{ column_name: string }>(
|
|
`SELECT column_name
|
|
FROM information_schema.key_column_usage
|
|
WHERE table_name = $1
|
|
AND constraint_name LIKE '%_pkey'`,
|
|
[tableName]
|
|
);
|
|
|
|
return result.map((row) => row.column_name);
|
|
} catch (error) {
|
|
console.error(`❌ Primary Key 조회 실패: ${tableName}`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* INSERT 수행
|
|
*/
|
|
private async performInsert(
|
|
tableName: string,
|
|
data: Record<string, any>
|
|
): Promise<{ data?: any; affectedRows: number; insertedId?: any }> {
|
|
const columns = Object.keys(data);
|
|
const values = Object.values(data);
|
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
|
|
|
const insertQuery = `
|
|
INSERT INTO ${tableName} (${columns.join(", ")})
|
|
VALUES (${placeholders})
|
|
RETURNING *
|
|
`;
|
|
|
|
console.log(`📝 INSERT 쿼리 실행: ${tableName}`, {
|
|
columns: columns.length,
|
|
query: insertQuery.replace(/\n\s+/g, " "),
|
|
});
|
|
|
|
const result = await query<any>(insertQuery, values);
|
|
|
|
return {
|
|
data: result[0],
|
|
affectedRows: result.length,
|
|
insertedId: result[0]?.id || result[0],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* UPDATE 수행
|
|
*/
|
|
private async performUpdate(
|
|
tableName: string,
|
|
data: Record<string, any>,
|
|
primaryKeys: string[]
|
|
): Promise<{ data?: any; affectedRows: number; insertedId?: any }> {
|
|
const updateColumns = Object.keys(data).filter(
|
|
(col) => !primaryKeys.includes(col)
|
|
);
|
|
const whereColumns = primaryKeys.filter((pk) => data[pk] !== undefined);
|
|
|
|
if (updateColumns.length === 0) {
|
|
throw new Error("업데이트할 컬럼이 없습니다.");
|
|
}
|
|
|
|
const setClause = updateColumns
|
|
.map((col, index) => `${col} = $${index + 1}`)
|
|
.join(", ");
|
|
|
|
const whereClause = whereColumns
|
|
.map((col, index) => `${col} = $${updateColumns.length + index + 1}`)
|
|
.join(" AND ");
|
|
|
|
const updateValues = [
|
|
...updateColumns.map((col) => data[col]),
|
|
...whereColumns.map((col) => data[col]),
|
|
];
|
|
|
|
const updateQuery = `
|
|
UPDATE ${tableName}
|
|
SET ${setClause}
|
|
WHERE ${whereClause}
|
|
RETURNING *
|
|
`;
|
|
|
|
console.log(`📝 UPDATE 쿼리 실행: ${tableName}`, {
|
|
updateColumns: updateColumns.length,
|
|
whereColumns: whereColumns.length,
|
|
query: updateQuery.replace(/\n\s+/g, " "),
|
|
});
|
|
|
|
const result = await query<any>(updateQuery, updateValues);
|
|
|
|
return {
|
|
data: result[0],
|
|
affectedRows: result.length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* PostgreSQL 타입 변환 (레거시 지원)
|
|
*/
|
|
private convertValueForPostgreSQL(value: any, dataType: string): any {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
const lowerDataType = dataType.toLowerCase();
|
|
|
|
// 숫자 타입 처리
|
|
if (
|
|
lowerDataType.includes("integer") ||
|
|
lowerDataType.includes("bigint") ||
|
|
lowerDataType.includes("serial")
|
|
) {
|
|
return parseInt(value) || null;
|
|
}
|
|
|
|
if (
|
|
lowerDataType.includes("numeric") ||
|
|
lowerDataType.includes("decimal") ||
|
|
lowerDataType.includes("real") ||
|
|
lowerDataType.includes("double")
|
|
) {
|
|
return parseFloat(value) || null;
|
|
}
|
|
|
|
// 불린 타입 처리
|
|
if (lowerDataType.includes("boolean")) {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "string") {
|
|
return value.toLowerCase() === "true" || value === "1";
|
|
}
|
|
return Boolean(value);
|
|
}
|
|
|
|
// 날짜/시간 타입 처리
|
|
if (
|
|
lowerDataType.includes("timestamp") ||
|
|
lowerDataType.includes("datetime")
|
|
) {
|
|
const date = new Date(value);
|
|
return isNaN(date.getTime()) ? null : date.toISOString();
|
|
}
|
|
|
|
if (lowerDataType.includes("date")) {
|
|
const date = new Date(value);
|
|
return isNaN(date.getTime()) ? null : date.toISOString().split("T")[0];
|
|
}
|
|
|
|
// JSON 타입 처리
|
|
if (lowerDataType.includes("json")) {
|
|
if (typeof value === "string") {
|
|
try {
|
|
JSON.parse(value);
|
|
return value;
|
|
} catch {
|
|
return JSON.stringify(value);
|
|
}
|
|
}
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
/**
|
|
* 오류 메시지 포맷팅
|
|
*/
|
|
private formatValidationErrors(errors: FormValidationError[]): string {
|
|
if (errors.length === 0) return "알 수 없는 오류가 발생했습니다.";
|
|
if (errors.length === 1) return errors[0].message;
|
|
|
|
return `다음 오류들을 수정해주세요:\n• ${errors.map((e) => e.message).join("\n• ")}`;
|
|
}
|
|
|
|
/**
|
|
* 시스템 오류 메시지 포맷팅
|
|
*/
|
|
private formatErrorMessage(error: any): string {
|
|
if (error.code === "23505") {
|
|
return "중복된 데이터가 이미 존재합니다.";
|
|
}
|
|
|
|
if (error.code === "23503") {
|
|
return "참조 무결성 제약조건을 위반했습니다.";
|
|
}
|
|
|
|
if (error.code === "23502") {
|
|
return "필수 입력 항목이 누락되었습니다.";
|
|
}
|
|
|
|
if (
|
|
error.message?.includes("relation") &&
|
|
error.message?.includes("does not exist")
|
|
) {
|
|
return "지정된 테이블이 존재하지 않습니다.";
|
|
}
|
|
|
|
return `저장 중 오류가 발생했습니다: ${error.message || error}`;
|
|
}
|
|
|
|
/**
|
|
* 캐시 클리어
|
|
*/
|
|
public clearCache(): void {
|
|
this.columnCache.clear();
|
|
this.webTypeCache.clear();
|
|
console.log("🧹 동적 폼 서비스 캐시가 클리어되었습니다.");
|
|
}
|
|
|
|
/**
|
|
* 테이블별 캐시 클리어
|
|
*/
|
|
public clearTableCache(tableName: string): void {
|
|
this.columnCache.delete(tableName);
|
|
this.webTypeCache.delete(tableName);
|
|
console.log(`🧹 테이블 '${tableName}' 캐시가 클리어되었습니다.`);
|
|
}
|
|
}
|
|
|
|
// 싱글톤 인스턴스
|
|
export const enhancedDynamicFormService = new EnhancedDynamicFormService();
|