연쇄 통합관리
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* 상호 배제 (Mutual Exclusion) 컨트롤러
|
||||
* 두 필드가 같은 값을 선택할 수 없도록 제한하는 기능
|
||||
*/
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
// =====================================================
|
||||
// 상호 배제 규칙 CRUD
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* 상호 배제 규칙 목록 조회
|
||||
*/
|
||||
export const getExclusions = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { isActive } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT * FROM cascading_mutual_exclusion
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사 필터
|
||||
if (companyCode !== "*") {
|
||||
sql += ` AND company_code = $${paramIndex++}`;
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
// 활성 상태 필터
|
||||
if (isActive) {
|
||||
sql += ` AND is_active = $${paramIndex++}`;
|
||||
params.push(isActive);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY exclusion_name`;
|
||||
|
||||
const result = await query(sql, params);
|
||||
|
||||
logger.info("상호 배제 규칙 목록 조회", { count: result.length, companyCode });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 규칙 목록 조회 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙 목록 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 상호 배제 규칙 상세 조회
|
||||
*/
|
||||
export const getExclusionDetail = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { exclusionId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let sql = `SELECT * FROM cascading_mutual_exclusion WHERE exclusion_id = $1`;
|
||||
const params: any[] = [Number(exclusionId)];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
sql += ` AND company_code = $2`;
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
const result = await queryOne(sql, params);
|
||||
|
||||
if (!result) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("상호 배제 규칙 상세 조회", { exclusionId, companyCode });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 규칙 상세 조회 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙 상세 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 배제 코드 자동 생성 함수
|
||||
*/
|
||||
const generateExclusionCode = async (companyCode: string): Promise<string> => {
|
||||
const prefix = "EX";
|
||||
const result = await queryOne(
|
||||
`SELECT COUNT(*) as cnt FROM cascading_mutual_exclusion WHERE company_code = $1`,
|
||||
[companyCode]
|
||||
);
|
||||
const count = parseInt(result?.cnt || "0", 10) + 1;
|
||||
const timestamp = Date.now().toString(36).toUpperCase().slice(-4);
|
||||
return `${prefix}_${timestamp}_${count.toString().padStart(3, "0")}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 상호 배제 규칙 생성
|
||||
*/
|
||||
export const createExclusion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const {
|
||||
exclusionName,
|
||||
fieldNames, // 콤마로 구분된 필드명 (예: "source_warehouse,target_warehouse")
|
||||
sourceTable,
|
||||
valueColumn,
|
||||
labelColumn,
|
||||
exclusionType = "SAME_VALUE",
|
||||
errorMessage = "동일한 값을 선택할 수 없습니다",
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!exclusionName || !fieldNames || !sourceTable || !valueColumn) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (exclusionName, fieldNames, sourceTable, valueColumn)",
|
||||
});
|
||||
}
|
||||
|
||||
// 배제 코드 자동 생성
|
||||
const exclusionCode = await generateExclusionCode(companyCode);
|
||||
|
||||
// 중복 체크 (생략 - 자동 생성이므로 중복 불가)
|
||||
const existingCheck = await queryOne(
|
||||
`SELECT exclusion_id FROM cascading_mutual_exclusion WHERE exclusion_code = $1 AND company_code = $2`,
|
||||
[exclusionCode, companyCode]
|
||||
);
|
||||
|
||||
if (existingCheck) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 존재하는 배제 코드입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const insertSql = `
|
||||
INSERT INTO cascading_mutual_exclusion (
|
||||
exclusion_code, exclusion_name, field_names,
|
||||
source_table, value_column, label_column,
|
||||
exclusion_type, error_message,
|
||||
company_code, is_active, created_date
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', CURRENT_TIMESTAMP)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await queryOne(insertSql, [
|
||||
exclusionCode,
|
||||
exclusionName,
|
||||
fieldNames,
|
||||
sourceTable,
|
||||
valueColumn,
|
||||
labelColumn || null,
|
||||
exclusionType,
|
||||
errorMessage,
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
logger.info("상호 배제 규칙 생성", { exclusionCode, companyCode });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "상호 배제 규칙이 생성되었습니다.",
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 규칙 생성 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙 생성에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 상호 배제 규칙 수정
|
||||
*/
|
||||
export const updateExclusion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { exclusionId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const {
|
||||
exclusionName,
|
||||
fieldNames,
|
||||
sourceTable,
|
||||
valueColumn,
|
||||
labelColumn,
|
||||
exclusionType,
|
||||
errorMessage,
|
||||
isActive,
|
||||
} = req.body;
|
||||
|
||||
// 기존 규칙 확인
|
||||
let checkSql = `SELECT * FROM cascading_mutual_exclusion WHERE exclusion_id = $1`;
|
||||
const checkParams: any[] = [Number(exclusionId)];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
checkSql += ` AND company_code = $2`;
|
||||
checkParams.push(companyCode);
|
||||
}
|
||||
|
||||
const existing = await queryOne(checkSql, checkParams);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const updateSql = `
|
||||
UPDATE cascading_mutual_exclusion SET
|
||||
exclusion_name = COALESCE($1, exclusion_name),
|
||||
field_names = COALESCE($2, field_names),
|
||||
source_table = COALESCE($3, source_table),
|
||||
value_column = COALESCE($4, value_column),
|
||||
label_column = COALESCE($5, label_column),
|
||||
exclusion_type = COALESCE($6, exclusion_type),
|
||||
error_message = COALESCE($7, error_message),
|
||||
is_active = COALESCE($8, is_active)
|
||||
WHERE exclusion_id = $9
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await queryOne(updateSql, [
|
||||
exclusionName,
|
||||
fieldNames,
|
||||
sourceTable,
|
||||
valueColumn,
|
||||
labelColumn,
|
||||
exclusionType,
|
||||
errorMessage,
|
||||
isActive,
|
||||
Number(exclusionId),
|
||||
]);
|
||||
|
||||
logger.info("상호 배제 규칙 수정", { exclusionId, companyCode });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "상호 배제 규칙이 수정되었습니다.",
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 규칙 수정 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙 수정에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 상호 배제 규칙 삭제
|
||||
*/
|
||||
export const deleteExclusion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { exclusionId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let deleteSql = `DELETE FROM cascading_mutual_exclusion WHERE exclusion_id = $1`;
|
||||
const deleteParams: any[] = [Number(exclusionId)];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
deleteSql += ` AND company_code = $2`;
|
||||
deleteParams.push(companyCode);
|
||||
}
|
||||
|
||||
deleteSql += ` RETURNING exclusion_id`;
|
||||
|
||||
const result = await queryOne(deleteSql, deleteParams);
|
||||
|
||||
if (!result) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("상호 배제 규칙 삭제", { exclusionId, companyCode });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "상호 배제 규칙이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 규칙 삭제 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙 삭제에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// 상호 배제 검증 API (실제 사용)
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* 상호 배제 검증
|
||||
* 선택하려는 값이 다른 필드와 충돌하는지 확인
|
||||
*/
|
||||
export const validateExclusion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { exclusionCode } = req.params;
|
||||
const { fieldValues } = req.body; // { "source_warehouse": "WH001", "target_warehouse": "WH002" }
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 배제 규칙 조회
|
||||
let exclusionSql = `SELECT * FROM cascading_mutual_exclusion WHERE exclusion_code = $1 AND is_active = 'Y'`;
|
||||
const exclusionParams: any[] = [exclusionCode];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
exclusionSql += ` AND company_code = $2`;
|
||||
exclusionParams.push(companyCode);
|
||||
}
|
||||
|
||||
const exclusion = await queryOne(exclusionSql, exclusionParams);
|
||||
|
||||
if (!exclusion) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 필드명 파싱
|
||||
const fields = exclusion.field_names.split(",").map((f: string) => f.trim());
|
||||
|
||||
// 필드 값 수집
|
||||
const values: string[] = [];
|
||||
for (const field of fields) {
|
||||
if (fieldValues[field]) {
|
||||
values.push(fieldValues[field]);
|
||||
}
|
||||
}
|
||||
|
||||
// 상호 배제 검증
|
||||
let isValid = true;
|
||||
let errorMessage = null;
|
||||
let conflictingFields: string[] = [];
|
||||
|
||||
if (exclusion.exclusion_type === "SAME_VALUE") {
|
||||
// 같은 값이 있는지 확인
|
||||
const uniqueValues = new Set(values);
|
||||
if (uniqueValues.size !== values.length) {
|
||||
isValid = false;
|
||||
errorMessage = exclusion.error_message;
|
||||
|
||||
// 충돌하는 필드 찾기
|
||||
const valueCounts: Record<string, string[]> = {};
|
||||
for (const field of fields) {
|
||||
const val = fieldValues[field];
|
||||
if (val) {
|
||||
if (!valueCounts[val]) {
|
||||
valueCounts[val] = [];
|
||||
}
|
||||
valueCounts[val].push(field);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, fieldList] of Object.entries(valueCounts)) {
|
||||
if (fieldList.length > 1) {
|
||||
conflictingFields = fieldList;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("상호 배제 검증", {
|
||||
exclusionCode,
|
||||
isValid,
|
||||
fieldValues,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
isValid,
|
||||
errorMessage: isValid ? null : errorMessage,
|
||||
conflictingFields,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 검증 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 검증에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 필드에 대한 배제 옵션 조회
|
||||
* 다른 필드에서 이미 선택한 값을 제외한 옵션 반환
|
||||
*/
|
||||
export const getExcludedOptions = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { exclusionCode } = req.params;
|
||||
const { currentField, selectedValues } = req.query; // selectedValues: 이미 선택된 값들 (콤마 구분)
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 배제 규칙 조회
|
||||
let exclusionSql = `SELECT * FROM cascading_mutual_exclusion WHERE exclusion_code = $1 AND is_active = 'Y'`;
|
||||
const exclusionParams: any[] = [exclusionCode];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
exclusionSql += ` AND company_code = $2`;
|
||||
exclusionParams.push(companyCode);
|
||||
}
|
||||
|
||||
const exclusion = await queryOne(exclusionSql, exclusionParams);
|
||||
|
||||
if (!exclusion) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "상호 배제 규칙을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 옵션 조회
|
||||
const labelColumn = exclusion.label_column || exclusion.value_column;
|
||||
let optionsSql = `
|
||||
SELECT
|
||||
${exclusion.value_column} as value,
|
||||
${labelColumn} as label
|
||||
FROM ${exclusion.source_table}
|
||||
WHERE 1=1
|
||||
`;
|
||||
const optionsParams: any[] = [];
|
||||
let optionsParamIndex = 1;
|
||||
|
||||
// 멀티테넌시 필터
|
||||
if (companyCode !== "*") {
|
||||
const columnCheck = await queryOne(
|
||||
`SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = $1 AND column_name = 'company_code'`,
|
||||
[exclusion.source_table]
|
||||
);
|
||||
|
||||
if (columnCheck) {
|
||||
optionsSql += ` AND company_code = $${optionsParamIndex++}`;
|
||||
optionsParams.push(companyCode);
|
||||
}
|
||||
}
|
||||
|
||||
// 이미 선택된 값 제외
|
||||
if (selectedValues) {
|
||||
const excludeValues = (selectedValues as string).split(",").map((v) => v.trim()).filter((v) => v);
|
||||
if (excludeValues.length > 0) {
|
||||
const placeholders = excludeValues.map((_, i) => `$${optionsParamIndex + i}`).join(",");
|
||||
optionsSql += ` AND ${exclusion.value_column} NOT IN (${placeholders})`;
|
||||
optionsParams.push(...excludeValues);
|
||||
}
|
||||
}
|
||||
|
||||
optionsSql += ` ORDER BY ${labelColumn}`;
|
||||
|
||||
const optionsResult = await query(optionsSql, optionsParams);
|
||||
|
||||
logger.info("상호 배제 옵션 조회", {
|
||||
exclusionCode,
|
||||
currentField,
|
||||
excludedCount: (selectedValues as string)?.split(",").length || 0,
|
||||
optionCount: optionsResult.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: optionsResult,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("상호 배제 옵션 조회 실패", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "상호 배제 옵션 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user