feat: 카테고리 값 삭제 기능 개선 및 하위 카테고리 수집 로직 추가
- 카테고리 값 삭제 시, 자기 자신과 모든 하위 카테고리 ID를 재귀적으로 수집하는 기능을 추가하였습니다. - 삭제 대상 카테고리 값 수집 완료 후, 하위 카테고리부터 역순으로 삭제하는 로직을 구현하였습니다. - 관련된 로그 메시지를 추가하여 삭제 과정과 결과를 기록하도록 하였습니다. - 화면 관리 기능에서 하위 항목 개수를 계산하는 로직을 개선하여 사용자에게 더 정확한 정보를 제공하도록 하였습니다.
This commit is contained in:
@@ -403,6 +403,33 @@ class CategoryTreeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 하위 카테고리 값 ID 재귀 수집
|
||||
*/
|
||||
private async collectAllChildValueIds(
|
||||
companyCode: string,
|
||||
valueId: number
|
||||
): Promise<number[]> {
|
||||
const pool = getPool();
|
||||
|
||||
// 재귀 CTE를 사용하여 모든 하위 카테고리 수집
|
||||
const query = `
|
||||
WITH RECURSIVE category_tree AS (
|
||||
SELECT value_id FROM category_values_test
|
||||
WHERE parent_value_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||
UNION ALL
|
||||
SELECT cv.value_id
|
||||
FROM category_values_test cv
|
||||
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
||||
WHERE cv.company_code = $2 OR cv.company_code = '*'
|
||||
)
|
||||
SELECT value_id FROM category_tree
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [valueId, companyCode]);
|
||||
return result.rows.map(row => row.value_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 삭제 (하위 항목도 함께 삭제)
|
||||
*/
|
||||
@@ -410,20 +437,33 @@ class CategoryTreeService {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*') AND value_id = $2
|
||||
RETURNING value_id
|
||||
`;
|
||||
// 1. 모든 하위 카테고리 ID 수집
|
||||
const childValueIds = await this.collectAllChildValueIds(companyCode, valueId);
|
||||
const allValueIds = [valueId, ...childValueIds];
|
||||
|
||||
logger.info("삭제 대상 카테고리 값 수집 완료", {
|
||||
valueId,
|
||||
childCount: childValueIds.length,
|
||||
totalCount: allValueIds.length,
|
||||
});
|
||||
|
||||
const result = await pool.query(query, [companyCode, valueId]);
|
||||
|
||||
if (result.rowCount && result.rowCount > 0) {
|
||||
logger.info("카테고리 값 삭제 완료", { valueId });
|
||||
return true;
|
||||
// 2. 하위 카테고리부터 역순으로 삭제 (외래키 제약 회피)
|
||||
const reversedIds = [...allValueIds].reverse();
|
||||
|
||||
for (const id of reversedIds) {
|
||||
await pool.query(
|
||||
`DELETE FROM category_values_test WHERE (company_code = $1 OR company_code = '*') AND value_id = $2`,
|
||||
[companyCode, id]
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
logger.info("카테고리 값 삭제 완료", {
|
||||
valueId,
|
||||
deletedCount: allValueIds.length,
|
||||
deletedChildCount: childValueIds.length,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 삭제 실패", { error: err.message, valueId });
|
||||
|
||||
@@ -619,7 +619,55 @@ class TableCategoryValueService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 삭제 (물리적 삭제)
|
||||
* 모든 하위 카테고리 값 ID 재귀 수집
|
||||
*/
|
||||
private async collectAllChildValueIds(
|
||||
valueId: number,
|
||||
companyCode: string
|
||||
): Promise<number[]> {
|
||||
const pool = getPool();
|
||||
const allChildIds: number[] = [];
|
||||
|
||||
// 재귀 CTE를 사용하여 모든 하위 카테고리 수집
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
query = `
|
||||
WITH RECURSIVE category_tree AS (
|
||||
SELECT value_id FROM table_column_category_values WHERE parent_value_id = $1
|
||||
UNION ALL
|
||||
SELECT cv.value_id
|
||||
FROM table_column_category_values cv
|
||||
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
||||
)
|
||||
SELECT value_id FROM category_tree
|
||||
`;
|
||||
params = [valueId];
|
||||
} else {
|
||||
query = `
|
||||
WITH RECURSIVE category_tree AS (
|
||||
SELECT value_id FROM table_column_category_values
|
||||
WHERE parent_value_id = $1 AND company_code = $2
|
||||
UNION ALL
|
||||
SELECT cv.value_id
|
||||
FROM table_column_category_values cv
|
||||
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
||||
WHERE cv.company_code = $2
|
||||
)
|
||||
SELECT value_id FROM category_tree
|
||||
`;
|
||||
params = [valueId, companyCode];
|
||||
}
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
result.rows.forEach(row => allChildIds.push(row.value_id));
|
||||
|
||||
return allChildIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 삭제 (하위 카테고리 포함 물리적 삭제)
|
||||
*/
|
||||
async deleteCategoryValue(
|
||||
valueId: number,
|
||||
@@ -629,82 +677,74 @@ class TableCategoryValueService {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
// 1. 사용 여부 확인
|
||||
const usage = await this.checkCategoryValueUsage(valueId, companyCode);
|
||||
// 1. 자기 자신 + 모든 하위 카테고리 ID 수집
|
||||
const childValueIds = await this.collectAllChildValueIds(valueId, companyCode);
|
||||
const allValueIds = [valueId, ...childValueIds];
|
||||
|
||||
logger.info("삭제 대상 카테고리 값 수집 완료", {
|
||||
valueId,
|
||||
childCount: childValueIds.length,
|
||||
totalCount: allValueIds.length,
|
||||
});
|
||||
|
||||
if (usage.isUsed) {
|
||||
let errorMessage = "이 카테고리 값을 삭제할 수 없습니다.\n";
|
||||
errorMessage += `\n현재 ${usage.totalCount}개의 데이터에서 사용 중입니다.`;
|
||||
// 2. 모든 대상 항목의 사용 여부 확인
|
||||
for (const id of allValueIds) {
|
||||
const usage = await this.checkCategoryValueUsage(id, companyCode);
|
||||
|
||||
if (usage.usedInTables.length > 0) {
|
||||
const menuNames = usage.usedInTables.map((t) => t.menuName).join(", ");
|
||||
errorMessage += `\n\n다음 메뉴에서 사용 중입니다:\n${menuNames}`;
|
||||
if (usage.isUsed) {
|
||||
// 사용 중인 항목 정보 조회
|
||||
let labelQuery: string;
|
||||
let labelParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1`;
|
||||
labelParams = [id];
|
||||
} else {
|
||||
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`;
|
||||
labelParams = [id, companyCode];
|
||||
}
|
||||
|
||||
const labelResult = await pool.query(labelQuery, labelParams);
|
||||
const valueLabel = labelResult.rows[0]?.value_label || `ID:${id}`;
|
||||
|
||||
let errorMessage = `카테고리 "${valueLabel}"을(를) 삭제할 수 없습니다.\n`;
|
||||
errorMessage += `\n현재 ${usage.totalCount}개의 데이터에서 사용 중입니다.`;
|
||||
|
||||
if (usage.usedInTables.length > 0) {
|
||||
const menuNames = usage.usedInTables.map((t) => t.menuName).join(", ");
|
||||
errorMessage += `\n\n다음 메뉴에서 사용 중입니다:\n${menuNames}`;
|
||||
}
|
||||
|
||||
errorMessage += "\n\n메뉴에서 사용하는 카테고리 항목을 수정한 후 다시 삭제해주세요.";
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
errorMessage += "\n\n메뉴에서 사용하는 카테고리 항목을 수정한 후 다시 삭제해주세요.";
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 2. 하위 값 체크 (멀티테넌시 적용)
|
||||
let checkQuery: string;
|
||||
let checkParams: any[];
|
||||
// 3. 하위 카테고리부터 역순으로 삭제 (외래키 제약 회피)
|
||||
// 가장 깊은 하위부터 삭제해야 하므로 역순으로
|
||||
const reversedIds = [...allValueIds].reverse();
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 하위 값 체크
|
||||
checkQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM table_column_category_values
|
||||
WHERE parent_value_id = $1
|
||||
`;
|
||||
checkParams = [valueId];
|
||||
} else {
|
||||
// 일반 회사: 자신의 하위 값만 체크
|
||||
checkQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM table_column_category_values
|
||||
WHERE parent_value_id = $1
|
||||
AND company_code = $2
|
||||
`;
|
||||
checkParams = [valueId, companyCode];
|
||||
}
|
||||
|
||||
const checkResult = await pool.query(checkQuery, checkParams);
|
||||
|
||||
if (parseInt(checkResult.rows[0].count) > 0) {
|
||||
throw new Error("하위 카테고리 값이 있어 삭제할 수 없습니다");
|
||||
}
|
||||
|
||||
// 3. 물리적 삭제 (멀티테넌시 적용)
|
||||
let deleteQuery: string;
|
||||
let deleteParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 카테고리 값 삭제 가능
|
||||
deleteQuery = `
|
||||
DELETE FROM table_column_category_values
|
||||
WHERE value_id = $1
|
||||
`;
|
||||
deleteParams = [valueId];
|
||||
} else {
|
||||
// 일반 회사: 자신의 카테고리 값만 삭제 가능
|
||||
deleteQuery = `
|
||||
DELETE FROM table_column_category_values
|
||||
WHERE value_id = $1
|
||||
AND company_code = $2
|
||||
`;
|
||||
deleteParams = [valueId, companyCode];
|
||||
}
|
||||
|
||||
const result = await pool.query(deleteQuery, deleteParams);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
throw new Error("카테고리 값을 찾을 수 없거나 권한이 없습니다");
|
||||
for (const id of reversedIds) {
|
||||
let deleteQuery: string;
|
||||
let deleteParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1`;
|
||||
deleteParams = [id];
|
||||
} else {
|
||||
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`;
|
||||
deleteParams = [id, companyCode];
|
||||
}
|
||||
|
||||
await pool.query(deleteQuery, deleteParams);
|
||||
}
|
||||
|
||||
logger.info("카테고리 값 삭제 완료", {
|
||||
valueId,
|
||||
companyCode,
|
||||
deletedCount: allValueIds.length,
|
||||
deletedChildCount: childValueIds.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
|
||||
|
||||
Reference in New Issue
Block a user