타입 관리 개선 및 화면 비율조정 중간커밋
This commit is contained in:
786
backend-node/src/services/enhancedDynamicFormService.ts
Normal file
786
backend-node/src/services/enhancedDynamicFormService.ts
Normal file
@@ -0,0 +1,786 @@
|
||||
/**
|
||||
* 개선된 동적 폼 서비스
|
||||
* 타입 안전성과 검증 강화
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
WebType,
|
||||
DynamicWebType,
|
||||
normalizeWebType,
|
||||
isValidWebType,
|
||||
WEB_TYPE_TO_POSTGRES_CONVERTER,
|
||||
WEB_TYPE_VALIDATION_PATTERNS,
|
||||
} from "../types/unified-web-types";
|
||||
import { DataflowControlService } from "./dataflowControlService";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 테이블 컬럼 정보
|
||||
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 prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = $1
|
||||
) as exists
|
||||
`,
|
||||
tableName
|
||||
);
|
||||
|
||||
return (result as any)[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 prisma.$queryRawUnsafe(
|
||||
`
|
||||
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
|
||||
)) as TableColumn[];
|
||||
|
||||
// 캐시 저장 (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 prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT
|
||||
column_name,
|
||||
web_type,
|
||||
is_nullable,
|
||||
detail_settings
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
`,
|
||||
tableName
|
||||
)) as any[];
|
||||
|
||||
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 prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = $1
|
||||
AND constraint_name LIKE '%_pkey'
|
||||
`,
|
||||
tableName
|
||||
)) as any[];
|
||||
|
||||
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 prisma.$queryRawUnsafe(
|
||||
insertQuery,
|
||||
...values
|
||||
)) as any[];
|
||||
|
||||
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 prisma.$queryRawUnsafe(
|
||||
updateQuery,
|
||||
...updateValues
|
||||
)) as any[];
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user