diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/users/department-domain/service.ts | 439 | ||||
| -rw-r--r-- | lib/users/knox-service.ts | 377 |
2 files changed, 816 insertions, 0 deletions
diff --git a/lib/users/department-domain/service.ts b/lib/users/department-domain/service.ts new file mode 100644 index 00000000..570ef2cf --- /dev/null +++ b/lib/users/department-domain/service.ts @@ -0,0 +1,439 @@ +"use server"; + +import { revalidatePath, revalidateTag } from "next/cache"; +import { unstable_cache, unstable_noStore } from "next/cache"; +import db from "@/db/db"; +import { + departmentDomainAssignments, + departmentDomainAssignmentHistory +} from "@/db/schema/departmentDomainAssignments"; +import { and, eq, inArray, desc } from "drizzle-orm"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getErrorMessage } from "@/lib/handle-error"; +import { getCurrentCompanyCode } from "@/lib/users/knox-service"; + +// 도메인 타입 +export type UserDomain = "pending" | "evcp" | "procurement" | "sales" | "engineering" | "partners"; + +// 부서별 도메인 할당 정보 조회 +export async function getDepartmentDomainAssignments() { + return unstable_cache( + async () => { + try { + const assignments = await db + .select({ + id: departmentDomainAssignments.id, + companyCode: departmentDomainAssignments.companyCode, + departmentCode: departmentDomainAssignments.departmentCode, + departmentName: departmentDomainAssignments.departmentName, + assignedDomain: departmentDomainAssignments.assignedDomain, + isActive: departmentDomainAssignments.isActive, + description: departmentDomainAssignments.description, + createdAt: departmentDomainAssignments.createdAt, + updatedAt: departmentDomainAssignments.updatedAt, + }) + .from(departmentDomainAssignments) + .where(eq(departmentDomainAssignments.isActive, true)) + .orderBy( + desc(departmentDomainAssignments.updatedAt) + ); + + return assignments; + } catch (error) { + console.error("부서별 도메인 할당 정보 조회 실패:", error); + return []; + } + }, + ["department-domain-assignments"], + { + revalidate: 3600, // 1시간 캐시 + tags: ["department-domain-assignments"], + } + )(); +} + +// 특정 부서들의 도메인 할당 정보 조회 +export async function getDepartmentDomainAssignmentsByDepartments(departmentCodes: string[]) { + return unstable_cache( + async () => { + try { + if (departmentCodes.length === 0) return []; + + const assignments = await db + .select() + .from(departmentDomainAssignments) + .where( + and( + inArray(departmentDomainAssignments.departmentCode, departmentCodes), + eq(departmentDomainAssignments.isActive, true) + ) + ); + + return assignments; + } catch (error) { + console.error("부서별 도메인 할당 정보 조회 실패:", error); + return []; + } + }, + [`department-assignments-${departmentCodes.sort().join(',')}`], + { + revalidate: 3600, + tags: ["department-domain-assignments"], + } + )(); +} + +// 부서별 도메인 할당 +export async function assignDomainToDepartments(params: { + departmentCodes: string[]; + domain: UserDomain; + description?: string; + departmentNames?: Record<string, string>; // departmentCode -> departmentName 매핑 +}) { + unstable_noStore(); + + try { + // 세션 확인 + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + message: "인증이 필요합니다.", + }; + } + + const { departmentCodes, domain, description, departmentNames = {} } = params; + const userId = parseInt(session.user.id); + + if (!departmentCodes.length || !domain) { + return { + success: false, + message: "부서 코드와 도메인이 필요합니다.", + }; + } + + // 현재 회사 코드 가져오기 + const companyCode = await getCurrentCompanyCode(); + + await db.transaction(async (tx) => { + // 기존 할당 정보를 비활성화 (soft delete) + const existingAssignments = await tx + .select() + .from(departmentDomainAssignments) + .where( + and( + inArray(departmentDomainAssignments.departmentCode, departmentCodes), + eq(departmentDomainAssignments.isActive, true) + ) + ); + + // 기존 할당이 있으면 비활성화하고 히스토리 기록 + for (const existing of existingAssignments) { + // 히스토리 기록 + await tx.insert(departmentDomainAssignmentHistory).values({ + assignmentId: existing.id, + action: "deactivated", + previousValues: JSON.stringify({ + assignedDomain: existing.assignedDomain, + isActive: true, + description: existing.description, + }), + newValues: JSON.stringify({ + isActive: false, + }), + changedBy: userId, + changeReason: `새로운 도메인 할당으로 인한 기존 할당 비활성화: ${domain}`, + }); + + // 기존 할당 비활성화 + await tx + .update(departmentDomainAssignments) + .set({ + isActive: false, + updatedBy: userId, + updatedAt: new Date(), + }) + .where(eq(departmentDomainAssignments.id, existing.id)); + } + + // 새로운 할당 생성 + const newAssignments = departmentCodes.map(departmentCode => { + return { + companyCode, + departmentCode, + departmentName: departmentNames[departmentCode] || departmentCode, + assignedDomain: domain, + description, + isActive: true, + createdBy: userId, + updatedBy: userId, + }; + }); + + const insertedAssignments = await tx + .insert(departmentDomainAssignments) + .values(newAssignments) + .returning(); + + // 신규 생성 히스토리 기록 + for (let i = 0; i < insertedAssignments.length; i++) { + const assignment = insertedAssignments[i]; + await tx.insert(departmentDomainAssignmentHistory).values({ + assignmentId: assignment.id, + action: "created", + newValues: JSON.stringify({ + companyCode: assignment.companyCode, + departmentCode: assignment.departmentCode, + assignedDomain: assignment.assignedDomain, + description: assignment.description, + }), + changedBy: userId, + changeReason: description || "부서별 도메인 할당", + }); + } + }); + + // 캐시 무효화 + revalidateTag("department-domain-assignments"); + revalidatePath("/evcp/menu-access-dept"); + + return { + success: true, + message: `${departmentCodes.length}개 부서에 ${domain} 도메인이 성공적으로 할당되었습니다.`, + }; + + } catch (error) { + console.error("부서별 도메인 할당 실패:", error); + return { + success: false, + message: getErrorMessage(error), + }; + } +} + +// 부서별 도메인 할당 수정 +export async function updateDepartmentDomainAssignment(params: { + assignmentId: number; + domain: UserDomain; + description?: string; + isActive?: boolean; +}) { + unstable_noStore(); + + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + message: "인증이 필요합니다.", + }; + } + + const { assignmentId, domain, description, isActive = true } = params; + const userId = parseInt(session.user.id); + + await db.transaction(async (tx) => { + // 기존 할당 정보 조회 + const existing = await tx + .select() + .from(departmentDomainAssignments) + .where(eq(departmentDomainAssignments.id, assignmentId)) + .limit(1); + + if (existing.length === 0) { + throw new Error("존재하지 않는 할당 정보입니다."); + } + + const currentAssignment = existing[0]; + + // 히스토리 기록 + await tx.insert(departmentDomainAssignmentHistory).values({ + assignmentId, + action: "updated", + previousValues: JSON.stringify({ + assignedDomain: currentAssignment.assignedDomain, + description: currentAssignment.description, + isActive: currentAssignment.isActive, + }), + newValues: JSON.stringify({ + assignedDomain: domain, + description, + isActive, + }), + changedBy: userId, + changeReason: description || "부서별 도메인 할당 수정", + }); + + // 할당 정보 업데이트 + await tx + .update(departmentDomainAssignments) + .set({ + assignedDomain: domain, + description, + isActive, + updatedBy: userId, + updatedAt: new Date(), + }) + .where(eq(departmentDomainAssignments.id, assignmentId)); + }); + + // 캐시 무효화 + revalidateTag("department-domain-assignments"); + revalidatePath("/evcp/menu-access-dept"); + + return { + success: true, + message: "도메인 할당 정보가 성공적으로 수정되었습니다.", + }; + + } catch (error) { + console.error("부서별 도메인 할당 수정 실패:", error); + return { + success: false, + message: getErrorMessage(error), + }; + } +} + +// 부서별 도메인 할당 삭제 (soft delete) +export async function deleteDepartmentDomainAssignment(assignmentId: number) { + unstable_noStore(); + + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + message: "인증이 필요합니다.", + }; + } + + const userId = parseInt(session.user.id); + + await db.transaction(async (tx) => { + // 기존 할당 정보 조회 + const existing = await tx + .select() + .from(departmentDomainAssignments) + .where(eq(departmentDomainAssignments.id, assignmentId)) + .limit(1); + + if (existing.length === 0) { + throw new Error("존재하지 않는 할당 정보입니다."); + } + + const currentAssignment = existing[0]; + + // 히스토리 기록 + await tx.insert(departmentDomainAssignmentHistory).values({ + assignmentId, + action: "deleted", + previousValues: JSON.stringify({ + assignedDomain: currentAssignment.assignedDomain, + description: currentAssignment.description, + isActive: currentAssignment.isActive, + }), + newValues: JSON.stringify({ + isActive: false, + }), + changedBy: userId, + changeReason: "부서별 도메인 할당 삭제", + }); + + // 할당 정보 비활성화 + await tx + .update(departmentDomainAssignments) + .set({ + isActive: false, + updatedBy: userId, + updatedAt: new Date(), + }) + .where(eq(departmentDomainAssignments.id, assignmentId)); + }); + + // 캐시 무효화 + revalidateTag("department-domain-assignments"); + revalidatePath("/evcp/menu-access-dept"); + + return { + success: true, + message: "도메인 할당 정보가 성공적으로 삭제되었습니다.", + }; + + } catch (error) { + console.error("부서별 도메인 할당 삭제 실패:", error); + return { + success: false, + message: getErrorMessage(error), + }; + } +} + +// 부서별 도메인 할당 히스토리 조회 +export async function getDepartmentDomainAssignmentHistory(assignmentId?: number) { + return unstable_cache( + async () => { + try { + const query = db + .select({ + id: departmentDomainAssignmentHistory.id, + assignmentId: departmentDomainAssignmentHistory.assignmentId, + action: departmentDomainAssignmentHistory.action, + previousValues: departmentDomainAssignmentHistory.previousValues, + newValues: departmentDomainAssignmentHistory.newValues, + changedBy: departmentDomainAssignmentHistory.changedBy, + changeReason: departmentDomainAssignmentHistory.changeReason, + createdAt: departmentDomainAssignmentHistory.createdAt, + }) + .from(departmentDomainAssignmentHistory); + + if (assignmentId) { + query.where(eq(departmentDomainAssignmentHistory.assignmentId, assignmentId)); + } + + const history = await query + .orderBy(desc(departmentDomainAssignmentHistory.createdAt)) + .limit(100); // 최근 100개 제한 + + return history; + } catch (error) { + console.error("부서별 도메인 할당 히스토리 조회 실패:", error); + return []; + } + }, + [`department-domain-assignment-history-${assignmentId || 'all'}`], + { + revalidate: 1800, // 30분 캐시 + tags: ["department-domain-assignment-history"], + } + )(); +} + +// 도메인별 통계 조회 +export async function getDepartmentDomainStats() { + return unstable_cache( + async () => { + try { + const stats = await db + .select({ + domain: departmentDomainAssignments.assignedDomain, + count: db.$count(departmentDomainAssignments), + }) + .from(departmentDomainAssignments) + .where(eq(departmentDomainAssignments.isActive, true)) + .groupBy(departmentDomainAssignments.assignedDomain); + + return stats; + } catch (error) { + console.error("도메인별 통계 조회 실패:", error); + return []; + } + }, + ["department-domain-stats"], + { + revalidate: 3600, + tags: ["department-domain-assignments"], + } + )(); +}
\ No newline at end of file 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: "삼성중공업" + }; +} |
