공통코드 계층구조 구현

This commit is contained in:
kjs
2025-12-23 09:31:18 +09:00
parent b85b888007
commit 5f406fbe88
32 changed files with 3673 additions and 478 deletions

View File

@@ -25,6 +25,8 @@ export interface CodeInfo {
is_active: string;
company_code: string;
menu_objid?: number | null; // 메뉴 기반 코드 관리용
parent_code_value?: string | null; // 계층구조: 부모 코드값
depth?: number; // 계층구조: 깊이 (1, 2, 3단계)
created_date?: Date | null;
created_by?: string | null;
updated_date?: Date | null;
@@ -61,6 +63,8 @@ export interface CreateCodeData {
description?: string;
sortOrder?: number;
isActive?: string;
parentCodeValue?: string; // 계층구조: 부모 코드값
depth?: number; // 계층구조: 깊이 (1, 2, 3단계)
}
export class CommonCodeService {
@@ -405,11 +409,22 @@ export class CommonCodeService {
menuObjid: number
) {
try {
// 계층구조: depth 계산 (부모가 있으면 부모의 depth + 1, 없으면 1)
let depth = 1;
if (data.parentCodeValue) {
const parentCode = await queryOne<CodeInfo>(
`SELECT depth FROM code_info
WHERE code_category = $1 AND code_value = $2 AND company_code = $3`,
[categoryCode, data.parentCodeValue, companyCode]
);
depth = parentCode ? (parentCode.depth || 1) + 1 : 1;
}
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, NOW(), NOW())
is_active, menu_objid, company_code, parent_code_value, depth, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, $11, $12, NOW(), NOW())
RETURNING *`,
[
categoryCode,
@@ -420,13 +435,15 @@ export class CommonCodeService {
data.sortOrder || 0,
menuObjid,
companyCode,
data.parentCodeValue || null,
depth,
createdBy,
createdBy,
]
);
logger.info(
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode})`
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode}, 부모: ${data.parentCodeValue || '없음'}, 깊이: ${depth})`
);
return code;
} catch (error) {
@@ -491,6 +508,24 @@ export class CommonCodeService {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
// 계층구조: 부모 코드값 수정
if (data.parentCodeValue !== undefined) {
updateFields.push(`parent_code_value = $${paramIndex++}`);
values.push(data.parentCodeValue || null);
// depth도 함께 업데이트
let newDepth = 1;
if (data.parentCodeValue) {
const parentCode = await queryOne<CodeInfo>(
`SELECT depth FROM code_info
WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
newDepth = parentCode ? (parentCode.depth || 1) + 1 : 1;
}
updateFields.push(`depth = $${paramIndex++}`);
values.push(newDepth);
}
// WHERE 절 구성
let whereClause = `WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}`;
@@ -847,4 +882,170 @@ export class CommonCodeService {
throw error;
}
}
/**
* 계층구조 코드 조회 (특정 depth 또는 부모코드 기준)
* @param categoryCode 카테고리 코드
* @param parentCodeValue 부모 코드값 (없으면 최상위 코드만 조회)
* @param depth 특정 깊이만 조회 (선택)
*/
async getHierarchicalCodes(
categoryCode: string,
parentCodeValue?: string | null,
depth?: number,
userCompanyCode?: string,
menuObjid?: number
) {
try {
const whereConditions: string[] = ["code_category = $1", "is_active = 'Y'"];
const values: any[] = [categoryCode];
let paramIndex = 2;
// 부모 코드값 필터링
if (parentCodeValue === null || parentCodeValue === undefined) {
// 최상위 코드 (부모가 없는 코드)
whereConditions.push("(parent_code_value IS NULL OR parent_code_value = '')");
} else if (parentCodeValue !== '') {
whereConditions.push(`parent_code_value = $${paramIndex}`);
values.push(parentCodeValue);
paramIndex++;
}
// 특정 깊이 필터링
if (depth !== undefined) {
whereConditions.push(`depth = $${paramIndex}`);
values.push(depth);
paramIndex++;
}
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex}`);
values.push(userCompanyCode);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const codes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY sort_order ASC, code_value ASC`,
values
);
logger.info(
`계층구조 코드 조회: ${categoryCode}, 부모: ${parentCodeValue || '최상위'}, 깊이: ${depth || '전체'} - ${codes.length}`
);
return codes;
} catch (error) {
logger.error(`계층구조 코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 계층구조 코드 트리 전체 조회 (카테고리 기준)
*/
async getCodeTree(
categoryCode: string,
userCompanyCode?: string,
menuObjid?: number
) {
try {
const whereConditions: string[] = ["code_category = $1", "is_active = 'Y'"];
const values: any[] = [categoryCode];
let paramIndex = 2;
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex}`);
values.push(userCompanyCode);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const allCodes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY depth ASC, sort_order ASC, code_value ASC`,
values
);
// 트리 구조로 변환
const buildTree = (codes: CodeInfo[], parentValue: string | null = null): any[] => {
return codes
.filter(code => {
const codeParent = code.parent_code_value || null;
return codeParent === parentValue;
})
.map(code => ({
...code,
children: buildTree(codes, code.code_value)
}));
};
const tree = buildTree(allCodes);
logger.info(
`코드 트리 조회 완료: ${categoryCode} - 전체 ${allCodes.length}`
);
return {
flat: allCodes,
tree
};
} catch (error) {
logger.error(`코드 트리 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 자식 코드가 있는지 확인
*/
async hasChildren(
categoryCode: string,
codeValue: string,
companyCode?: string
): Promise<boolean> {
try {
let sql = `SELECT COUNT(*) as count FROM code_info
WHERE code_category = $1 AND parent_code_value = $2`;
const values: any[] = [categoryCode, codeValue];
if (companyCode && companyCode !== "*") {
sql += ` AND company_code = $3`;
values.push(companyCode);
}
const result = await queryOne<{ count: string }>(sql, values);
const count = parseInt(result?.count || "0");
return count > 0;
} catch (error) {
logger.error(`자식 코드 확인 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
}