jskim-node #394
|
|
@ -921,6 +921,24 @@ export async function addTableData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 회사별 NOT NULL 소프트 제약조건 검증
|
||||||
|
const notNullViolations = await tableManagementService.validateNotNullConstraints(
|
||||||
|
tableName,
|
||||||
|
data,
|
||||||
|
companyCode || "*"
|
||||||
|
);
|
||||||
|
if (notNullViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `필수 항목이 비어있습니다: ${notNullViolations.join(", ")}`,
|
||||||
|
error: {
|
||||||
|
code: "NOT_NULL_VIOLATION",
|
||||||
|
details: notNullViolations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 데이터 추가
|
// 데이터 추가
|
||||||
await tableManagementService.addTableData(tableName, data);
|
await tableManagementService.addTableData(tableName, data);
|
||||||
|
|
||||||
|
|
@ -1003,6 +1021,25 @@ export async function editTableData(
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableManagementService = new TableManagementService();
|
const tableManagementService = new TableManagementService();
|
||||||
|
const companyCode = req.user?.companyCode || "*";
|
||||||
|
|
||||||
|
// 회사별 NOT NULL 소프트 제약조건 검증 (수정 데이터 대상)
|
||||||
|
const notNullViolations = await tableManagementService.validateNotNullConstraints(
|
||||||
|
tableName,
|
||||||
|
updatedData,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
if (notNullViolations.length > 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `필수 항목이 비어있습니다: ${notNullViolations.join(", ")}`,
|
||||||
|
error: {
|
||||||
|
code: "NOT_NULL_VIOLATION",
|
||||||
|
details: notNullViolations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 데이터 수정
|
// 데이터 수정
|
||||||
await tableManagementService.editTableData(
|
await tableManagementService.editTableData(
|
||||||
|
|
@ -2652,8 +2689,11 @@ export async function toggleTableIndex(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOT NULL 토글
|
* NOT NULL 토글 (회사별 소프트 제약조건)
|
||||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/nullable
|
* PUT /api/table-management/tables/:tableName/columns/:columnName/nullable
|
||||||
|
*
|
||||||
|
* DB 레벨 ALTER TABLE 대신 table_type_columns.is_nullable을 회사별로 관리한다.
|
||||||
|
* 멀티테넌시 환경에서 회사 A는 NOT NULL, 회사 B는 NULL 허용이 가능하다.
|
||||||
*/
|
*/
|
||||||
export async function toggleColumnNullable(
|
export async function toggleColumnNullable(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
|
|
@ -2662,6 +2702,7 @@ export async function toggleColumnNullable(
|
||||||
try {
|
try {
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
const { nullable } = req.body;
|
const { nullable } = req.body;
|
||||||
|
const companyCode = req.user?.companyCode || "*";
|
||||||
|
|
||||||
if (!tableName || !columnName || typeof nullable !== "boolean") {
|
if (!tableName || !columnName || typeof nullable !== "boolean") {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
|
|
@ -2671,17 +2712,53 @@ export async function toggleColumnNullable(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nullable) {
|
// is_nullable 값: 'Y' = NULL 허용, 'N' = NOT NULL
|
||||||
// NOT NULL 해제
|
const isNullableValue = nullable ? "Y" : "N";
|
||||||
const sql = `ALTER TABLE "public"."${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL`;
|
|
||||||
logger.info(`NOT NULL 해제: ${sql}`);
|
if (!nullable) {
|
||||||
await query(sql);
|
// NOT NULL 설정 전 - 해당 회사의 기존 데이터에 NULL이 있는지 확인
|
||||||
} else {
|
const hasCompanyCode = await query<{ column_name: string }>(
|
||||||
// NOT NULL 설정
|
`SELECT column_name FROM information_schema.columns
|
||||||
const sql = `ALTER TABLE "public"."${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL`;
|
WHERE table_name = $1 AND column_name = 'company_code'`,
|
||||||
logger.info(`NOT NULL 설정: ${sql}`);
|
[tableName]
|
||||||
await query(sql);
|
);
|
||||||
|
|
||||||
|
if (hasCompanyCode.length > 0) {
|
||||||
|
const nullCheckQuery = companyCode === "*"
|
||||||
|
? `SELECT COUNT(*) as null_count FROM "${tableName}" WHERE "${columnName}" IS NULL`
|
||||||
|
: `SELECT COUNT(*) as null_count FROM "${tableName}" WHERE "${columnName}" IS NULL AND company_code = $1`;
|
||||||
|
const nullCheckParams = companyCode === "*" ? [] : [companyCode];
|
||||||
|
|
||||||
|
const nullCheckResult = await query<{ null_count: string }>(nullCheckQuery, nullCheckParams);
|
||||||
|
const nullCount = parseInt(nullCheckResult[0]?.null_count || "0", 10);
|
||||||
|
|
||||||
|
if (nullCount > 0) {
|
||||||
|
logger.warn(`NOT NULL 설정 불가 - 해당 회사에 NULL 데이터 존재: ${tableName}.${columnName}`, {
|
||||||
|
companyCode,
|
||||||
|
nullCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `현재 회사 데이터에 NULL 값이 ${nullCount}건 존재합니다. NULL 데이터를 먼저 정리해주세요.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// table_type_columns에 회사별 is_nullable 설정 UPSERT
|
||||||
|
await query(
|
||||||
|
`INSERT INTO table_type_columns (table_name, column_name, is_nullable, company_code, created_date, updated_date)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||||
|
ON CONFLICT (table_name, column_name, company_code)
|
||||||
|
DO UPDATE SET is_nullable = $3, updated_date = NOW()`,
|
||||||
|
[tableName, columnName, isNullableValue, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`NOT NULL 소프트 제약조건 변경: ${tableName}.${columnName} → is_nullable=${isNullableValue}`, {
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -2692,14 +2769,9 @@ export async function toggleColumnNullable(
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("NOT NULL 토글 오류:", error);
|
logger.error("NOT NULL 토글 오류:", error);
|
||||||
|
|
||||||
// NULL 데이터가 있는 컬럼에 NOT NULL 설정 시 안내
|
|
||||||
const errorMsg = error.message?.includes("contains null values")
|
|
||||||
? "해당 컬럼에 NULL 값이 있어 NOT NULL 설정이 불가합니다. NULL 데이터를 먼저 정리해주세요."
|
|
||||||
: "NOT NULL 설정 중 오류가 발생했습니다.";
|
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: errorMsg,
|
message: "NOT NULL 설정 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,11 @@ export class TableManagementService {
|
||||||
cl.input_type as "cl_input_type",
|
cl.input_type as "cl_input_type",
|
||||||
COALESCE(ttc.detail_settings::text, cl.detail_settings::text, '') as "detailSettings",
|
COALESCE(ttc.detail_settings::text, cl.detail_settings::text, '') as "detailSettings",
|
||||||
COALESCE(ttc.description, cl.description, '') as "description",
|
COALESCE(ttc.description, cl.description, '') as "description",
|
||||||
c.is_nullable as "isNullable",
|
CASE
|
||||||
|
WHEN COALESCE(ttc.is_nullable, cl.is_nullable) IS NOT NULL
|
||||||
|
THEN CASE WHEN COALESCE(ttc.is_nullable, cl.is_nullable) = 'N' THEN 'NO' ELSE 'YES' END
|
||||||
|
ELSE c.is_nullable
|
||||||
|
END as "isNullable",
|
||||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
||||||
c.column_default as "defaultValue",
|
c.column_default as "defaultValue",
|
||||||
c.character_maximum_length as "maxLength",
|
c.character_maximum_length as "maxLength",
|
||||||
|
|
@ -241,7 +245,11 @@ export class TableManagementService {
|
||||||
COALESCE(cl.input_type, 'direct') as "inputType",
|
COALESCE(cl.input_type, 'direct') as "inputType",
|
||||||
COALESCE(cl.detail_settings::text, '') as "detailSettings",
|
COALESCE(cl.detail_settings::text, '') as "detailSettings",
|
||||||
COALESCE(cl.description, '') as "description",
|
COALESCE(cl.description, '') as "description",
|
||||||
c.is_nullable as "isNullable",
|
CASE
|
||||||
|
WHEN cl.is_nullable IS NOT NULL
|
||||||
|
THEN CASE WHEN cl.is_nullable = 'N' THEN 'NO' ELSE 'YES' END
|
||||||
|
ELSE c.is_nullable
|
||||||
|
END as "isNullable",
|
||||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
||||||
c.column_default as "defaultValue",
|
c.column_default as "defaultValue",
|
||||||
c.character_maximum_length as "maxLength",
|
c.character_maximum_length as "maxLength",
|
||||||
|
|
@ -2431,6 +2439,67 @@ export class TableManagementService {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사별 NOT NULL 소프트 제약조건 검증
|
||||||
|
* table_type_columns.is_nullable = 'N'인 컬럼에 NULL/빈값이 들어오면 위반 목록을 반환한다.
|
||||||
|
*/
|
||||||
|
async validateNotNullConstraints(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
// 회사별 설정 우선, 없으면 공통(*) 설정 사용
|
||||||
|
const notNullColumns = await query<{ column_name: string; column_label: string }>(
|
||||||
|
`SELECT
|
||||||
|
ttc.column_name,
|
||||||
|
COALESCE(ttc.column_label, ttc.column_name) as column_label
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
WHERE ttc.table_name = $1
|
||||||
|
AND ttc.is_nullable = 'N'
|
||||||
|
AND ttc.company_code = $2`,
|
||||||
|
[tableName, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 회사별 설정이 없으면 공통 설정 확인
|
||||||
|
if (notNullColumns.length === 0 && companyCode !== "*") {
|
||||||
|
const globalNotNull = await query<{ column_name: string; column_label: string }>(
|
||||||
|
`SELECT
|
||||||
|
ttc.column_name,
|
||||||
|
COALESCE(ttc.column_label, ttc.column_name) as column_label
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
WHERE ttc.table_name = $1
|
||||||
|
AND ttc.is_nullable = 'N'
|
||||||
|
AND ttc.company_code = '*'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM table_type_columns ttc2
|
||||||
|
WHERE ttc2.table_name = ttc.table_name
|
||||||
|
AND ttc2.column_name = ttc.column_name
|
||||||
|
AND ttc2.company_code = $2
|
||||||
|
)`,
|
||||||
|
[tableName, companyCode]
|
||||||
|
);
|
||||||
|
notNullColumns.push(...globalNotNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notNullColumns.length === 0) return [];
|
||||||
|
|
||||||
|
const violations: string[] = [];
|
||||||
|
for (const col of notNullColumns) {
|
||||||
|
const value = data[col.column_name];
|
||||||
|
// NULL, undefined, 빈 문자열을 NOT NULL 위반으로 처리
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
violations.push(col.column_label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`NOT NULL 검증 오류: ${tableName}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블에 데이터 추가
|
* 테이블에 데이터 추가
|
||||||
* @returns 무시된 컬럼 정보 (디버깅용)
|
* @returns 무시된 컬럼 정보 (디버깅용)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue