Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into mhkim-node

This commit is contained in:
kmh
2026-03-10 16:20:57 +09:00
63 changed files with 6657 additions and 771 deletions

View File

@@ -3563,19 +3563,21 @@ export async function getTableSchema(
logger.info("테이블 스키마 조회", { tableName, companyCode });
// information_schema와 table_type_columns를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기
// 회사별 라벨 우선, 없으면 공통(*) 라벨 사용
// information_schema와 table_type_columns를 JOIN하여 컬럼 정보 + 회사별 제약조건 함께 가져오기
// 회사별 설정 우선, 없으면 공통(*) 설정 사용
const schemaQuery = `
SELECT
ic.column_name,
ic.data_type,
ic.is_nullable,
ic.is_nullable AS db_is_nullable,
ic.column_default,
ic.character_maximum_length,
ic.numeric_precision,
ic.numeric_scale,
COALESCE(ttc_company.column_label, ttc_common.column_label) AS column_label,
COALESCE(ttc_company.display_order, ttc_common.display_order) AS display_order
COALESCE(ttc_company.display_order, ttc_common.display_order) AS display_order,
COALESCE(ttc_company.is_nullable, ttc_common.is_nullable) AS ttc_is_nullable,
COALESCE(ttc_company.is_unique, ttc_common.is_unique) AS ttc_is_unique
FROM information_schema.columns ic
LEFT JOIN table_type_columns ttc_common
ON ttc_common.table_name = ic.table_name
@@ -3600,17 +3602,28 @@ export async function getTableSchema(
return;
}
// 컬럼 정보를 간단한 형태로 변환 (라벨 정보 포함)
const columnList = columns.map((col: any) => ({
name: col.column_name,
label: col.column_label || col.column_name, // 라벨이 없으면 컬럼명 사용
type: col.data_type,
nullable: col.is_nullable === "YES",
default: col.column_default,
maxLength: col.character_maximum_length,
precision: col.numeric_precision,
scale: col.numeric_scale,
}));
// 컬럼 정보를 간단한 형태로 변환 (회사별 제약조건 반영)
const columnList = columns.map((col: any) => {
// DB level nullable + 회사별 table_type_columns 제약조건 통합
// table_type_columns에서 is_nullable = 'N'이면 필수 (DB가 nullable이어도)
const dbNullable = col.db_is_nullable === "YES";
const ttcNotNull = col.ttc_is_nullable === "N";
const effectiveNullable = ttcNotNull ? false : dbNullable;
const ttcUnique = col.ttc_is_unique === "Y";
return {
name: col.column_name,
label: col.column_label || col.column_name,
type: col.data_type,
nullable: effectiveNullable,
unique: ttcUnique,
default: col.column_default,
maxLength: col.character_maximum_length,
precision: col.numeric_precision,
scale: col.numeric_scale,
};
});
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);

View File

@@ -8,6 +8,7 @@ import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query } from "../database/db";
import logger from "../utils/logger";
import { TableManagementService } from "../services/tableManagementService";
/**
* 데이터 액션 실행
@@ -81,6 +82,19 @@ async function executeMainDatabaseAction(
company_code: companyCode,
};
// UNIQUE 제약조건 검증 (INSERT/UPDATE/UPSERT 전)
if (["insert", "update", "upsert"].includes(actionType.toLowerCase())) {
const tms = new TableManagementService();
const uniqueViolations = await tms.validateUniqueConstraints(
tableName,
dataWithCompany,
companyCode
);
if (uniqueViolations.length > 0) {
throw new Error(`중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`);
}
}
switch (actionType.toLowerCase()) {
case "insert":
return await executeInsert(tableName, dataWithCompany);

View File

@@ -1,6 +1,7 @@
import { Response } from "express";
import { dynamicFormService } from "../services/dynamicFormService";
import { enhancedDynamicFormService } from "../services/enhancedDynamicFormService";
import { TableManagementService } from "../services/tableManagementService";
import { AuthenticatedRequest } from "../types/auth";
import { formatPgError } from "../utils/pgErrorUtil";
@@ -48,6 +49,21 @@ export const saveFormData = async (
formDataWithMeta.company_code = companyCode;
}
// UNIQUE 제약조건 검증 (INSERT 전)
const tms = new TableManagementService();
const uniqueViolations = await tms.validateUniqueConstraints(
tableName,
formDataWithMeta,
companyCode || "*"
);
if (uniqueViolations.length > 0) {
res.status(400).json({
success: false,
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
});
return;
}
// 클라이언트 IP 주소 추출
const ipAddress =
req.ip ||
@@ -112,6 +128,21 @@ export const saveFormDataEnhanced = async (
formDataWithMeta.company_code = companyCode;
}
// UNIQUE 제약조건 검증 (INSERT 전)
const tmsEnhanced = new TableManagementService();
const uniqueViolations = await tmsEnhanced.validateUniqueConstraints(
tableName,
formDataWithMeta,
companyCode || "*"
);
if (uniqueViolations.length > 0) {
res.status(400).json({
success: false,
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
});
return;
}
// 개선된 서비스 사용
const result = await enhancedDynamicFormService.saveFormData(
screenId,
@@ -153,12 +184,28 @@ export const updateFormData = async (
const formDataWithMeta = {
...data,
updated_by: userId,
writer: data.writer || userId, // ✅ writer가 없으면 userId로 설정
writer: data.writer || userId,
updated_at: new Date(),
};
// UNIQUE 제약조건 검증 (UPDATE 시 자기 자신 제외)
const tmsUpdate = new TableManagementService();
const uniqueViolations = await tmsUpdate.validateUniqueConstraints(
tableName,
formDataWithMeta,
companyCode || "*",
id
);
if (uniqueViolations.length > 0) {
res.status(400).json({
success: false,
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
});
return;
}
const result = await dynamicFormService.updateFormData(
id, // parseInt 제거 - 문자열 ID 지원
id,
tableName,
formDataWithMeta
);
@@ -209,11 +256,27 @@ export const updateFormDataPartial = async (
const newDataWithMeta = {
...newData,
updated_by: userId,
writer: newData.writer || userId, // ✅ writer가 없으면 userId로 설정
writer: newData.writer || userId,
};
// UNIQUE 제약조건 검증 (부분 UPDATE 시 자기 자신 제외)
const tmsPartial = new TableManagementService();
const uniqueViolations = await tmsPartial.validateUniqueConstraints(
tableName,
newDataWithMeta,
companyCode || "*",
id
);
if (uniqueViolations.length > 0) {
res.status(400).json({
success: false,
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
});
return;
}
const result = await dynamicFormService.updateFormDataPartial(
id, // 🔧 parseInt 제거 - UUID 문자열도 지원
id,
tableName,
originalData,
newDataWithMeta

View File

@@ -2087,6 +2087,23 @@ export async function multiTableSave(
return;
}
// UNIQUE 제약조건 검증 (트랜잭션 전에)
const tmsMulti = new TableManagementService();
const uniqueViolations = await tmsMulti.validateUniqueConstraints(
mainTable.tableName,
mainData,
companyCode,
isUpdate ? mainData[mainTable.primaryKeyColumn] : undefined
);
if (uniqueViolations.length > 0) {
client.release();
res.status(400).json({
success: false,
message: `중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`,
});
return;
}
await client.query("BEGIN");
// 1. 메인 테이블 저장