2025-11-05 15:23:57 +09:00
|
|
|
import { getPool } from "../database/db";
|
|
|
|
|
import { logger } from "../utils/logger";
|
2025-11-11 14:32:00 +09:00
|
|
|
import { getSiblingMenuObjids } from "./menuService";
|
2025-11-05 15:23:57 +09:00
|
|
|
import {
|
|
|
|
|
TableCategoryValue,
|
|
|
|
|
CategoryColumn,
|
|
|
|
|
} from "../types/tableCategoryValue";
|
|
|
|
|
|
|
|
|
|
class TableCategoryValueService {
|
|
|
|
|
/**
|
|
|
|
|
* 테이블의 카테고리 타입 컬럼 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
async getCategoryColumns(
|
|
|
|
|
tableName: string,
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<CategoryColumn[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info("카테고리 컬럼 목록 조회", { tableName, companyCode });
|
|
|
|
|
|
|
|
|
|
const pool = getPool();
|
2025-11-06 17:01:13 +09:00
|
|
|
|
|
|
|
|
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
|
|
|
|
let query: string;
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 조회
|
|
|
|
|
query = `
|
|
|
|
|
SELECT
|
|
|
|
|
tc.table_name AS "tableName",
|
|
|
|
|
tc.column_name AS "columnName",
|
|
|
|
|
tc.column_name AS "columnLabel",
|
|
|
|
|
COUNT(cv.value_id) AS "valueCount"
|
|
|
|
|
FROM table_type_columns tc
|
|
|
|
|
LEFT JOIN table_column_category_values cv
|
|
|
|
|
ON tc.table_name = cv.table_name
|
|
|
|
|
AND tc.column_name = cv.column_name
|
|
|
|
|
AND cv.is_active = true
|
|
|
|
|
WHERE tc.table_name = $1
|
|
|
|
|
AND tc.input_type = 'category'
|
|
|
|
|
GROUP BY tc.table_name, tc.column_name, tc.display_order
|
|
|
|
|
ORDER BY tc.display_order, tc.column_name
|
|
|
|
|
`;
|
|
|
|
|
logger.info("최고 관리자 카테고리 컬럼 조회");
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 조회
|
|
|
|
|
query = `
|
|
|
|
|
SELECT
|
|
|
|
|
tc.table_name AS "tableName",
|
|
|
|
|
tc.column_name AS "columnName",
|
|
|
|
|
tc.column_name AS "columnLabel",
|
|
|
|
|
COUNT(cv.value_id) AS "valueCount"
|
|
|
|
|
FROM table_type_columns tc
|
|
|
|
|
LEFT JOIN table_column_category_values cv
|
|
|
|
|
ON tc.table_name = cv.table_name
|
|
|
|
|
AND tc.column_name = cv.column_name
|
|
|
|
|
AND cv.is_active = true
|
|
|
|
|
AND cv.company_code = $2
|
|
|
|
|
WHERE tc.table_name = $1
|
|
|
|
|
AND tc.input_type = 'category'
|
|
|
|
|
GROUP BY tc.table_name, tc.column_name, tc.display_order
|
|
|
|
|
ORDER BY tc.display_order, tc.column_name
|
|
|
|
|
`;
|
|
|
|
|
logger.info("회사별 카테고리 컬럼 조회", { companyCode });
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
2025-11-11 10:29:47 +09:00
|
|
|
// 쿼리 파라미터는 company_code에 따라 다름
|
|
|
|
|
const params = companyCode === "*" ? [tableName] : [tableName, companyCode];
|
|
|
|
|
const result = await pool.query(query, params);
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
logger.info(`카테고리 컬럼 ${result.rows.length}개 조회 완료`, {
|
|
|
|
|
tableName,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result.rows;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 컬럼 조회 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-11 14:32:00 +09:00
|
|
|
* 특정 컬럼의 카테고리 값 목록 조회 (메뉴 스코프)
|
|
|
|
|
*
|
|
|
|
|
* 메뉴 스코프 규칙:
|
|
|
|
|
* - menuObjid가 제공되면 해당 메뉴와 형제 메뉴의 카테고리 값을 조회
|
|
|
|
|
* - menuObjid가 없으면 테이블 스코프로 동작 (하위 호환성)
|
2025-11-05 15:23:57 +09:00
|
|
|
*/
|
|
|
|
|
async getCategoryValues(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string,
|
|
|
|
|
companyCode: string,
|
2025-11-11 14:32:00 +09:00
|
|
|
includeInactive: boolean = false,
|
|
|
|
|
menuObjid?: number
|
2025-11-05 15:23:57 +09:00
|
|
|
): Promise<TableCategoryValue[]> {
|
|
|
|
|
try {
|
2025-11-11 14:32:00 +09:00
|
|
|
logger.info("카테고리 값 목록 조회 (메뉴 스코프)", {
|
2025-11-05 15:23:57 +09:00
|
|
|
tableName,
|
|
|
|
|
columnName,
|
|
|
|
|
companyCode,
|
|
|
|
|
includeInactive,
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid,
|
2025-11-05 15:23:57 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pool = getPool();
|
2025-11-06 17:01:13 +09:00
|
|
|
|
2025-11-11 14:32:00 +09:00
|
|
|
// 1. 메뉴 스코프: 형제 메뉴 OBJID 조회
|
|
|
|
|
let siblingObjids: number[] = [];
|
|
|
|
|
if (menuObjid) {
|
|
|
|
|
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
|
|
|
|
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 카테고리 값 조회 (형제 메뉴 포함)
|
2025-11-06 17:01:13 +09:00
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 조회
|
2025-11-12 18:02:17 +09:00
|
|
|
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
|
|
|
|
query = `
|
|
|
|
|
SELECT
|
|
|
|
|
value_id AS "valueId",
|
|
|
|
|
table_name AS "tableName",
|
|
|
|
|
column_name AS "columnName",
|
|
|
|
|
value_code AS "valueCode",
|
|
|
|
|
value_label AS "valueLabel",
|
|
|
|
|
value_order AS "valueOrder",
|
|
|
|
|
parent_value_id AS "parentValueId",
|
|
|
|
|
depth,
|
|
|
|
|
description,
|
|
|
|
|
color,
|
|
|
|
|
icon,
|
|
|
|
|
is_active AS "isActive",
|
|
|
|
|
is_default AS "isDefault",
|
|
|
|
|
company_code AS "companyCode",
|
|
|
|
|
menu_objid AS "menuObjid",
|
|
|
|
|
created_at AS "createdAt",
|
|
|
|
|
updated_at AS "updatedAt",
|
|
|
|
|
created_by AS "createdBy",
|
|
|
|
|
updated_by AS "updatedBy"
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
AND column_name = $2
|
|
|
|
|
`;
|
|
|
|
|
params = [tableName, columnName];
|
2025-11-06 17:01:13 +09:00
|
|
|
logger.info("최고 관리자 카테고리 값 조회");
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 조회
|
2025-11-12 18:02:17 +09:00
|
|
|
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
|
|
|
|
query = `
|
|
|
|
|
SELECT
|
|
|
|
|
value_id AS "valueId",
|
|
|
|
|
table_name AS "tableName",
|
|
|
|
|
column_name AS "columnName",
|
|
|
|
|
value_code AS "valueCode",
|
|
|
|
|
value_label AS "valueLabel",
|
|
|
|
|
value_order AS "valueOrder",
|
|
|
|
|
parent_value_id AS "parentValueId",
|
|
|
|
|
depth,
|
|
|
|
|
description,
|
|
|
|
|
color,
|
|
|
|
|
icon,
|
|
|
|
|
is_active AS "isActive",
|
|
|
|
|
is_default AS "isDefault",
|
|
|
|
|
company_code AS "companyCode",
|
|
|
|
|
menu_objid AS "menuObjid",
|
|
|
|
|
created_at AS "createdAt",
|
|
|
|
|
updated_at AS "updatedAt",
|
|
|
|
|
created_by AS "createdBy",
|
|
|
|
|
updated_by AS "updatedBy"
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
AND column_name = $2
|
|
|
|
|
AND company_code = $3
|
|
|
|
|
`;
|
|
|
|
|
params = [tableName, columnName, companyCode];
|
2025-11-06 17:01:13 +09:00
|
|
|
logger.info("회사별 카테고리 값 조회", { companyCode });
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
if (!includeInactive) {
|
|
|
|
|
query += ` AND is_active = true`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += ` ORDER BY value_order, value_label`;
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
// 계층 구조로 변환
|
|
|
|
|
const values = this.buildHierarchy(result.rows);
|
|
|
|
|
|
|
|
|
|
logger.info(`카테고리 값 ${result.rows.length}개 조회 완료`, {
|
|
|
|
|
tableName,
|
|
|
|
|
columnName,
|
2025-11-06 17:01:13 +09:00
|
|
|
companyCode,
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid,
|
|
|
|
|
scopeType: menuObjid ? "menu" : "table",
|
2025-11-05 15:23:57 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return values;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 값 조회 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-11 14:32:00 +09:00
|
|
|
* 카테고리 값 추가 (메뉴 스코프)
|
|
|
|
|
*
|
|
|
|
|
* @param value 카테고리 값 정보
|
|
|
|
|
* @param companyCode 회사 코드
|
|
|
|
|
* @param userId 생성자 ID
|
|
|
|
|
* @param menuObjid 메뉴 OBJID (필수)
|
2025-11-05 15:23:57 +09:00
|
|
|
*/
|
|
|
|
|
async addCategoryValue(
|
|
|
|
|
value: TableCategoryValue,
|
|
|
|
|
companyCode: string,
|
2025-11-11 14:32:00 +09:00
|
|
|
userId: string,
|
|
|
|
|
menuObjid: number
|
2025-11-05 15:23:57 +09:00
|
|
|
): Promise<TableCategoryValue> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-11 14:32:00 +09:00
|
|
|
logger.info("카테고리 값 추가 (메뉴 스코프)", {
|
|
|
|
|
tableName: value.tableName,
|
|
|
|
|
columnName: value.columnName,
|
|
|
|
|
valueCode: value.valueCode,
|
|
|
|
|
menuObjid,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 중복 코드 체크 (멀티테넌시 + 메뉴 스코프)
|
2025-11-06 17:01:13 +09:00
|
|
|
let duplicateQuery: string;
|
|
|
|
|
let duplicateParams: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 회사에서 중복 체크
|
|
|
|
|
duplicateQuery = `
|
|
|
|
|
SELECT value_id
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
AND column_name = $2
|
|
|
|
|
AND value_code = $3
|
2025-11-11 14:32:00 +09:00
|
|
|
AND menu_objid = $4
|
2025-11-06 17:01:13 +09:00
|
|
|
`;
|
2025-11-11 14:32:00 +09:00
|
|
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
|
2025-11-06 17:01:13 +09:00
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 회사에서만 중복 체크
|
|
|
|
|
duplicateQuery = `
|
|
|
|
|
SELECT value_id
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
AND column_name = $2
|
|
|
|
|
AND value_code = $3
|
2025-11-11 14:32:00 +09:00
|
|
|
AND menu_objid = $4
|
|
|
|
|
AND company_code = $5
|
2025-11-06 17:01:13 +09:00
|
|
|
`;
|
2025-11-11 14:32:00 +09:00
|
|
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
|
2025-11-06 17:01:13 +09:00
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
if (duplicateResult.rows.length > 0) {
|
|
|
|
|
throw new Error("이미 존재하는 코드입니다");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const insertQuery = `
|
|
|
|
|
INSERT INTO table_column_category_values (
|
|
|
|
|
table_name, column_name, value_code, value_label, value_order,
|
|
|
|
|
parent_value_id, depth, description, color, icon,
|
2025-11-12 18:02:17 +09:00
|
|
|
is_active, is_default, company_code, menu_objid, created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
2025-11-05 15:23:57 +09:00
|
|
|
RETURNING
|
|
|
|
|
value_id AS "valueId",
|
|
|
|
|
table_name AS "tableName",
|
|
|
|
|
column_name AS "columnName",
|
|
|
|
|
value_code AS "valueCode",
|
|
|
|
|
value_label AS "valueLabel",
|
|
|
|
|
value_order AS "valueOrder",
|
|
|
|
|
parent_value_id AS "parentValueId",
|
|
|
|
|
depth,
|
|
|
|
|
description,
|
|
|
|
|
color,
|
|
|
|
|
icon,
|
|
|
|
|
is_active AS "isActive",
|
|
|
|
|
is_default AS "isDefault",
|
|
|
|
|
company_code AS "companyCode",
|
2025-11-11 14:32:00 +09:00
|
|
|
menu_objid AS "menuObjid",
|
2025-11-05 15:23:57 +09:00
|
|
|
created_at AS "createdAt",
|
|
|
|
|
created_by AS "createdBy"
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(insertQuery, [
|
|
|
|
|
value.tableName,
|
|
|
|
|
value.columnName,
|
|
|
|
|
value.valueCode,
|
|
|
|
|
value.valueLabel,
|
|
|
|
|
value.valueOrder || 0,
|
|
|
|
|
value.parentValueId || null,
|
|
|
|
|
value.depth || 1,
|
|
|
|
|
value.description || null,
|
|
|
|
|
value.color || null,
|
|
|
|
|
value.icon || null,
|
|
|
|
|
value.isActive !== false,
|
|
|
|
|
value.isDefault || false,
|
|
|
|
|
companyCode,
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid, // ← 메뉴 OBJID 저장
|
2025-11-05 15:23:57 +09:00
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
logger.info("카테고리 값 추가 완료", {
|
|
|
|
|
valueId: result.rows[0].valueId,
|
|
|
|
|
tableName: value.tableName,
|
|
|
|
|
columnName: value.columnName,
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid,
|
2025-11-05 15:23:57 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 값 추가 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 카테고리 값 수정
|
|
|
|
|
*/
|
|
|
|
|
async updateCategoryValue(
|
|
|
|
|
valueId: number,
|
|
|
|
|
updates: Partial<TableCategoryValue>,
|
|
|
|
|
companyCode: string,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<TableCategoryValue> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const setClauses: string[] = [];
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (updates.valueLabel !== undefined) {
|
|
|
|
|
setClauses.push(`value_label = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.valueLabel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.valueOrder !== undefined) {
|
|
|
|
|
setClauses.push(`value_order = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.valueOrder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.description !== undefined) {
|
|
|
|
|
setClauses.push(`description = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.description);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.color !== undefined) {
|
|
|
|
|
setClauses.push(`color = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.icon !== undefined) {
|
|
|
|
|
setClauses.push(`icon = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.icon);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.isActive !== undefined) {
|
|
|
|
|
setClauses.push(`is_active = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.isActive);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updates.isDefault !== undefined) {
|
|
|
|
|
setClauses.push(`is_default = $${paramIndex++}`);
|
|
|
|
|
values.push(updates.isDefault);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setClauses.push(`updated_at = NOW()`);
|
|
|
|
|
setClauses.push(`updated_by = $${paramIndex++}`);
|
|
|
|
|
values.push(userId);
|
|
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
// 멀티테넌시: 최고 관리자는 company_code 조건 제외
|
|
|
|
|
let updateQuery: string;
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 수정 가능
|
|
|
|
|
values.push(valueId);
|
|
|
|
|
updateQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET ${setClauses.join(", ")}
|
|
|
|
|
WHERE value_id = $${paramIndex++}
|
|
|
|
|
RETURNING
|
|
|
|
|
value_id AS "valueId",
|
|
|
|
|
table_name AS "tableName",
|
|
|
|
|
column_name AS "columnName",
|
|
|
|
|
value_code AS "valueCode",
|
|
|
|
|
value_label AS "valueLabel",
|
|
|
|
|
value_order AS "valueOrder",
|
|
|
|
|
description,
|
|
|
|
|
color,
|
|
|
|
|
icon,
|
|
|
|
|
is_active AS "isActive",
|
|
|
|
|
is_default AS "isDefault",
|
|
|
|
|
company_code AS "companyCode",
|
|
|
|
|
updated_at AS "updatedAt",
|
|
|
|
|
updated_by AS "updatedBy"
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 수정 가능
|
|
|
|
|
values.push(valueId, companyCode);
|
|
|
|
|
updateQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET ${setClauses.join(", ")}
|
|
|
|
|
WHERE value_id = $${paramIndex++}
|
|
|
|
|
AND company_code = $${paramIndex++}
|
|
|
|
|
RETURNING
|
|
|
|
|
value_id AS "valueId",
|
|
|
|
|
table_name AS "tableName",
|
|
|
|
|
column_name AS "columnName",
|
|
|
|
|
value_code AS "valueCode",
|
|
|
|
|
value_label AS "valueLabel",
|
|
|
|
|
value_order AS "valueOrder",
|
|
|
|
|
description,
|
|
|
|
|
color,
|
|
|
|
|
icon,
|
|
|
|
|
is_active AS "isActive",
|
|
|
|
|
is_default AS "isDefault",
|
|
|
|
|
company_code AS "companyCode",
|
|
|
|
|
updated_at AS "updatedAt",
|
|
|
|
|
updated_by AS "updatedBy"
|
|
|
|
|
`;
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
const result = await pool.query(updateQuery, values);
|
|
|
|
|
|
|
|
|
|
if (result.rowCount === 0) {
|
|
|
|
|
throw new Error("카테고리 값을 찾을 수 없습니다");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("카테고리 값 수정 완료", { valueId, companyCode });
|
|
|
|
|
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 값 수정 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 카테고리 값 삭제 (비활성화)
|
|
|
|
|
*/
|
|
|
|
|
async deleteCategoryValue(
|
|
|
|
|
valueId: number,
|
|
|
|
|
companyCode: string,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-06 17:01:13 +09:00
|
|
|
// 하위 값 체크 (멀티테넌시 적용)
|
|
|
|
|
let checkQuery: string;
|
|
|
|
|
let checkParams: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 하위 값 체크
|
|
|
|
|
checkQuery = `
|
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE parent_value_id = $1
|
|
|
|
|
AND is_active = true
|
|
|
|
|
`;
|
|
|
|
|
checkParams = [valueId];
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 하위 값만 체크
|
|
|
|
|
checkQuery = `
|
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
|
FROM table_column_category_values
|
|
|
|
|
WHERE parent_value_id = $1
|
|
|
|
|
AND company_code = $2
|
|
|
|
|
AND is_active = true
|
|
|
|
|
`;
|
|
|
|
|
checkParams = [valueId, companyCode];
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
const checkResult = await pool.query(checkQuery, checkParams);
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
if (parseInt(checkResult.rows[0].count) > 0) {
|
|
|
|
|
throw new Error("하위 카테고리 값이 있어 삭제할 수 없습니다");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
// 비활성화 (멀티테넌시 적용)
|
|
|
|
|
let deleteQuery: string;
|
|
|
|
|
let deleteParams: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 삭제 가능
|
|
|
|
|
deleteQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET is_active = false, updated_at = NOW(), updated_by = $2
|
|
|
|
|
WHERE value_id = $1
|
|
|
|
|
`;
|
|
|
|
|
deleteParams = [valueId, userId];
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 삭제 가능
|
|
|
|
|
deleteQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET is_active = false, updated_at = NOW(), updated_by = $3
|
|
|
|
|
WHERE value_id = $1
|
|
|
|
|
AND company_code = $2
|
|
|
|
|
`;
|
|
|
|
|
deleteParams = [valueId, companyCode, userId];
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
const result = await pool.query(deleteQuery, deleteParams);
|
|
|
|
|
|
|
|
|
|
if (result.rowCount === 0) {
|
|
|
|
|
throw new Error("카테고리 값을 찾을 수 없거나 권한이 없습니다");
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
logger.info("카테고리 값 삭제(비활성화) 완료", {
|
|
|
|
|
valueId,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 카테고리 값 일괄 삭제
|
|
|
|
|
*/
|
|
|
|
|
async bulkDeleteCategoryValues(
|
|
|
|
|
valueIds: number[],
|
|
|
|
|
companyCode: string,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-06 17:01:13 +09:00
|
|
|
// 멀티테넌시 적용
|
|
|
|
|
let deleteQuery: string;
|
|
|
|
|
let deleteParams: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 일괄 삭제 가능
|
|
|
|
|
deleteQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET is_active = false, updated_at = NOW(), updated_by = $2
|
|
|
|
|
WHERE value_id = ANY($1::int[])
|
|
|
|
|
`;
|
|
|
|
|
deleteParams = [valueIds, userId];
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능
|
|
|
|
|
deleteQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET is_active = false, updated_at = NOW(), updated_by = $3
|
|
|
|
|
WHERE value_id = ANY($1::int[])
|
|
|
|
|
AND company_code = $2
|
|
|
|
|
`;
|
|
|
|
|
deleteParams = [valueIds, companyCode, userId];
|
|
|
|
|
}
|
2025-11-05 15:23:57 +09:00
|
|
|
|
2025-11-06 17:01:13 +09:00
|
|
|
await pool.query(deleteQuery, deleteParams);
|
2025-11-05 15:23:57 +09:00
|
|
|
|
|
|
|
|
logger.info("카테고리 값 일괄 삭제 완료", {
|
|
|
|
|
count: valueIds.length,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error(`카테고리 값 일괄 삭제 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 카테고리 값 순서 변경
|
|
|
|
|
*/
|
|
|
|
|
async reorderCategoryValues(
|
|
|
|
|
orderedValueIds: number[],
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
const client = await pool.connect();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await client.query("BEGIN");
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < orderedValueIds.length; i++) {
|
2025-11-06 17:01:13 +09:00
|
|
|
// 멀티테넌시 적용
|
|
|
|
|
let updateQuery: string;
|
|
|
|
|
let updateParams: any[];
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 카테고리 값 순서 변경 가능
|
|
|
|
|
updateQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET value_order = $1, updated_at = NOW()
|
|
|
|
|
WHERE value_id = $2
|
|
|
|
|
`;
|
|
|
|
|
updateParams = [i + 1, orderedValueIds[i]];
|
|
|
|
|
} else {
|
|
|
|
|
// 일반 회사: 자신의 카테고리 값만 순서 변경 가능
|
|
|
|
|
updateQuery = `
|
|
|
|
|
UPDATE table_column_category_values
|
|
|
|
|
SET value_order = $1, updated_at = NOW()
|
|
|
|
|
WHERE value_id = $2
|
|
|
|
|
AND company_code = $3
|
|
|
|
|
`;
|
|
|
|
|
updateParams = [i + 1, orderedValueIds[i], companyCode];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await client.query(updateQuery, updateParams);
|
2025-11-05 15:23:57 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await client.query("COMMIT");
|
|
|
|
|
|
|
|
|
|
logger.info("카테고리 값 순서 변경 완료", {
|
|
|
|
|
count: orderedValueIds.length,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
await client.query("ROLLBACK");
|
|
|
|
|
logger.error(`카테고리 값 순서 변경 실패: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
client.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 계층 구조 변환 헬퍼
|
|
|
|
|
*/
|
|
|
|
|
private buildHierarchy(
|
|
|
|
|
values: TableCategoryValue[],
|
|
|
|
|
parentId: number | null = null
|
|
|
|
|
): TableCategoryValue[] {
|
|
|
|
|
return values
|
|
|
|
|
.filter((v) => v.parentValueId === parentId)
|
|
|
|
|
.map((v) => ({
|
|
|
|
|
...v,
|
|
|
|
|
children: this.buildHierarchy(values, v.valueId!),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default new TableCategoryValueService();
|