import { getPool } from "../database/db"; import { logger } from "../utils/logger"; /** * 메뉴 관련 유틸리티 서비스 * * 메뉴 스코프 기반 데이터 공유를 위한 형제 메뉴 조회 기능 제공 */ /** * 메뉴의 형제 메뉴 및 자식 메뉴 OBJID 목록 조회 * (같은 부모를 가진 메뉴들 + 자식 메뉴들) * * 메뉴 스코프 규칙: * - 같은 부모를 가진 형제 메뉴들은 카테고리/채번규칙을 공유 * - 자식 메뉴의 데이터도 부모 메뉴에서 조회 가능 (3레벨까지만 존재) * - 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환 * - 메뉴를 찾을 수 없으면 안전하게 자기 자신만 반환 * * @param menuObjid 현재 메뉴의 OBJID * @returns 형제 메뉴 + 자식 메뉴 OBJID 배열 (자기 자신 포함, 정렬됨) * * @example * // 영업관리 (200) * // ├── 고객관리 (201) * // │ └── 고객등록 (211) * // ├── 계약관리 (202) * // └── 주문관리 (203) * * await getSiblingMenuObjids(201); * // 결과: [201, 202, 203, 211] - 형제(202, 203) + 자식(211) */ export async function getSiblingMenuObjids(menuObjid: number): Promise { const pool = getPool(); try { logger.debug("메뉴 스코프 조회 시작", { menuObjid }); // 1. 현재 메뉴 정보 조회 (부모 ID 확인) const currentMenuQuery = ` SELECT parent_obj_id FROM menu_info WHERE objid = $1 `; const currentMenuResult = await pool.query(currentMenuQuery, [menuObjid]); if (currentMenuResult.rows.length === 0) { logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid }); return [menuObjid]; } const parentObjId = Number(currentMenuResult.rows[0].parent_obj_id); // 2. 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환 if (parentObjId === 0) { logger.debug("최상위 메뉴, 자기 자신만 반환", { menuObjid }); return [menuObjid]; } // 3. 형제 메뉴들 조회 (같은 부모를 가진 메뉴들) const siblingsQuery = ` SELECT objid FROM menu_info WHERE parent_obj_id = $1 ORDER BY objid `; const siblingsResult = await pool.query(siblingsQuery, [parentObjId]); const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid)); // 4. 각 형제 메뉴(자기 자신 포함)의 자식 메뉴들도 조회 const allObjids = [...siblingObjids]; for (const siblingObjid of siblingObjids) { const childrenQuery = ` SELECT objid FROM menu_info WHERE parent_obj_id = $1 ORDER BY objid `; const childrenResult = await pool.query(childrenQuery, [siblingObjid]); const childObjids = childrenResult.rows.map((row) => Number(row.objid)); allObjids.push(...childObjids); } // 5. 중복 제거 및 정렬 const uniqueObjids = Array.from(new Set(allObjids)).sort((a, b) => a - b); logger.debug("메뉴 스코프 조회 완료", { menuObjid, parentObjId, siblingCount: siblingObjids.length, totalCount: uniqueObjids.length }); return uniqueObjids; } catch (error: any) { logger.error("메뉴 스코프 조회 실패", { menuObjid, error: error.message, stack: error.stack }); // 에러 발생 시 안전하게 자기 자신만 반환 return [menuObjid]; } } /** * 선택한 메뉴와 그 하위 메뉴들의 OBJID 조회 * * 형제 메뉴는 포함하지 않고, 선택한 메뉴와 그 자식 메뉴들만 반환합니다. * 채번 규칙 필터링 등 특정 메뉴 계층만 필요할 때 사용합니다. * * @param menuObjid 메뉴 OBJID * @returns 선택한 메뉴 + 모든 하위 메뉴 OBJID 배열 (재귀적) * * @example * // 메뉴 구조: * // └── 구매관리 (100) * // ├── 공급업체관리 (101) * // ├── 발주관리 (102) * // └── 입고관리 (103) * // └── 입고상세 (104) * * await getMenuAndChildObjids(100); * // 결과: [100, 101, 102, 103, 104] */ export async function getMenuAndChildObjids(menuObjid: number): Promise { const pool = getPool(); try { logger.debug("메뉴 및 하위 메뉴 조회 시작", { menuObjid }); // 재귀 CTE를 사용하여 선택한 메뉴와 모든 하위 메뉴 조회 const query = ` WITH RECURSIVE menu_tree AS ( -- 시작점: 선택한 메뉴 SELECT objid, parent_obj_id, 1 AS depth FROM menu_info WHERE objid = $1 UNION ALL -- 재귀: 하위 메뉴들 SELECT m.objid, m.parent_obj_id, mt.depth + 1 FROM menu_info m INNER JOIN menu_tree mt ON m.parent_obj_id = mt.objid WHERE mt.depth < 10 -- 무한 루프 방지 ) SELECT objid FROM menu_tree ORDER BY depth, objid `; const result = await pool.query(query, [menuObjid]); const objids = result.rows.map((row) => Number(row.objid)); logger.debug("메뉴 및 하위 메뉴 조회 완료", { menuObjid, totalCount: objids.length, objids }); return objids; } catch (error: any) { logger.error("메뉴 및 하위 메뉴 조회 실패", { menuObjid, error: error.message, stack: error.stack }); // 에러 발생 시 안전하게 자기 자신만 반환 return [menuObjid]; } } /** * 여러 메뉴의 형제 메뉴 OBJID 합집합 조회 * * 여러 메뉴에 속한 모든 형제 메뉴를 중복 제거하여 반환 * * @param menuObjids 메뉴 OBJID 배열 * @returns 모든 형제 메뉴 OBJID 배열 (중복 제거, 정렬됨) * * @example * // 서로 다른 부모를 가진 메뉴들의 형제를 모두 조회 * await getAllSiblingMenuObjids([201, 301]); * // 201의 형제: [201, 202, 203] * // 301의 형제: [301, 302] * // 결과: [201, 202, 203, 301, 302] */ export async function getAllSiblingMenuObjids( menuObjids: number[] ): Promise { if (!menuObjids || menuObjids.length === 0) { logger.warn("getAllSiblingMenuObjids: 빈 배열 입력"); return []; } const allSiblings = new Set(); for (const objid of menuObjids) { const siblings = await getSiblingMenuObjids(objid); siblings.forEach((s) => allSiblings.add(s)); } const result = Array.from(allSiblings).sort((a, b) => a - b); logger.info("여러 메뉴의 형제 조회 완료", { inputMenus: menuObjids, resultCount: result.length, result, }); return result; } /** * 메뉴 정보 조회 * * @param menuObjid 메뉴 OBJID * @returns 메뉴 정보 (없으면 null) */ export async function getMenuInfo(menuObjid: number): Promise { const pool = getPool(); try { const query = ` SELECT objid, parent_obj_id AS "parentObjId", menu_name_kor AS "menuNameKor", menu_name_eng AS "menuNameEng", menu_url AS "menuUrl", company_code AS "companyCode" FROM menu_info WHERE objid = $1 `; const result = await pool.query(query, [menuObjid]); if (result.rows.length === 0) { return null; } return result.rows[0]; } catch (error: any) { logger.error("메뉴 정보 조회 실패", { menuObjid, error: error.message }); return null; } }