카테고리 설정 구현
This commit is contained in:
@@ -662,6 +662,10 @@ export const getParentOptions = async (
|
||||
/**
|
||||
* 연쇄 관계로 자식 옵션 조회
|
||||
* 실제 연쇄 드롭다운에서 사용하는 API
|
||||
*
|
||||
* 다중 부모값 지원:
|
||||
* - parentValue: 단일 값 (예: "공정검사")
|
||||
* - parentValues: 다중 값 (예: "공정검사,출하검사" 또는 배열)
|
||||
*/
|
||||
export const getCascadingOptions = async (
|
||||
req: AuthenticatedRequest,
|
||||
@@ -669,10 +673,26 @@ export const getCascadingOptions = async (
|
||||
) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const { parentValue } = req.query;
|
||||
const { parentValue, parentValues } = req.query;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
if (!parentValue) {
|
||||
// 다중 부모값 파싱
|
||||
let parentValueArray: string[] = [];
|
||||
|
||||
if (parentValues) {
|
||||
// parentValues가 있으면 우선 사용 (다중 선택)
|
||||
if (Array.isArray(parentValues)) {
|
||||
parentValueArray = parentValues.map(v => String(v));
|
||||
} else {
|
||||
// 콤마로 구분된 문자열
|
||||
parentValueArray = String(parentValues).split(',').map(v => v.trim()).filter(v => v);
|
||||
}
|
||||
} else if (parentValue) {
|
||||
// 기존 단일 값 호환
|
||||
parentValueArray = [String(parentValue)];
|
||||
}
|
||||
|
||||
if (parentValueArray.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: [],
|
||||
@@ -714,13 +734,17 @@ export const getCascadingOptions = async (
|
||||
|
||||
const relation = relationResult.rows[0];
|
||||
|
||||
// 자식 옵션 조회
|
||||
// 자식 옵션 조회 - 다중 부모값에 대해 IN 절 사용
|
||||
// SQL Injection 방지를 위해 파라미터화된 쿼리 사용
|
||||
const placeholders = parentValueArray.map((_, idx) => `$${idx + 1}`).join(', ');
|
||||
|
||||
let optionsQuery = `
|
||||
SELECT
|
||||
SELECT DISTINCT
|
||||
${relation.child_value_column} as value,
|
||||
${relation.child_label_column} as label
|
||||
${relation.child_label_column} as label,
|
||||
${relation.child_filter_column} as parent_value
|
||||
FROM ${relation.child_table}
|
||||
WHERE ${relation.child_filter_column} = $1
|
||||
WHERE ${relation.child_filter_column} IN (${placeholders})
|
||||
`;
|
||||
|
||||
// 멀티테넌시 적용 (테이블에 company_code가 있는 경우)
|
||||
@@ -730,7 +754,8 @@ export const getCascadingOptions = async (
|
||||
[relation.child_table]
|
||||
);
|
||||
|
||||
const optionsParams: any[] = [parentValue];
|
||||
const optionsParams: any[] = [...parentValueArray];
|
||||
let paramIndex = parentValueArray.length + 1;
|
||||
|
||||
// company_code = "*"는 최고 관리자 전용이므로 일반 회사는 자기 회사 데이터만
|
||||
if (
|
||||
@@ -738,8 +763,9 @@ export const getCascadingOptions = async (
|
||||
tableInfoResult.rowCount > 0 &&
|
||||
companyCode !== "*"
|
||||
) {
|
||||
optionsQuery += ` AND company_code = $2`;
|
||||
optionsQuery += ` AND company_code = $${paramIndex}`;
|
||||
optionsParams.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 정렬
|
||||
@@ -751,9 +777,9 @@ export const getCascadingOptions = async (
|
||||
|
||||
const optionsResult = await pool.query(optionsQuery, optionsParams);
|
||||
|
||||
logger.info("연쇄 옵션 조회", {
|
||||
logger.info("연쇄 옵션 조회 (다중 부모값 지원)", {
|
||||
relationCode: code,
|
||||
parentValue,
|
||||
parentValues: parentValueArray,
|
||||
optionsCount: optionsResult.rowCount,
|
||||
});
|
||||
|
||||
|
||||
927
backend-node/src/controllers/categoryValueCascadingController.ts
Normal file
927
backend-node/src/controllers/categoryValueCascadingController.ts
Normal file
@@ -0,0 +1,927 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// ============================================
|
||||
// 카테고리 값 연쇄관계 그룹 CRUD
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄관계 그룹 목록 조회
|
||||
*/
|
||||
export const getCategoryValueCascadingGroups = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { isActive } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
group_id,
|
||||
relation_code,
|
||||
relation_name,
|
||||
description,
|
||||
parent_table_name,
|
||||
parent_column_name,
|
||||
parent_menu_objid,
|
||||
child_table_name,
|
||||
child_column_name,
|
||||
child_menu_objid,
|
||||
clear_on_parent_change,
|
||||
show_group_label,
|
||||
empty_parent_message,
|
||||
no_options_message,
|
||||
company_code,
|
||||
is_active,
|
||||
created_by,
|
||||
created_date,
|
||||
updated_by,
|
||||
updated_date
|
||||
FROM category_value_cascading_group
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시 필터링
|
||||
if (companyCode !== "*") {
|
||||
query += ` AND (company_code = $${paramIndex} OR company_code = '*')`;
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (isActive !== undefined) {
|
||||
query += ` AND is_active = $${paramIndex}`;
|
||||
params.push(isActive);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
query += ` ORDER BY relation_name ASC`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("카테고리 값 연쇄관계 그룹 목록 조회", {
|
||||
companyCode,
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 그룹 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹 목록 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄관계 그룹 상세 조회
|
||||
*/
|
||||
export const getCategoryValueCascadingGroupById = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { groupId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 그룹 정보 조회
|
||||
let groupQuery = `
|
||||
SELECT
|
||||
group_id,
|
||||
relation_code,
|
||||
relation_name,
|
||||
description,
|
||||
parent_table_name,
|
||||
parent_column_name,
|
||||
parent_menu_objid,
|
||||
child_table_name,
|
||||
child_column_name,
|
||||
child_menu_objid,
|
||||
clear_on_parent_change,
|
||||
show_group_label,
|
||||
empty_parent_message,
|
||||
no_options_message,
|
||||
company_code,
|
||||
is_active
|
||||
FROM category_value_cascading_group
|
||||
WHERE group_id = $1
|
||||
`;
|
||||
|
||||
const groupParams: any[] = [groupId];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
groupQuery += ` AND (company_code = $2 OR company_code = '*')`;
|
||||
groupParams.push(companyCode);
|
||||
}
|
||||
|
||||
const groupResult = await pool.query(groupQuery, groupParams);
|
||||
|
||||
if (groupResult.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 매핑 정보 조회
|
||||
const mappingQuery = `
|
||||
SELECT
|
||||
mapping_id,
|
||||
parent_value_code,
|
||||
parent_value_label,
|
||||
child_value_code,
|
||||
child_value_label,
|
||||
display_order,
|
||||
is_active
|
||||
FROM category_value_cascading_mapping
|
||||
WHERE group_id = $1 AND is_active = 'Y'
|
||||
ORDER BY parent_value_code, display_order, child_value_label
|
||||
`;
|
||||
|
||||
const mappingResult = await pool.query(mappingQuery, [groupId]);
|
||||
|
||||
// 부모 값별로 자식 값 그룹화
|
||||
const mappingsByParent: Record<string, any[]> = {};
|
||||
for (const row of mappingResult.rows) {
|
||||
const parentKey = row.parent_value_code;
|
||||
if (!mappingsByParent[parentKey]) {
|
||||
mappingsByParent[parentKey] = [];
|
||||
}
|
||||
mappingsByParent[parentKey].push({
|
||||
childValueCode: row.child_value_code,
|
||||
childValueLabel: row.child_value_label,
|
||||
displayOrder: row.display_order,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...groupResult.rows[0],
|
||||
mappings: mappingResult.rows,
|
||||
mappingsByParent,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 그룹 상세 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계 코드로 조회
|
||||
*/
|
||||
export const getCategoryValueCascadingByCode = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
group_id,
|
||||
relation_code,
|
||||
relation_name,
|
||||
description,
|
||||
parent_table_name,
|
||||
parent_column_name,
|
||||
parent_menu_objid,
|
||||
child_table_name,
|
||||
child_column_name,
|
||||
child_menu_objid,
|
||||
clear_on_parent_change,
|
||||
show_group_label,
|
||||
empty_parent_message,
|
||||
no_options_message,
|
||||
company_code,
|
||||
is_active
|
||||
FROM category_value_cascading_group
|
||||
WHERE relation_code = $1 AND is_active = 'Y'
|
||||
`;
|
||||
|
||||
const params: any[] = [code];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
query += ` AND (company_code = $2 OR company_code = '*')`;
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
query += ` LIMIT 1`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 코드 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄관계 그룹 생성
|
||||
*/
|
||||
export const createCategoryValueCascadingGroup = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "system";
|
||||
|
||||
const {
|
||||
relationCode,
|
||||
relationName,
|
||||
description,
|
||||
parentTableName,
|
||||
parentColumnName,
|
||||
parentMenuObjid,
|
||||
childTableName,
|
||||
childColumnName,
|
||||
childMenuObjid,
|
||||
clearOnParentChange = true,
|
||||
showGroupLabel = true,
|
||||
emptyParentMessage,
|
||||
noOptionsMessage,
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!relationCode || !relationName || !parentTableName || !parentColumnName || !childTableName || !childColumnName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 중복 코드 체크
|
||||
const duplicateCheck = await pool.query(
|
||||
`SELECT group_id FROM category_value_cascading_group
|
||||
WHERE relation_code = $1 AND (company_code = $2 OR company_code = '*')`,
|
||||
[relationCode, companyCode]
|
||||
);
|
||||
|
||||
if (duplicateCheck.rowCount && duplicateCheck.rowCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "이미 존재하는 관계 코드입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO category_value_cascading_group (
|
||||
relation_code,
|
||||
relation_name,
|
||||
description,
|
||||
parent_table_name,
|
||||
parent_column_name,
|
||||
parent_menu_objid,
|
||||
child_table_name,
|
||||
child_column_name,
|
||||
child_menu_objid,
|
||||
clear_on_parent_change,
|
||||
show_group_label,
|
||||
empty_parent_message,
|
||||
no_options_message,
|
||||
company_code,
|
||||
is_active,
|
||||
created_by,
|
||||
created_date
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, 'Y', $15, NOW())
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
relationCode,
|
||||
relationName,
|
||||
description || null,
|
||||
parentTableName,
|
||||
parentColumnName,
|
||||
parentMenuObjid || null,
|
||||
childTableName,
|
||||
childColumnName,
|
||||
childMenuObjid || null,
|
||||
clearOnParentChange ? "Y" : "N",
|
||||
showGroupLabel ? "Y" : "N",
|
||||
emptyParentMessage || "상위 항목을 먼저 선택하세요",
|
||||
noOptionsMessage || "선택 가능한 항목이 없습니다",
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("카테고리 값 연쇄관계 그룹 생성", {
|
||||
groupId: result.rows[0].group_id,
|
||||
relationCode,
|
||||
companyCode,
|
||||
userId,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: "카테고리 값 연쇄관계 그룹이 생성되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 그룹 생성 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹 생성에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄관계 그룹 수정
|
||||
*/
|
||||
export const updateCategoryValueCascadingGroup = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { groupId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "system";
|
||||
|
||||
const {
|
||||
relationName,
|
||||
description,
|
||||
parentTableName,
|
||||
parentColumnName,
|
||||
parentMenuObjid,
|
||||
childTableName,
|
||||
childColumnName,
|
||||
childMenuObjid,
|
||||
clearOnParentChange,
|
||||
showGroupLabel,
|
||||
emptyParentMessage,
|
||||
noOptionsMessage,
|
||||
isActive,
|
||||
} = req.body;
|
||||
|
||||
// 권한 체크
|
||||
const existingCheck = await pool.query(
|
||||
`SELECT group_id, company_code FROM category_value_cascading_group WHERE group_id = $1`,
|
||||
[groupId]
|
||||
);
|
||||
|
||||
if (existingCheck.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const existingCompanyCode = existingCheck.rows[0].company_code;
|
||||
if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "수정 권한이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
UPDATE category_value_cascading_group SET
|
||||
relation_name = COALESCE($1, relation_name),
|
||||
description = COALESCE($2, description),
|
||||
parent_table_name = COALESCE($3, parent_table_name),
|
||||
parent_column_name = COALESCE($4, parent_column_name),
|
||||
parent_menu_objid = COALESCE($5, parent_menu_objid),
|
||||
child_table_name = COALESCE($6, child_table_name),
|
||||
child_column_name = COALESCE($7, child_column_name),
|
||||
child_menu_objid = COALESCE($8, child_menu_objid),
|
||||
clear_on_parent_change = COALESCE($9, clear_on_parent_change),
|
||||
show_group_label = COALESCE($10, show_group_label),
|
||||
empty_parent_message = COALESCE($11, empty_parent_message),
|
||||
no_options_message = COALESCE($12, no_options_message),
|
||||
is_active = COALESCE($13, is_active),
|
||||
updated_by = $14,
|
||||
updated_date = NOW()
|
||||
WHERE group_id = $15
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
relationName,
|
||||
description,
|
||||
parentTableName,
|
||||
parentColumnName,
|
||||
parentMenuObjid,
|
||||
childTableName,
|
||||
childColumnName,
|
||||
childMenuObjid,
|
||||
clearOnParentChange !== undefined ? (clearOnParentChange ? "Y" : "N") : null,
|
||||
showGroupLabel !== undefined ? (showGroupLabel ? "Y" : "N") : null,
|
||||
emptyParentMessage,
|
||||
noOptionsMessage,
|
||||
isActive !== undefined ? (isActive ? "Y" : "N") : null,
|
||||
userId,
|
||||
groupId,
|
||||
]);
|
||||
|
||||
logger.info("카테고리 값 연쇄관계 그룹 수정", {
|
||||
groupId,
|
||||
companyCode,
|
||||
userId,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: "카테고리 값 연쇄관계 그룹이 수정되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 그룹 수정 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹 수정에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄관계 그룹 삭제
|
||||
*/
|
||||
export const deleteCategoryValueCascadingGroup = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { groupId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "system";
|
||||
|
||||
// 권한 체크
|
||||
const existingCheck = await pool.query(
|
||||
`SELECT group_id, company_code FROM category_value_cascading_group WHERE group_id = $1`,
|
||||
[groupId]
|
||||
);
|
||||
|
||||
if (existingCheck.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const existingCompanyCode = existingCheck.rows[0].company_code;
|
||||
if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "삭제 권한이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 소프트 삭제
|
||||
await pool.query(
|
||||
`UPDATE category_value_cascading_group
|
||||
SET is_active = 'N', updated_by = $1, updated_date = NOW()
|
||||
WHERE group_id = $2`,
|
||||
[userId, groupId]
|
||||
);
|
||||
|
||||
logger.info("카테고리 값 연쇄관계 그룹 삭제", {
|
||||
groupId,
|
||||
companyCode,
|
||||
userId,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "카테고리 값 연쇄관계 그룹이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 그룹 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹 삭제에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 카테고리 값 연쇄관계 매핑 CRUD
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 매핑 일괄 저장 (기존 매핑 교체)
|
||||
*/
|
||||
export const saveCategoryValueCascadingMappings = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { groupId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { mappings } = req.body; // [{ parentValueCode, parentValueLabel, childValueCode, childValueLabel, displayOrder }]
|
||||
|
||||
if (!Array.isArray(mappings)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "mappings는 배열이어야 합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 그룹 존재 확인
|
||||
const groupCheck = await pool.query(
|
||||
`SELECT group_id FROM category_value_cascading_group WHERE group_id = $1 AND is_active = 'Y'`,
|
||||
[groupId]
|
||||
);
|
||||
|
||||
if (groupCheck.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 그룹을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 트랜잭션으로 처리
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 기존 매핑 삭제 (하드 삭제)
|
||||
await client.query(
|
||||
`DELETE FROM category_value_cascading_mapping WHERE group_id = $1`,
|
||||
[groupId]
|
||||
);
|
||||
|
||||
// 새 매핑 삽입
|
||||
if (mappings.length > 0) {
|
||||
const insertQuery = `
|
||||
INSERT INTO category_value_cascading_mapping (
|
||||
group_id, parent_value_code, parent_value_label,
|
||||
child_value_code, child_value_label, display_order,
|
||||
company_code, is_active, created_date
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', NOW())
|
||||
`;
|
||||
|
||||
for (const mapping of mappings) {
|
||||
await client.query(insertQuery, [
|
||||
groupId,
|
||||
mapping.parentValueCode,
|
||||
mapping.parentValueLabel || null,
|
||||
mapping.childValueCode,
|
||||
mapping.childValueLabel || null,
|
||||
mapping.displayOrder || 0,
|
||||
companyCode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("카테고리 값 연쇄관계 매핑 저장", {
|
||||
groupId,
|
||||
mappingCount: mappings.length,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${mappings.length}개의 매핑이 저장되었습니다.`,
|
||||
});
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄관계 매핑 저장 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계 매핑 저장에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 연쇄 옵션 조회 (실제 드롭다운에서 사용)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 카테고리 값 연쇄 옵션 조회
|
||||
* 부모 값(들)에 해당하는 자식 카테고리 값 목록 반환
|
||||
* 다중 부모값 지원
|
||||
*/
|
||||
export const getCategoryValueCascadingOptions = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const { parentValue, parentValues } = req.query;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 다중 부모값 파싱
|
||||
let parentValueArray: string[] = [];
|
||||
|
||||
if (parentValues) {
|
||||
if (Array.isArray(parentValues)) {
|
||||
parentValueArray = parentValues.map(v => String(v));
|
||||
} else {
|
||||
parentValueArray = String(parentValues).split(',').map(v => v.trim()).filter(v => v);
|
||||
}
|
||||
} else if (parentValue) {
|
||||
parentValueArray = [String(parentValue)];
|
||||
}
|
||||
|
||||
if (parentValueArray.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: "부모 값이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 관계 정보 조회
|
||||
let groupQuery = `
|
||||
SELECT group_id, show_group_label
|
||||
FROM category_value_cascading_group
|
||||
WHERE relation_code = $1 AND is_active = 'Y'
|
||||
`;
|
||||
|
||||
const groupParams: any[] = [code];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
groupQuery += ` AND (company_code = $2 OR company_code = '*')`;
|
||||
groupParams.push(companyCode);
|
||||
}
|
||||
|
||||
groupQuery += ` LIMIT 1`;
|
||||
|
||||
const groupResult = await pool.query(groupQuery, groupParams);
|
||||
|
||||
if (groupResult.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const group = groupResult.rows[0];
|
||||
|
||||
// 매핑된 자식 값 조회 (다중 부모값에 대해 IN 절 사용)
|
||||
const placeholders = parentValueArray.map((_, idx) => `$${idx + 2}`).join(', ');
|
||||
|
||||
const optionsQuery = `
|
||||
SELECT DISTINCT
|
||||
child_value_code as value,
|
||||
child_value_label as label,
|
||||
parent_value_code as parent_value,
|
||||
parent_value_label as parent_label,
|
||||
display_order
|
||||
FROM category_value_cascading_mapping
|
||||
WHERE group_id = $1
|
||||
AND parent_value_code IN (${placeholders})
|
||||
AND is_active = 'Y'
|
||||
ORDER BY parent_value_code, display_order, child_value_label
|
||||
`;
|
||||
|
||||
const optionsResult = await pool.query(optionsQuery, [group.group_id, ...parentValueArray]);
|
||||
|
||||
logger.info("카테고리 값 연쇄 옵션 조회", {
|
||||
relationCode: code,
|
||||
parentValues: parentValueArray,
|
||||
optionsCount: optionsResult.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: optionsResult.rows,
|
||||
showGroupLabel: group.show_group_label === 'Y',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 값 연쇄 옵션 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄 옵션 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 부모 카테고리 값 목록 조회
|
||||
*/
|
||||
export const getCategoryValueCascadingParentOptions = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 관계 정보 조회
|
||||
let groupQuery = `
|
||||
SELECT
|
||||
group_id,
|
||||
parent_table_name,
|
||||
parent_column_name,
|
||||
parent_menu_objid
|
||||
FROM category_value_cascading_group
|
||||
WHERE relation_code = $1 AND is_active = 'Y'
|
||||
`;
|
||||
|
||||
const groupParams: any[] = [code];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
groupQuery += ` AND (company_code = $2 OR company_code = '*')`;
|
||||
groupParams.push(companyCode);
|
||||
}
|
||||
|
||||
groupQuery += ` LIMIT 1`;
|
||||
|
||||
const groupResult = await pool.query(groupQuery, groupParams);
|
||||
|
||||
if (groupResult.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const group = groupResult.rows[0];
|
||||
|
||||
// 부모 카테고리 값 조회 (table_column_category_values에서)
|
||||
let optionsQuery = `
|
||||
SELECT
|
||||
value_code as value,
|
||||
value_label as label,
|
||||
value_order as display_order
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND is_active = true
|
||||
`;
|
||||
|
||||
const optionsParams: any[] = [group.parent_table_name, group.parent_column_name];
|
||||
let paramIndex = 3;
|
||||
|
||||
// 메뉴 스코프 적용
|
||||
if (group.parent_menu_objid) {
|
||||
optionsQuery += ` AND menu_objid = $${paramIndex}`;
|
||||
optionsParams.push(group.parent_menu_objid);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 멀티테넌시 적용
|
||||
if (companyCode !== "*") {
|
||||
optionsQuery += ` AND (company_code = $${paramIndex} OR company_code = '*')`;
|
||||
optionsParams.push(companyCode);
|
||||
}
|
||||
|
||||
optionsQuery += ` ORDER BY value_order, value_label`;
|
||||
|
||||
const optionsResult = await pool.query(optionsQuery, optionsParams);
|
||||
|
||||
logger.info("부모 카테고리 값 조회", {
|
||||
relationCode: code,
|
||||
tableName: group.parent_table_name,
|
||||
columnName: group.parent_column_name,
|
||||
optionsCount: optionsResult.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: optionsResult.rows,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("부모 카테고리 값 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "부모 카테고리 값 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 자식 카테고리 값 목록 조회 (매핑 설정 UI용)
|
||||
*/
|
||||
export const getCategoryValueCascadingChildOptions = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 관계 정보 조회
|
||||
let groupQuery = `
|
||||
SELECT
|
||||
group_id,
|
||||
child_table_name,
|
||||
child_column_name,
|
||||
child_menu_objid
|
||||
FROM category_value_cascading_group
|
||||
WHERE relation_code = $1 AND is_active = 'Y'
|
||||
`;
|
||||
|
||||
const groupParams: any[] = [code];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
groupQuery += ` AND (company_code = $2 OR company_code = '*')`;
|
||||
groupParams.push(companyCode);
|
||||
}
|
||||
|
||||
groupQuery += ` LIMIT 1`;
|
||||
|
||||
const groupResult = await pool.query(groupQuery, groupParams);
|
||||
|
||||
if (groupResult.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "카테고리 값 연쇄관계를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const group = groupResult.rows[0];
|
||||
|
||||
// 자식 카테고리 값 조회 (table_column_category_values에서)
|
||||
let optionsQuery = `
|
||||
SELECT
|
||||
value_code as value,
|
||||
value_label as label,
|
||||
value_order as display_order
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND is_active = true
|
||||
`;
|
||||
|
||||
const optionsParams: any[] = [group.child_table_name, group.child_column_name];
|
||||
let paramIndex = 3;
|
||||
|
||||
// 메뉴 스코프 적용
|
||||
if (group.child_menu_objid) {
|
||||
optionsQuery += ` AND menu_objid = $${paramIndex}`;
|
||||
optionsParams.push(group.child_menu_objid);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 멀티테넌시 적용
|
||||
if (companyCode !== "*") {
|
||||
optionsQuery += ` AND (company_code = $${paramIndex} OR company_code = '*')`;
|
||||
optionsParams.push(companyCode);
|
||||
}
|
||||
|
||||
optionsQuery += ` ORDER BY value_order, value_label`;
|
||||
|
||||
const optionsResult = await pool.query(optionsQuery, optionsParams);
|
||||
|
||||
logger.info("자식 카테고리 값 조회", {
|
||||
relationCode: code,
|
||||
tableName: group.child_table_name,
|
||||
columnName: group.child_column_name,
|
||||
optionsCount: optionsResult.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: optionsResult.rows,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("자식 카테고리 값 조회 실패", { error: error.message });
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "자식 카테고리 값 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user