diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
| commit | f9bfc82880212e1a13f6bbb28ecfc87b89346f26 (patch) | |
| tree | ee792f340ebfa7eaf30d2e79f99f41213e5c5cf3 /lib/users/knox-service.ts | |
| parent | edc0eabc8f5fc44408c28023ca155bd73ddf8183 (diff) | |
(김준회) 메뉴접근제어(부서별) 메뉴 구현
Diffstat (limited to 'lib/users/knox-service.ts')
| -rw-r--r-- | lib/users/knox-service.ts | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/lib/users/knox-service.ts b/lib/users/knox-service.ts new file mode 100644 index 00000000..d5453072 --- /dev/null +++ b/lib/users/knox-service.ts @@ -0,0 +1,377 @@ +"use server"; + +import { unstable_cache } from "next/cache"; +import db from "@/db/db"; +import { organization } from "@/db/schema/knox/organization"; +import { eq, and, asc } from "drizzle-orm"; + +// 조직 트리 노드 타입 +export interface DepartmentNode { + companyCode: string; + departmentCode: string; + departmentName: string; + departmentLevel?: string; + uprDepartmentCode?: string; + lowDepartmentYn?: string; + hiddenDepartmentYn?: string; + children: DepartmentNode[]; + // UI에서 사용할 추가 필드 + label: string; + value: string; + key: string; +} + +// 기본 회사 코드 (환경변수에서 가져오되 폴백 제공) +const getCompanyCode = () => { + const envCodes = process.env.KNOX_COMPANY_CODES; + if (envCodes) { + // 쉼표로 구분된 경우 첫 번째 값 사용 + return envCodes.split(',')[0].trim(); + } + return "D60"; // 폴백 값 +}; + +const DEFAULT_COMPANY_CODE = getCompanyCode(); + +// 조직 데이터 조회 (hiddenDepartmentYn = 'F'만) +export async function getVisibleOrganizations() { + return unstable_cache( + async () => { + try { + const organizations = await db + .select({ + companyCode: organization.companyCode, + departmentCode: organization.departmentCode, + departmentName: organization.departmentName, + departmentLevel: organization.departmentLevel, + uprDepartmentCode: organization.uprDepartmentCode, + lowDepartmentYn: organization.lowDepartmentYn, + hiddenDepartmentYn: organization.hiddenDepartmentYn, + }) + .from(organization) + .where(eq(organization.hiddenDepartmentYn, 'F')) + .orderBy( + asc(organization.companyCode), + asc(organization.departmentLevel), + asc(organization.departmentCode) + ); + + return organizations; + } catch (error) { + console.error("조직 데이터 조회 실패:", error); + return []; + } + }, + ["visible-organizations"], + { + revalidate: 3600, // 1시간 캐시 + tags: ["knox-organizations"], + } + )(); +} + +// 기본 회사의 부서 트리 구조 조회 (처음부터 모든 데이터 로드) +export async function getAllDepartmentsTree(): Promise<DepartmentNode[]> { + return unstable_cache( + async () => { + try { + const organizations = await db + .select({ + companyCode: organization.companyCode, + departmentCode: organization.departmentCode, + departmentName: organization.departmentName, + departmentLevel: organization.departmentLevel, + uprDepartmentCode: organization.uprDepartmentCode, + lowDepartmentYn: organization.lowDepartmentYn, + hiddenDepartmentYn: organization.hiddenDepartmentYn, + }) + .from(organization) + .where( + and( + eq(organization.companyCode, DEFAULT_COMPANY_CODE), + eq(organization.hiddenDepartmentYn, 'F') + ) + ) + .orderBy( + asc(organization.departmentLevel), + asc(organization.departmentCode) + ); + + // 트리 구조 생성 + const tree = buildDepartmentTree(organizations); + return tree; + } catch (error) { + console.error("모든 부서 트리 구성 실패:", error); + return []; + } + }, + [`all-departments-tree-${DEFAULT_COMPANY_CODE}`], + { + revalidate: 3600, + tags: ["knox-organizations"], + } + )(); +} + +// 회사별 조직 트리 구조 생성 (기존 호환성 유지) +export async function getDepartmentTreeByCompany(companyCode: string): Promise<DepartmentNode[]> { + return unstable_cache( + async () => { + try { + const organizations = await db + .select({ + companyCode: organization.companyCode, + departmentCode: organization.departmentCode, + departmentName: organization.departmentName, + departmentLevel: organization.departmentLevel, + uprDepartmentCode: organization.uprDepartmentCode, + lowDepartmentYn: organization.lowDepartmentYn, + hiddenDepartmentYn: organization.hiddenDepartmentYn, + }) + .from(organization) + .where( + and( + eq(organization.companyCode, companyCode), + eq(organization.hiddenDepartmentYn, 'F') + ) + ) + .orderBy( + asc(organization.departmentLevel), + asc(organization.departmentCode) + ); + + // 트리 구조 생성 + const tree = buildDepartmentTree(organizations); + return tree; + } catch (error) { + console.error(`회사 ${companyCode} 조직 트리 구성 실패:`, error); + return []; + } + }, + [`department-tree-${companyCode}`], + { + revalidate: 3600, + tags: ["knox-organizations", `company-${companyCode}`], + } + )(); +} + +// 전체 회사의 조직 트리 구조 생성 +export async function getAllDepartmentTrees(): Promise<Record<string, DepartmentNode[]>> { + return unstable_cache( + async () => { + try { + const organizations = await getVisibleOrganizations(); + + // 회사별로 그룹화 + const companiesMap = new Map<string, typeof organizations>(); + + organizations.forEach((org) => { + if (!companiesMap.has(org.companyCode)) { + companiesMap.set(org.companyCode, []); + } + companiesMap.get(org.companyCode)!.push(org); + }); + + // 각 회사별로 트리 구조 생성 + const result: Record<string, DepartmentNode[]> = {}; + + for (const [companyCode, orgs] of companiesMap) { + result[companyCode] = buildDepartmentTree(orgs); + } + + return result; + } catch (error) { + console.error("전체 조직 트리 구성 실패:", error); + return {}; + } + }, + ["all-department-trees"], + { + revalidate: 3600, + tags: ["knox-organizations"], + } + )(); +} + +// 부서 트리 구조 빌더 헬퍼 함수 (개선) +function buildDepartmentTree( + organizations: Array<{ + companyCode: string; + departmentCode: string; + departmentName: string | null; + departmentLevel?: string | null; + uprDepartmentCode?: string | null; + lowDepartmentYn?: string | null; + hiddenDepartmentYn?: string | null; + }> +): DepartmentNode[] { + // 맵으로 빠른 조회를 위한 인덱스 생성 + const orgMap = new Map<string, DepartmentNode>(); + const rootNodes: DepartmentNode[] = []; + + // 1단계: 모든 노드를 맵에 추가 + organizations.forEach((org) => { + const node: DepartmentNode = { + companyCode: org.companyCode, + departmentCode: org.departmentCode, + departmentName: org.departmentName || "", + departmentLevel: org.departmentLevel || undefined, + uprDepartmentCode: org.uprDepartmentCode || undefined, + lowDepartmentYn: org.lowDepartmentYn || undefined, + hiddenDepartmentYn: org.hiddenDepartmentYn || undefined, + children: [], + // UI용 필드 + label: org.departmentName || org.departmentCode, + value: org.departmentCode, + key: `${org.companyCode}-${org.departmentCode}`, + }; + + orgMap.set(org.departmentCode, node); + }); + + // 2단계: 부모-자식 관계 설정 + organizations.forEach((org) => { + const currentNode = orgMap.get(org.departmentCode); + if (!currentNode) return; + + if (org.uprDepartmentCode && orgMap.has(org.uprDepartmentCode)) { + // 부모가 있으면 부모의 children에 추가 + const parentNode = orgMap.get(org.uprDepartmentCode); + parentNode!.children.push(currentNode); + } else { + // 부모가 없으면 루트 노드로 처리 + // 하지만 상위 부서가 없는 부서들은 depth 1에 배치 + rootNodes.push(currentNode); + } + }); + + // 3단계: 고립된 부서들 처리 (상위도 하위도 없는 부서들) + // lowDepartmentYn이 'F'이거나 null이고, uprDepartmentCode가 없거나 존재하지 않는 부서들을 확인 + organizations.forEach((org) => { + const currentNode = orgMap.get(org.departmentCode); + if (!currentNode) return; + + // 이미 루트에 추가되었거나 다른 부서의 자식이 된 경우는 스킵 + const isAlreadyPlaced = rootNodes.includes(currentNode) || + organizations.some(otherOrg => { + const otherNode = orgMap.get(otherOrg.departmentCode); + return otherNode && otherNode.children.includes(currentNode); + }); + + if (!isAlreadyPlaced) { + // 고립된 부서를 루트에 추가 + rootNodes.push(currentNode); + } + }); + + // 4단계: 각 노드의 children을 정렬 + const sortChildren = (node: DepartmentNode) => { + node.children.sort((a, b) => { + // departmentLevel이 있으면 그걸로 정렬, 없으면 departmentCode로 정렬 + const aLevel = parseInt(a.departmentLevel || "999"); + const bLevel = parseInt(b.departmentLevel || "999"); + + if (aLevel !== bLevel) { + return aLevel - bLevel; + } + + return a.departmentCode.localeCompare(b.departmentCode); + }); + + // 재귀적으로 자식들도 정렬 + node.children.forEach(sortChildren); + }; + + // 5단계: 루트 노드들도 정렬 + rootNodes.sort((a, b) => { + const aLevel = parseInt(a.departmentLevel || "1"); + const bLevel = parseInt(b.departmentLevel || "1"); + + if (aLevel !== bLevel) { + return aLevel - bLevel; + } + + return a.departmentCode.localeCompare(b.departmentCode); + }); + + rootNodes.forEach(sortChildren); + + return rootNodes; +} + +// 특정 부서의 모든 하위 부서 코드 조회 (재귀) - 기본 회사 대상 +export async function getChildDepartmentCodes(departmentCode: string): Promise<string[]> { + const tree = await getAllDepartmentsTree(); + const result: string[] = []; + + const findAndCollectChildren = (nodes: DepartmentNode[], targetCode: string): boolean => { + for (const node of nodes) { + if (node.departmentCode === targetCode) { + // 타겟 노드 발견, 모든 하위 부서 코드 수집 + collectAllDepartmentCodes(node, result); + return true; + } + + // 자식 노드들에서 재귀 검색 + if (findAndCollectChildren(node.children, targetCode)) { + return true; + } + } + return false; + }; + + const collectAllDepartmentCodes = (node: DepartmentNode, codes: string[]) => { + codes.push(node.departmentCode); + node.children.forEach(child => collectAllDepartmentCodes(child, codes)); + }; + + findAndCollectChildren(tree, departmentCode); + return result; +} + +// 회사 목록 조회 (호환성 유지용) +export async function getCompanies(): Promise<Array<{ code: string; name: string }>> { + return unstable_cache( + async () => { + try { + const companies = await db + .selectDistinct({ + code: organization.companyCode, + name: organization.companyName, + }) + .from(organization) + .where(eq(organization.hiddenDepartmentYn, 'F')) + .orderBy(asc(organization.companyCode)); + + return companies + .filter(company => company.code && company.name) + .map(company => ({ + code: company.code, + name: company.name!, + })); + } catch (error) { + console.error("회사 목록 조회 실패:", error); + return []; + } + }, + ["companies"], + { + revalidate: 3600, + tags: ["knox-organizations"], + } + )(); +} + +// 현재 사용 중인 회사 코드 반환 +export async function getCurrentCompanyCode(): Promise<string> { + return DEFAULT_COMPANY_CODE; +} + +// 현재 사용 중인 회사 정보 반환 +export async function getCurrentCompanyInfo(): Promise<{ code: string; name: string }> { + return { + code: DEFAULT_COMPANY_CODE, + name: "삼성중공업" + }; +} |
