1615 lines
51 KiB
TypeScript
1615 lines
51 KiB
TypeScript
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
import { getSiblingMenuObjids } from "./menuService";
|
|
import {
|
|
TableCategoryValue,
|
|
CategoryColumn,
|
|
} from "../types/tableCategoryValue";
|
|
|
|
class TableCategoryValueService {
|
|
/**
|
|
* 테이블의 카테고리 타입 컬럼 목록 조회
|
|
*/
|
|
async getCategoryColumns(
|
|
tableName: string,
|
|
companyCode: string
|
|
): Promise<CategoryColumn[]> {
|
|
try {
|
|
logger.info("카테고리 컬럼 목록 조회", { tableName, companyCode });
|
|
|
|
const pool = getPool();
|
|
|
|
// 멀티테넌시: 최고 관리자만 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 });
|
|
}
|
|
|
|
// 쿼리 파라미터는 company_code에 따라 다름
|
|
const params = companyCode === "*" ? [tableName] : [tableName, companyCode];
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.info(`카테고리 컬럼 ${result.rows.length}개 조회 완료`, {
|
|
tableName,
|
|
companyCode,
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 컬럼 조회 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 테이블의 카테고리 컬럼 목록 조회 (Select 옵션 설정용)
|
|
* 테이블 선택 없이 등록된 모든 카테고리 컬럼을 조회합니다.
|
|
*/
|
|
async getAllCategoryColumns(
|
|
companyCode: string
|
|
): Promise<CategoryColumn[]> {
|
|
try {
|
|
logger.info("전체 카테고리 컬럼 목록 조회", { companyCode });
|
|
|
|
const pool = getPool();
|
|
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 카테고리 컬럼 조회 (중복 제거)
|
|
query = `
|
|
SELECT
|
|
tc.table_name AS "tableName",
|
|
tc.column_name AS "columnName",
|
|
tc.column_name AS "columnLabel",
|
|
COALESCE(cv_count.cnt, 0) AS "valueCount"
|
|
FROM (
|
|
SELECT DISTINCT table_name, column_name, MIN(display_order) as display_order
|
|
FROM table_type_columns
|
|
WHERE input_type = 'category'
|
|
GROUP BY table_name, column_name
|
|
) tc
|
|
LEFT JOIN (
|
|
SELECT table_name, column_name, COUNT(*) as cnt
|
|
FROM table_column_category_values
|
|
WHERE is_active = true
|
|
GROUP BY table_name, column_name
|
|
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
|
ORDER BY tc.table_name, tc.display_order, tc.column_name
|
|
`;
|
|
params = [];
|
|
} else {
|
|
// 일반 회사: 자신의 카테고리 값만 카운트 (중복 제거)
|
|
query = `
|
|
SELECT
|
|
tc.table_name AS "tableName",
|
|
tc.column_name AS "columnName",
|
|
tc.column_name AS "columnLabel",
|
|
COALESCE(cv_count.cnt, 0) AS "valueCount"
|
|
FROM (
|
|
SELECT DISTINCT table_name, column_name, MIN(display_order) as display_order
|
|
FROM table_type_columns
|
|
WHERE input_type = 'category'
|
|
GROUP BY table_name, column_name
|
|
) tc
|
|
LEFT JOIN (
|
|
SELECT table_name, column_name, COUNT(*) as cnt
|
|
FROM table_column_category_values
|
|
WHERE is_active = true AND company_code = $1
|
|
GROUP BY table_name, column_name
|
|
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
|
ORDER BY tc.table_name, tc.display_order, tc.column_name
|
|
`;
|
|
params = [companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.info(`전체 카테고리 컬럼 ${result.rows.length}개 조회 완료`, {
|
|
companyCode,
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error(`전체 카테고리 컬럼 조회 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 컬럼의 카테고리 값 목록 조회 (메뉴 스코프)
|
|
*
|
|
* 메뉴 스코프 규칙:
|
|
* - menuObjid가 제공되면 해당 메뉴와 형제 메뉴의 카테고리 값을 조회
|
|
* - menuObjid가 없으면 테이블 스코프로 동작 (하위 호환성)
|
|
*/
|
|
async getCategoryValues(
|
|
tableName: string,
|
|
columnName: string,
|
|
companyCode: string,
|
|
includeInactive: boolean = false,
|
|
menuObjid?: number
|
|
): Promise<TableCategoryValue[]> {
|
|
try {
|
|
logger.info("카테고리 값 목록 조회 (메뉴 스코프)", {
|
|
tableName,
|
|
columnName,
|
|
companyCode,
|
|
includeInactive,
|
|
menuObjid,
|
|
});
|
|
|
|
const pool = getPool();
|
|
|
|
// 1. 메뉴 스코프: 형제 메뉴 OBJID 조회
|
|
let siblingObjids: number[] = [];
|
|
if (menuObjid) {
|
|
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
|
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
|
}
|
|
|
|
// 2. 카테고리 값 조회 (메뉴 스코프 또는 형제 메뉴 포함)
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
const baseSelect = `
|
|
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
|
|
`;
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: menuObjid가 있으면 해당 메뉴(및 형제 메뉴)의 값만 조회
|
|
if (menuObjid && siblingObjids.length > 0) {
|
|
query = baseSelect + ` AND menu_objid = ANY($3::numeric[])`;
|
|
params = [tableName, columnName, siblingObjids];
|
|
logger.info("최고 관리자 메뉴 스코프 카테고리 값 조회", { menuObjid, siblingObjids });
|
|
} else if (menuObjid) {
|
|
query = baseSelect + ` AND menu_objid = $3`;
|
|
params = [tableName, columnName, menuObjid];
|
|
logger.info("최고 관리자 단일 메뉴 카테고리 값 조회", { menuObjid });
|
|
} else {
|
|
// menuObjid 없으면 모든 값 조회 (중복 가능)
|
|
query = baseSelect;
|
|
params = [tableName, columnName];
|
|
logger.info("최고 관리자 전체 카테고리 값 조회 (menuObjid 없음)");
|
|
}
|
|
} else {
|
|
// 일반 회사: 자신의 회사 + menuObjid로 필터링
|
|
if (menuObjid && siblingObjids.length > 0) {
|
|
query = baseSelect + ` AND company_code = $3 AND menu_objid = ANY($4::numeric[])`;
|
|
params = [tableName, columnName, companyCode, siblingObjids];
|
|
logger.info("회사별 메뉴 스코프 카테고리 값 조회", { companyCode, menuObjid, siblingObjids });
|
|
} else if (menuObjid) {
|
|
query = baseSelect + ` AND company_code = $3 AND menu_objid = $4`;
|
|
params = [tableName, columnName, companyCode, menuObjid];
|
|
logger.info("회사별 단일 메뉴 카테고리 값 조회", { companyCode, menuObjid });
|
|
} else {
|
|
// menuObjid 없으면 회사 전체 조회 (중복 가능하지만 회사별로 제한)
|
|
query = baseSelect + ` AND company_code = $3`;
|
|
params = [tableName, columnName, companyCode];
|
|
logger.info("회사별 카테고리 값 조회 (menuObjid 없음)", { companyCode });
|
|
}
|
|
}
|
|
|
|
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,
|
|
companyCode,
|
|
menuObjid,
|
|
scopeType: menuObjid ? "menu" : "table",
|
|
});
|
|
|
|
return values;
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 값 조회 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리 값 추가 (메뉴 스코프)
|
|
*
|
|
* @param value 카테고리 값 정보
|
|
* @param companyCode 회사 코드
|
|
* @param userId 생성자 ID
|
|
* @param menuObjid 메뉴 OBJID (필수)
|
|
*/
|
|
async addCategoryValue(
|
|
value: TableCategoryValue,
|
|
companyCode: string,
|
|
userId: string,
|
|
menuObjid: number
|
|
): Promise<TableCategoryValue> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("카테고리 값 추가 (메뉴 스코프)", {
|
|
tableName: value.tableName,
|
|
columnName: value.columnName,
|
|
valueCode: value.valueCode,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
// 중복 코드 체크 (멀티테넌시 + 메뉴 스코프)
|
|
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
|
|
AND menu_objid = $4
|
|
`;
|
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
|
|
} else {
|
|
// 일반 회사: 자신의 회사에서만 중복 체크
|
|
duplicateQuery = `
|
|
SELECT value_id
|
|
FROM table_column_category_values
|
|
WHERE table_name = $1
|
|
AND column_name = $2
|
|
AND value_code = $3
|
|
AND menu_objid = $4
|
|
AND company_code = $5
|
|
`;
|
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
|
|
}
|
|
|
|
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
|
|
|
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,
|
|
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)
|
|
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",
|
|
menu_objid AS "menuObjid",
|
|
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,
|
|
menuObjid, // ← 메뉴 OBJID 저장
|
|
userId,
|
|
]);
|
|
|
|
logger.info("카테고리 값 추가 완료", {
|
|
valueId: result.rows[0].valueId,
|
|
tableName: value.tableName,
|
|
columnName: value.columnName,
|
|
menuObjid,
|
|
});
|
|
|
|
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);
|
|
|
|
// 멀티테넌시: 최고 관리자는 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"
|
|
`;
|
|
}
|
|
|
|
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 checkCategoryValueUsage(
|
|
valueId: number,
|
|
companyCode: string
|
|
): Promise<{ isUsed: boolean; usedInTables: any[]; totalCount: number }> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("카테고리 값 사용 여부 확인", { valueId, companyCode });
|
|
|
|
// 1. 카테고리 값 정보 조회
|
|
let valueQuery: string;
|
|
let valueParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
valueQuery = `
|
|
SELECT table_name, column_name, value_code
|
|
FROM table_column_category_values
|
|
WHERE value_id = $1
|
|
`;
|
|
valueParams = [valueId];
|
|
} else {
|
|
valueQuery = `
|
|
SELECT table_name, column_name, value_code
|
|
FROM table_column_category_values
|
|
WHERE value_id = $1
|
|
AND company_code = $2
|
|
`;
|
|
valueParams = [valueId, companyCode];
|
|
}
|
|
|
|
const valueResult = await pool.query(valueQuery, valueParams);
|
|
|
|
if (valueResult.rowCount === 0) {
|
|
throw new Error("카테고리 값을 찾을 수 없습니다");
|
|
}
|
|
|
|
const { table_name, column_name, value_code } = valueResult.rows[0];
|
|
|
|
// 2. 실제 데이터 테이블에서 사용 여부 확인
|
|
// 테이블이 존재하는지 먼저 확인
|
|
const tableExistsQuery = `
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = $1
|
|
) as exists
|
|
`;
|
|
|
|
const tableExistsResult = await pool.query(tableExistsQuery, [table_name]);
|
|
|
|
if (!tableExistsResult.rows[0].exists) {
|
|
logger.info("테이블이 존재하지 않음", { table_name });
|
|
return { isUsed: false, usedInTables: [], totalCount: 0 };
|
|
}
|
|
|
|
// 3. 해당 테이블에서 value_code를 사용하는 데이터 개수 확인
|
|
let dataCountQuery: string;
|
|
let dataCountParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
dataCountQuery = `
|
|
SELECT COUNT(*) as count
|
|
FROM ${table_name}
|
|
WHERE ${column_name} = $1
|
|
`;
|
|
dataCountParams = [value_code];
|
|
} else {
|
|
dataCountQuery = `
|
|
SELECT COUNT(*) as count
|
|
FROM ${table_name}
|
|
WHERE ${column_name} = $1
|
|
AND company_code = $2
|
|
`;
|
|
dataCountParams = [value_code, companyCode];
|
|
}
|
|
|
|
const dataCountResult = await pool.query(dataCountQuery, dataCountParams);
|
|
const totalCount = parseInt(dataCountResult.rows[0].count);
|
|
const isUsed = totalCount > 0;
|
|
|
|
// 4. 사용 중인 메뉴 목록 조회 (해당 테이블을 사용하는 화면/메뉴)
|
|
const menuQuery = `
|
|
SELECT DISTINCT
|
|
mi.objid as menu_objid,
|
|
mi.menu_name_kor as menu_name,
|
|
mi.menu_url
|
|
FROM menu_info mi
|
|
INNER JOIN screen_menu_assignments sma ON sma.menu_objid = mi.objid
|
|
INNER JOIN screen_definitions sd ON sd.screen_id = sma.screen_id
|
|
WHERE sd.table_name = $1
|
|
AND mi.company_code = $2
|
|
ORDER BY mi.menu_name_kor
|
|
`;
|
|
|
|
const menuResult = await pool.query(menuQuery, [table_name, companyCode]);
|
|
|
|
const usedInTables = menuResult.rows.map((row) => ({
|
|
menuObjid: row.menu_objid,
|
|
menuName: row.menu_name,
|
|
menuUrl: row.menu_url,
|
|
tableName: table_name,
|
|
columnName: column_name,
|
|
}));
|
|
|
|
logger.info("카테고리 값 사용 여부 확인 완료", {
|
|
valueId,
|
|
isUsed,
|
|
totalCount,
|
|
usedInMenusCount: usedInTables.length,
|
|
});
|
|
|
|
return { isUsed, usedInTables, totalCount };
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 값 사용 여부 확인 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리 값 삭제 (물리적 삭제)
|
|
*/
|
|
async deleteCategoryValue(
|
|
valueId: number,
|
|
companyCode: string,
|
|
userId: string
|
|
): Promise<void> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
// 1. 사용 여부 확인
|
|
const usage = await this.checkCategoryValueUsage(valueId, companyCode);
|
|
|
|
if (usage.isUsed) {
|
|
let errorMessage = "이 카테고리 값을 삭제할 수 없습니다.\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);
|
|
}
|
|
|
|
// 2. 하위 값 체크 (멀티테넌시 적용)
|
|
let checkQuery: string;
|
|
let checkParams: any[];
|
|
|
|
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("카테고리 값을 찾을 수 없거나 권한이 없습니다");
|
|
}
|
|
|
|
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 {
|
|
// 멀티테넌시 적용
|
|
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];
|
|
}
|
|
|
|
await pool.query(deleteQuery, deleteParams);
|
|
|
|
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++) {
|
|
// 멀티테넌시 적용
|
|
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);
|
|
}
|
|
|
|
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!),
|
|
}));
|
|
}
|
|
|
|
// ================================================
|
|
// 컬럼 매핑 관련 메서드 (논리명 ↔ 물리명)
|
|
// ================================================
|
|
|
|
/**
|
|
* 컬럼 매핑 조회
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param menuObjid - 메뉴 OBJID
|
|
* @param companyCode - 회사 코드
|
|
* @returns { logical_column: physical_column } 형태의 매핑 객체
|
|
*/
|
|
async getColumnMapping(
|
|
tableName: string,
|
|
menuObjid: number,
|
|
companyCode: string
|
|
): Promise<Record<string, string>> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("컬럼 매핑 조회", { tableName, menuObjid, companyCode });
|
|
|
|
// 멀티테넌시 적용
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 매핑 조회 가능
|
|
query = `
|
|
SELECT
|
|
logical_column_name AS "logicalColumnName",
|
|
physical_column_name AS "physicalColumnName"
|
|
FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND menu_objid = $2
|
|
`;
|
|
params = [tableName, menuObjid];
|
|
} else {
|
|
// 일반 회사: 자신의 매핑만 조회
|
|
query = `
|
|
SELECT
|
|
logical_column_name AS "logicalColumnName",
|
|
physical_column_name AS "physicalColumnName"
|
|
FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND menu_objid = $2
|
|
AND company_code = $3
|
|
`;
|
|
params = [tableName, menuObjid, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
// { logical_column: physical_column } 형태로 변환
|
|
const mapping: Record<string, string> = {};
|
|
result.rows.forEach((row: any) => {
|
|
mapping[row.logicalColumnName] = row.physicalColumnName;
|
|
});
|
|
|
|
logger.info(`컬럼 매핑 ${Object.keys(mapping).length}개 조회 완료`, {
|
|
tableName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
return mapping;
|
|
} catch (error: any) {
|
|
logger.error(`컬럼 매핑 조회 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 매핑 생성/수정
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param logicalColumnName - 논리적 컬럼명
|
|
* @param physicalColumnName - 물리적 컬럼명
|
|
* @param menuObjid - 메뉴 OBJID
|
|
* @param companyCode - 회사 코드
|
|
* @param userId - 사용자 ID
|
|
* @param description - 설명 (선택사항)
|
|
*/
|
|
async createColumnMapping(
|
|
tableName: string,
|
|
logicalColumnName: string,
|
|
physicalColumnName: string,
|
|
menuObjid: number,
|
|
companyCode: string,
|
|
userId: string,
|
|
description?: string
|
|
): Promise<any> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("컬럼 매핑 생성", {
|
|
tableName,
|
|
logicalColumnName,
|
|
physicalColumnName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
// 1. 물리적 컬럼이 실제로 존재하는지 확인
|
|
const columnCheckQuery = `
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name = $1
|
|
AND column_name = $2
|
|
`;
|
|
|
|
const columnCheck = await pool.query(columnCheckQuery, [
|
|
tableName,
|
|
physicalColumnName,
|
|
]);
|
|
|
|
if (columnCheck.rowCount === 0) {
|
|
throw new Error(
|
|
`테이블 ${tableName}에 컬럼 ${physicalColumnName}이(가) 존재하지 않습니다`
|
|
);
|
|
}
|
|
|
|
// 2. 매핑 저장 (UPSERT)
|
|
const insertQuery = `
|
|
INSERT INTO category_column_mapping (
|
|
table_name,
|
|
logical_column_name,
|
|
physical_column_name,
|
|
menu_objid,
|
|
company_code,
|
|
description,
|
|
created_by,
|
|
updated_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
ON CONFLICT (table_name, logical_column_name, menu_objid, company_code)
|
|
DO UPDATE SET
|
|
physical_column_name = EXCLUDED.physical_column_name,
|
|
description = EXCLUDED.description,
|
|
updated_at = NOW(),
|
|
updated_by = EXCLUDED.updated_by
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await pool.query(insertQuery, [
|
|
tableName,
|
|
logicalColumnName,
|
|
physicalColumnName,
|
|
menuObjid,
|
|
companyCode,
|
|
description || null,
|
|
userId,
|
|
userId,
|
|
]);
|
|
|
|
logger.info("컬럼 매핑 생성 완료", {
|
|
mappingId: result.rows[0].mapping_id,
|
|
tableName,
|
|
logicalColumnName,
|
|
physicalColumnName,
|
|
});
|
|
|
|
return result.rows[0];
|
|
} catch (error: any) {
|
|
logger.error(`컬럼 매핑 생성 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 논리적 컬럼 목록 조회
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param menuObjid - 메뉴 OBJID
|
|
* @param companyCode - 회사 코드
|
|
* @returns 논리적 컬럼 목록
|
|
*/
|
|
async getLogicalColumns(
|
|
tableName: string,
|
|
menuObjid: number,
|
|
companyCode: string
|
|
): Promise<any[]> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("논리적 컬럼 목록 조회", {
|
|
tableName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
// 멀티테넌시 적용
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 논리적 컬럼 조회
|
|
query = `
|
|
SELECT
|
|
mapping_id AS "mappingId",
|
|
logical_column_name AS "logicalColumnName",
|
|
physical_column_name AS "physicalColumnName",
|
|
description
|
|
FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND menu_objid = $2
|
|
ORDER BY logical_column_name
|
|
`;
|
|
params = [tableName, menuObjid];
|
|
} else {
|
|
// 일반 회사: 자신의 논리적 컬럼만 조회
|
|
query = `
|
|
SELECT
|
|
mapping_id AS "mappingId",
|
|
logical_column_name AS "logicalColumnName",
|
|
physical_column_name AS "physicalColumnName",
|
|
description
|
|
FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND menu_objid = $2
|
|
AND company_code = $3
|
|
ORDER BY logical_column_name
|
|
`;
|
|
params = [tableName, menuObjid, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.info(`논리적 컬럼 ${result.rows.length}개 조회 완료`, {
|
|
tableName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error(`논리적 컬럼 목록 조회 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 매핑 삭제
|
|
*
|
|
* @param mappingId - 매핑 ID
|
|
* @param companyCode - 회사 코드
|
|
*/
|
|
async deleteColumnMapping(
|
|
mappingId: number,
|
|
companyCode: string
|
|
): Promise<void> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("컬럼 매핑 삭제", { mappingId, companyCode });
|
|
|
|
// 멀티테넌시 적용
|
|
let deleteQuery: string;
|
|
let deleteParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 매핑 삭제 가능
|
|
deleteQuery = `
|
|
DELETE FROM category_column_mapping
|
|
WHERE mapping_id = $1
|
|
`;
|
|
deleteParams = [mappingId];
|
|
} else {
|
|
// 일반 회사: 자신의 매핑만 삭제 가능
|
|
deleteQuery = `
|
|
DELETE FROM category_column_mapping
|
|
WHERE mapping_id = $1
|
|
AND company_code = $2
|
|
`;
|
|
deleteParams = [mappingId, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(deleteQuery, deleteParams);
|
|
|
|
if (result.rowCount === 0) {
|
|
throw new Error("컬럼 매핑을 찾을 수 없거나 권한이 없습니다");
|
|
}
|
|
|
|
logger.info("컬럼 매핑 삭제 완료", { mappingId, companyCode });
|
|
} catch (error: any) {
|
|
logger.error(`컬럼 매핑 삭제 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블+컬럼 기준으로 모든 매핑 삭제
|
|
*
|
|
* 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param columnName - 컬럼명
|
|
* @param companyCode - 회사 코드
|
|
* @returns 삭제된 매핑 수
|
|
*/
|
|
async deleteColumnMappingsByColumn(
|
|
tableName: string,
|
|
columnName: string,
|
|
companyCode: string
|
|
): Promise<number> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("테이블+컬럼 기준 매핑 삭제", { tableName, columnName, companyCode });
|
|
|
|
// 멀티테넌시 적용
|
|
let deleteQuery: string;
|
|
let deleteParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 해당 테이블+컬럼의 모든 매핑 삭제
|
|
deleteQuery = `
|
|
DELETE FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND logical_column_name = $2
|
|
`;
|
|
deleteParams = [tableName, columnName];
|
|
} else {
|
|
// 일반 회사: 자신의 매핑만 삭제
|
|
deleteQuery = `
|
|
DELETE FROM category_column_mapping
|
|
WHERE table_name = $1
|
|
AND logical_column_name = $2
|
|
AND company_code = $3
|
|
`;
|
|
deleteParams = [tableName, columnName, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(deleteQuery, deleteParams);
|
|
const deletedCount = result.rowCount || 0;
|
|
|
|
logger.info("테이블+컬럼 기준 매핑 삭제 완료", {
|
|
tableName,
|
|
columnName,
|
|
companyCode,
|
|
deletedCount
|
|
});
|
|
|
|
return deletedCount;
|
|
} catch (error: any) {
|
|
logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 논리적 컬럼명을 물리적 컬럼명으로 변환
|
|
*
|
|
* 데이터 저장 시 사용
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param menuObjid - 메뉴 OBJID
|
|
* @param companyCode - 회사 코드
|
|
* @param data - 논리적 컬럼명으로 된 데이터
|
|
* @returns 물리적 컬럼명으로 변환된 데이터
|
|
*/
|
|
async convertToPhysicalColumns(
|
|
tableName: string,
|
|
menuObjid: number,
|
|
companyCode: string,
|
|
data: Record<string, any>
|
|
): Promise<Record<string, any>> {
|
|
try {
|
|
// 컬럼 매핑 조회
|
|
const mapping = await this.getColumnMapping(tableName, menuObjid, companyCode);
|
|
|
|
// 논리적 컬럼명 → 물리적 컬럼명 변환
|
|
const physicalData: Record<string, any> = {};
|
|
for (const [key, value] of Object.entries(data)) {
|
|
const physicalColumn = mapping[key] || key; // 매핑 없으면 원래 이름 사용
|
|
physicalData[physicalColumn] = value;
|
|
}
|
|
|
|
logger.info("컬럼명 변환 완료", {
|
|
tableName,
|
|
menuObjid,
|
|
logicalColumns: Object.keys(data),
|
|
physicalColumns: Object.keys(physicalData),
|
|
});
|
|
|
|
return physicalData;
|
|
} catch (error: any) {
|
|
logger.error(`컬럼명 변환 실패: ${error.message}`);
|
|
// 매핑이 없으면 원본 데이터 그대로 반환
|
|
return data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 2레벨 메뉴 목록 조회
|
|
*
|
|
* 카테고리 컬럼 매핑 생성 시 메뉴 선택용
|
|
*
|
|
* @param companyCode - 회사 코드
|
|
* @returns 2레벨 메뉴 목록
|
|
*/
|
|
async getSecondLevelMenus(companyCode: string): Promise<any[]> {
|
|
const pool = getPool();
|
|
|
|
try {
|
|
logger.info("2레벨 메뉴 목록 조회", { companyCode });
|
|
|
|
// menu_info 테이블에 company_code 컬럼이 있는지 확인
|
|
const columnCheckQuery = `
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'menu_info' AND column_name = 'company_code'
|
|
`;
|
|
const columnCheck = await pool.query(columnCheckQuery);
|
|
const hasCompanyCode = columnCheck.rows.length > 0;
|
|
|
|
logger.info("menu_info 테이블 company_code 컬럼 존재 여부", { hasCompanyCode });
|
|
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (!hasCompanyCode) {
|
|
// company_code 컬럼이 없는 경우: 모든 2레벨 사용자 메뉴 조회
|
|
query = `
|
|
SELECT
|
|
m1.objid as "menuObjid",
|
|
m1.menu_name_kor as "menuName",
|
|
m0.menu_name_kor as "parentMenuName",
|
|
m1.screen_code as "screenCode"
|
|
FROM menu_info m1
|
|
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
|
WHERE m1.menu_type = 1
|
|
AND m1.status = 'active'
|
|
AND m0.parent_obj_id = 0
|
|
ORDER BY m0.seq, m1.seq
|
|
`;
|
|
params = [];
|
|
} else if (companyCode === "*") {
|
|
// 최고 관리자: 모든 회사의 2레벨 사용자 메뉴 조회
|
|
query = `
|
|
SELECT
|
|
m1.objid as "menuObjid",
|
|
m1.menu_name_kor as "menuName",
|
|
m0.menu_name_kor as "parentMenuName",
|
|
m1.screen_code as "screenCode"
|
|
FROM menu_info m1
|
|
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
|
WHERE m1.menu_type = 1
|
|
AND m1.status = 'active'
|
|
AND m0.parent_obj_id = 0
|
|
ORDER BY m0.seq, m1.seq
|
|
`;
|
|
params = [];
|
|
} else {
|
|
// 일반 회사: 자신의 회사 메뉴만 조회 (공통 메뉴 제외)
|
|
query = `
|
|
SELECT
|
|
m1.objid as "menuObjid",
|
|
m1.menu_name_kor as "menuName",
|
|
m0.menu_name_kor as "parentMenuName",
|
|
m1.screen_code as "screenCode"
|
|
FROM menu_info m1
|
|
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
|
WHERE m1.menu_type = 1
|
|
AND m1.status = 'active'
|
|
AND m0.parent_obj_id = 0
|
|
AND m1.company_code = $1
|
|
ORDER BY m0.seq, m1.seq
|
|
`;
|
|
params = [companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.info(`2레벨 메뉴 ${result.rows.length}개 조회 완료`, { companyCode });
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error(`2레벨 메뉴 목록 조회 실패: ${error.message}`, { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리 코드로 라벨 조회
|
|
*
|
|
* @param valueCodes - 카테고리 코드 배열
|
|
* @param companyCode - 회사 코드
|
|
* @returns { [code]: label } 형태의 매핑 객체
|
|
*/
|
|
async getCategoryLabelsByCodes(
|
|
valueCodes: string[],
|
|
companyCode: string
|
|
): Promise<Record<string, string>> {
|
|
try {
|
|
if (!valueCodes || valueCodes.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
logger.info("카테고리 코드로 라벨 조회", { valueCodes, companyCode });
|
|
|
|
const pool = getPool();
|
|
|
|
// 동적으로 파라미터 플레이스홀더 생성
|
|
const placeholders = valueCodes.map((_, i) => `$${i + 1}`).join(", ");
|
|
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 카테고리 값 조회
|
|
query = `
|
|
SELECT value_code, value_label
|
|
FROM table_column_category_values
|
|
WHERE value_code IN (${placeholders})
|
|
AND is_active = true
|
|
`;
|
|
params = valueCodes;
|
|
} else {
|
|
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
|
query = `
|
|
SELECT value_code, value_label
|
|
FROM table_column_category_values
|
|
WHERE value_code IN (${placeholders})
|
|
AND is_active = true
|
|
AND (company_code = $${valueCodes.length + 1} OR company_code = '*')
|
|
`;
|
|
params = [...valueCodes, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
// { [code]: label } 형태로 변환
|
|
const labels: Record<string, string> = {};
|
|
for (const row of result.rows) {
|
|
labels[row.value_code] = row.value_label;
|
|
}
|
|
|
|
logger.info(`카테고리 라벨 ${Object.keys(labels).length}개 조회 완료`, { companyCode });
|
|
|
|
return labels;
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 코드로 라벨 조회 실패: ${error.message}`, { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블의 카테고리 타입 컬럼과 해당 값 매핑 조회 (라벨 → 코드 변환용)
|
|
*
|
|
* 엑셀 업로드 등에서 라벨 값을 코드 값으로 변환할 때 사용
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param companyCode - 회사 코드
|
|
* @returns { [columnName]: { [label]: code } } 형태의 매핑 객체
|
|
*/
|
|
async getCategoryLabelToCodeMapping(
|
|
tableName: string,
|
|
companyCode: string
|
|
): Promise<Record<string, Record<string, string>>> {
|
|
try {
|
|
logger.info("카테고리 라벨→코드 매핑 조회", { tableName, companyCode });
|
|
|
|
const pool = getPool();
|
|
|
|
// 1. 해당 테이블의 카테고리 타입 컬럼 조회
|
|
const categoryColumnsQuery = `
|
|
SELECT column_name
|
|
FROM table_type_columns
|
|
WHERE table_name = $1
|
|
AND input_type = 'category'
|
|
`;
|
|
const categoryColumnsResult = await pool.query(categoryColumnsQuery, [tableName]);
|
|
|
|
if (categoryColumnsResult.rows.length === 0) {
|
|
logger.info("카테고리 타입 컬럼 없음", { tableName });
|
|
return {};
|
|
}
|
|
|
|
const categoryColumns = categoryColumnsResult.rows.map(row => row.column_name);
|
|
logger.info(`카테고리 컬럼 ${categoryColumns.length}개 발견`, { categoryColumns });
|
|
|
|
// 2. 각 카테고리 컬럼의 라벨→코드 매핑 조회
|
|
const result: Record<string, Record<string, string>> = {};
|
|
|
|
for (const columnName of categoryColumns) {
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 카테고리 값 조회
|
|
query = `
|
|
SELECT value_code, value_label
|
|
FROM table_column_category_values
|
|
WHERE table_name = $1
|
|
AND column_name = $2
|
|
AND is_active = true
|
|
`;
|
|
params = [tableName, columnName];
|
|
} else {
|
|
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
|
query = `
|
|
SELECT value_code, value_label
|
|
FROM table_column_category_values
|
|
WHERE table_name = $1
|
|
AND column_name = $2
|
|
AND is_active = true
|
|
AND (company_code = $3 OR company_code = '*')
|
|
`;
|
|
params = [tableName, columnName, companyCode];
|
|
}
|
|
|
|
const valuesResult = await pool.query(query, params);
|
|
|
|
// { [label]: code } 형태로 변환
|
|
const labelToCodeMap: Record<string, string> = {};
|
|
for (const row of valuesResult.rows) {
|
|
// 라벨을 소문자로 변환하여 대소문자 구분 없이 매핑
|
|
labelToCodeMap[row.value_label] = row.value_code;
|
|
// 소문자 키도 추가 (대소문자 무시 검색용)
|
|
labelToCodeMap[row.value_label.toLowerCase()] = row.value_code;
|
|
}
|
|
|
|
if (Object.keys(labelToCodeMap).length > 0) {
|
|
result[columnName] = labelToCodeMap;
|
|
logger.info(`컬럼 ${columnName}의 라벨→코드 매핑 ${valuesResult.rows.length}개 조회`);
|
|
}
|
|
}
|
|
|
|
logger.info(`카테고리 라벨→코드 매핑 조회 완료`, {
|
|
tableName,
|
|
columnCount: Object.keys(result).length
|
|
});
|
|
|
|
return result;
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 라벨→코드 매핑 조회 실패: ${error.message}`, { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 데이터의 카테고리 라벨 값을 코드 값으로 변환
|
|
*
|
|
* 엑셀 업로드 등에서 사용자가 입력한 라벨 값을 DB 저장용 코드 값으로 변환
|
|
*
|
|
* @param tableName - 테이블명
|
|
* @param companyCode - 회사 코드
|
|
* @param data - 변환할 데이터 객체
|
|
* @returns 라벨이 코드로 변환된 데이터 객체
|
|
*/
|
|
async convertCategoryLabelsToCodesForData(
|
|
tableName: string,
|
|
companyCode: string,
|
|
data: Record<string, any>
|
|
): Promise<{ convertedData: Record<string, any>; conversions: Array<{ column: string; label: string; code: string }> }> {
|
|
try {
|
|
// 라벨→코드 매핑 조회
|
|
const labelToCodeMapping = await this.getCategoryLabelToCodeMapping(tableName, companyCode);
|
|
|
|
if (Object.keys(labelToCodeMapping).length === 0) {
|
|
// 카테고리 컬럼 없음
|
|
return { convertedData: data, conversions: [] };
|
|
}
|
|
|
|
const convertedData = { ...data };
|
|
const conversions: Array<{ column: string; label: string; code: string }> = [];
|
|
|
|
for (const [columnName, labelCodeMap] of Object.entries(labelToCodeMapping)) {
|
|
const value = data[columnName];
|
|
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
const stringValue = String(value).trim();
|
|
|
|
// 다중 값 확인 (쉼표로 구분된 경우)
|
|
if (stringValue.includes(",")) {
|
|
// 다중 카테고리 값 처리
|
|
const labels = stringValue.split(",").map(s => s.trim()).filter(s => s !== "");
|
|
const convertedCodes: string[] = [];
|
|
let allConverted = true;
|
|
|
|
for (const label of labels) {
|
|
// 정확한 라벨 매칭 시도
|
|
let matchedCode = labelCodeMap[label];
|
|
|
|
// 대소문자 무시 매칭
|
|
if (!matchedCode) {
|
|
matchedCode = labelCodeMap[label.toLowerCase()];
|
|
}
|
|
|
|
if (matchedCode) {
|
|
convertedCodes.push(matchedCode);
|
|
conversions.push({
|
|
column: columnName,
|
|
label: label,
|
|
code: matchedCode,
|
|
});
|
|
logger.info(`카테고리 라벨→코드 변환 (다중): ${columnName} "${label}" → "${matchedCode}"`);
|
|
} else {
|
|
// 이미 코드값인지 확인
|
|
const isAlreadyCode = Object.values(labelCodeMap).includes(label);
|
|
if (isAlreadyCode) {
|
|
// 이미 코드값이면 그대로 사용
|
|
convertedCodes.push(label);
|
|
} else {
|
|
// 라벨도 코드도 아니면 원래 값 유지
|
|
convertedCodes.push(label);
|
|
allConverted = false;
|
|
logger.warn(`카테고리 값 매핑 없음 (다중): ${columnName} = "${label}" (라벨도 코드도 아님)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 변환된 코드들을 쉼표로 합쳐서 저장
|
|
convertedData[columnName] = convertedCodes.join(",");
|
|
logger.info(`다중 카테고리 변환 완료: ${columnName} "${stringValue}" → "${convertedData[columnName]}"`);
|
|
} else {
|
|
// 단일 값 처리
|
|
// 정확한 라벨 매칭 시도
|
|
let matchedCode = labelCodeMap[stringValue];
|
|
|
|
// 대소문자 무시 매칭
|
|
if (!matchedCode) {
|
|
matchedCode = labelCodeMap[stringValue.toLowerCase()];
|
|
}
|
|
|
|
if (matchedCode) {
|
|
// 라벨 값을 코드 값으로 변환
|
|
convertedData[columnName] = matchedCode;
|
|
conversions.push({
|
|
column: columnName,
|
|
label: stringValue,
|
|
code: matchedCode,
|
|
});
|
|
logger.info(`카테고리 라벨→코드 변환: ${columnName} "${stringValue}" → "${matchedCode}"`);
|
|
} else {
|
|
// 이미 코드값인지 확인 (역방향 확인)
|
|
const isAlreadyCode = Object.values(labelCodeMap).includes(stringValue);
|
|
if (!isAlreadyCode) {
|
|
logger.warn(`카테고리 값 매핑 없음: ${columnName} = "${stringValue}" (라벨도 코드도 아님)`);
|
|
}
|
|
// 변환 없이 원래 값 유지
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info(`카테고리 라벨→코드 변환 완료`, {
|
|
tableName,
|
|
conversionCount: conversions.length,
|
|
conversions,
|
|
});
|
|
|
|
return { convertedData, conversions };
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 라벨→코드 변환 실패: ${error.message}`, { error });
|
|
// 실패 시 원본 데이터 반환
|
|
return { convertedData: data, conversions: [] };
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new TableCategoryValueService();
|