diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/knox-api/approval/approval.ts | 74 | ||||
| -rw-r--r-- | lib/knox-api/approval/service.ts | 140 | ||||
| -rw-r--r-- | lib/knox-api/common.ts | 1 | ||||
| -rw-r--r-- | lib/knox-sync/employee-sync-service.ts | 3 | ||||
| -rw-r--r-- | lib/knox-sync/master-sync-service.ts | 2 | ||||
| -rw-r--r-- | lib/users/service.ts | 95 |
6 files changed, 304 insertions, 11 deletions
diff --git a/lib/knox-api/approval/approval.ts b/lib/knox-api/approval/approval.ts index 75066478..5e62382d 100644 --- a/lib/knox-api/approval/approval.ts +++ b/lib/knox-api/approval/approval.ts @@ -1,6 +1,8 @@ "use server" import { getKnoxConfig, createJsonHeaders, createFormHeaders } from '../common'; +import { randomUUID } from 'crypto'; +import { saveApprovalToDatabase, deleteApprovalFromDatabase } from './service'; // Knox API Approval 서버 액션들 // 가이드: lib/knox-api/approval/guide.html @@ -15,7 +17,7 @@ export interface BaseResponse { // 결재 경로 타입 export interface ApprovalLine { epId?: string; - userId?: string; + userId?: string; // eVCP ID라서 사용하지 않음! emailAddress?: string; seq: string; role: string; // 기안(0), 결재(1), 합의(2), 후결(3), 병렬합의(4), 병렬결재(7), 통보(9) @@ -132,7 +134,8 @@ export interface ApprovalIdsResponse extends BaseResponse { * POST /approval/api/v2.0/approvals/submit */ export async function submitApproval( - request: SubmitApprovalRequest + request: SubmitApprovalRequest, + userInfo: { userId: string; epId: string; emailAddress: string } ): Promise<SubmitApprovalResponse> { try { const config = await getKnoxConfig(); @@ -149,8 +152,8 @@ export async function submitApproval( timeZone: request.timeZone, docMngSaveCode: request.docMngSaveCode, subject: request.subject, - sbmLang: request.sbmLang, - apInfId: request.apInfId, + sbmLang: request.sbmLang || 'ko', + apInfId: request.apInfId, // 고정값, 환경변수로 설정해 common 에서 가져오기 importantYn: request.importantYn, aplns: request.aplns }; @@ -174,7 +177,28 @@ export async function submitApproval( throw new Error(`결재 상신 실패: ${response.status}`); } - return await response.json(); + const result = await response.json(); + + // Knox API 성공 시 데이터베이스에 저장 + if (result.result === 'SUCCESS') { + try { + await saveApprovalToDatabase( + request.apInfId, + userInfo.userId, + userInfo.epId, + userInfo.emailAddress, + request.subject, + request.contents, + request.aplns + ); + } catch (dbError) { + console.error('데이터베이스 저장 실패:', dbError); + // 데이터베이스 저장 실패는 Knox API 성공을 무효화하지 않음 + // 필요시 별도 처리 로직 추가 + } + } + + return result; } catch (error) { console.error('결재 상신 오류:', error); throw error; @@ -426,7 +450,20 @@ export async function cancelApproval( throw new Error(`상신 취소 실패: ${response.status}`); } - return await response.json(); + const result = await response.json(); + + // Knox API 성공 시 데이터베이스에서 삭제 + if (result.result === 'SUCCESS') { + try { + await deleteApprovalFromDatabase(apInfId); + } catch (dbError) { + console.error('데이터베이스 삭제 실패:', dbError); + // 데이터베이스 삭제 실패는 Knox API 성공을 무효화하지 않음 + // 필요시 별도 처리 로직 추가 + } + } + + return result; } catch (error) { console.error('상신 취소 오류:', error); throw error; @@ -501,10 +538,29 @@ export async function createSubmitApprovalRequest( approvalLines: ApprovalLine[], options: Partial<SubmitApprovalRequest> = {} ): Promise<SubmitApprovalRequest> { - const config = await getKnoxConfig(); + + // 요구하는 날짜 형식으로 변환 (YYYYMMDDHHMMSS) - UTC 기준 (타임존 정보를 GTC+9 로 제공하고 있음) const now = new Date(); - const sbmDt = now.toISOString().replace(/[-:T]/g, '').slice(0, 14); - const apInfId = `${config.systemId}${sbmDt}${Math.random().toString(36).substr(2, 9)}`.padEnd(32, '0'); + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone: 'UTC', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + + const parts = formatter.formatToParts(now); + const sbmDt = parts + .filter((part) => part.type !== 'literal') + .map((part) => part.value) + .join(''); + + + // EVCP 접두어 뒤에 28자리 무작위 문자열을 붙여 32byte 고유 ID 생성 + const apInfId = `EVCP${randomUUID().replace(/-/g, '').slice(0, 28)}`; return { contents, diff --git a/lib/knox-api/approval/service.ts b/lib/knox-api/approval/service.ts new file mode 100644 index 00000000..6ef1b1f6 --- /dev/null +++ b/lib/knox-api/approval/service.ts @@ -0,0 +1,140 @@ +"use server" +import db from '@/db/db'; +import { ApprovalLine } from "./approval"; +import { approval } from '@/db/schema/knox/approvals'; +import { eq, and } from 'drizzle-orm'; + +// ========== 데이터베이스 서비스 함수들 ========== + + + +/** + * 결재 상신 데이터를 데이터베이스에 저장 + */ +export async function saveApprovalToDatabase( + apInfId: string, + userId: string, + epId: string, + emailAddress: string, + subject: string, + content: string, + aplns: ApprovalLine[] +): Promise<void> { + try { + await db.insert(approval).values({ + apInfId, + userId, + epId, + emailAddress, + subject, + content, + status: '1', // 진행중 상태로 초기 설정 + aplns, + isDeleted: false, + createdAt: new Date(), + updatedAt: new Date(), + }); + } catch (error) { + console.error('결재 데이터 저장 실패:', error); + throw new Error( + '결재 데이터를 데이터베이스에 저장하는 중 오류가 발생했습니다.' + ); + } +} + +/** + * 결재 상태 업데이트 + */ +export async function updateApprovalStatus( + apInfId: string, + status: string +): Promise<void> { + try { + await db + .update(approval) + .set({ + status, + updatedAt: new Date(), + }) + .where(eq(approval.apInfId, apInfId)); + } catch (error) { + console.error('결재 상태 업데이트 실패:', error); + throw new Error('결재 상태를 업데이트하는 중 오류가 발생했습니다.'); + } +} + +/** + * 결재 상세 정보 조회 + */ +export async function getApprovalFromDatabase( + apInfId: string, + includeDeleted: boolean = false +): Promise<typeof approval.$inferSelect | null> { + try { + const whereCondition = includeDeleted + ? eq(approval.apInfId, apInfId) + : and(eq(approval.apInfId, apInfId), eq(approval.isDeleted, false)); + + const result = await db + .select() + .from(approval) + .where(whereCondition) + .limit(1); + + return result[0] || null; + } catch (error) { + console.error('결재 데이터 조회 실패:', error); + throw new Error('결재 데이터를 조회하는 중 오류가 발생했습니다.'); + } +} + +/** + * 사용자별 결재 목록 조회 + */ +export async function getApprovalsByUser( + userId: string, + limit: number = 50, + offset: number = 0, + includeDeleted: boolean = false +): Promise<typeof approval.$inferSelect[]> { + try { + const whereCondition = includeDeleted + ? eq(approval.userId, userId) + : and(eq(approval.userId, userId), eq(approval.isDeleted, false)); + + const result = await db + .select() + .from(approval) + .where(whereCondition) + .orderBy(approval.createdAt) + .limit(limit) + .offset(offset); + + return result; + } catch (error) { + console.error('사용자 결재 목록 조회 실패:', error); + throw new Error('사용자 결재 목록을 조회하는 중 오류가 발생했습니다.'); + } +} + +/** + * 결재 삭제 (상신 취소 시) - Soft Delete + */ +export async function deleteApprovalFromDatabase( + apInfId: string +): Promise<void> { + try { + await db + .update(approval) + .set({ + isDeleted: true, + updatedAt: new Date(), + }) + .where(eq(approval.apInfId, apInfId)); + } catch (error) { + console.error('결재 데이터 삭제 실패:', error); + throw new Error('결재 데이터를 삭제하는 중 오류가 발생했습니다.'); + } +} + + diff --git a/lib/knox-api/common.ts b/lib/knox-api/common.ts index 4c037e56..db6910f2 100644 --- a/lib/knox-api/common.ts +++ b/lib/knox-api/common.ts @@ -6,6 +6,7 @@ export interface KnoxConfig { baseUrl: string; systemId: string; + apInfId?: string; // 환경변수에서 주입 (고정값) bearerToken: string; } diff --git a/lib/knox-sync/employee-sync-service.ts b/lib/knox-sync/employee-sync-service.ts index 3e8b048e..b7f2a323 100644 --- a/lib/knox-sync/employee-sync-service.ts +++ b/lib/knox-sync/employee-sync-service.ts @@ -198,6 +198,7 @@ async function syncEmployeesToUsers(): Promise<void> { departmentCode: employeeTable.departmentCode, departmentName: employeeTable.departmentName, companyCode: employeeTable.companyCode, + epId: employeeTable.epId, }) .from(employeeTable) .where( @@ -271,6 +272,7 @@ async function syncEmployeesToUsers(): Promise<void> { deptCode: employee.departmentCode, deptName: employee.departmentName, domain: assignedDomain as UserDomainType, + epId: employee.epId, updatedAt: new Date(), }) .where(eq(users.id, existingUsers[0].id)); @@ -295,6 +297,7 @@ async function syncEmployeesToUsers(): Promise<void> { deptCode: employee.departmentCode, deptName: employee.departmentName, domain: assignedDomain as UserDomainType, + epId: employee.epId, }); insertCount++; diff --git a/lib/knox-sync/master-sync-service.ts b/lib/knox-sync/master-sync-service.ts index 5cabe9ed..ed77a3fd 100644 --- a/lib/knox-sync/master-sync-service.ts +++ b/lib/knox-sync/master-sync-service.ts @@ -67,6 +67,6 @@ export async function startKnoxMasterSyncScheduler() { syncAllKnoxData().catch(console.error); }); + // 직급 정보 기반으로, 각 직급마다 임직원 조회 (Knox API 구조상 제한으로 직급 및 부서마다 조회 가능하며, 직급이 수가 더 적음(600 vs 2400)) logSchedulerInfo('통합(직급→조직→임직원)', CRON_STRING); - console.log('[KNOX-SYNC] 💡 순차 실행으로 의존성 문제 해결 (직급 완료 → 조직 완료 → 임직원 완료)'); }
\ No newline at end of file diff --git a/lib/users/service.ts b/lib/users/service.ts index 80c346fa..90ee170e 100644 --- a/lib/users/service.ts +++ b/lib/users/service.ts @@ -13,7 +13,7 @@ import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, ne } from "drizzle-orm"; +import { and, or, desc, asc, ilike, eq, isNull, isNotNull, sql, count, inArray, ne, not } from "drizzle-orm"; import { SaveFileResult, saveFile } from '../file-stroage'; interface AssignUsersArgs { @@ -1012,3 +1012,96 @@ export async function getUserRoles(userId: number): Promise<string[]> { } } +/** + * 사용자 선택기용 간단한 사용자 검색 함수 + */ +export async function searchUsersForSelector( + query: string, + page: number = 1, + perPage: number = 10, + domainFilter?: { type: "exclude" | "include"; domains: string[] } | null +) { + try { + const offset = (page - 1) * perPage; + + // 이름 검색 조건 + let searchWhere; + if (query.trim()) { + const searchPattern = `%${query.trim()}%`; + searchWhere = ilike(users.name, searchPattern); + } + + // 도메인 필터 조건 + let domainWhere; + if (domainFilter && domainFilter.domains.length > 0) { + if (domainFilter.type === "include") { + domainWhere = inArray(users.domain, domainFilter.domains); + } else if (domainFilter.type === "exclude") { + domainWhere = not(inArray(users.domain, domainFilter.domains)); + } + } + + // 활성 사용자만 + const activeWhere = eq(users.isActive, true); + + // 최종 조건 + const finalWhere = and(searchWhere, domainWhere, activeWhere); + + const { data, total } = await db.transaction(async (tx) => { + const data = await tx + .select({ + id: users.id, + epId: users.epId, + name: users.name, + email: users.email, + deptCode: users.deptCode, + deptName: users.deptName, + imageUrl: users.imageUrl, + domain: users.domain, + }) + .from(users) + .where(finalWhere) + .orderBy(asc(users.name)) + .limit(perPage) + .offset(offset); + + const totalResult = await tx + .select({ count: count() }) + .from(users) + .where(finalWhere); + + return { data, total: totalResult[0].count }; + }); + + const pageCount = Math.ceil(total / perPage); + + return { + success: true, + data, + pagination: { + page, + perPage, + total, + pageCount, + hasNextPage: page < pageCount, + hasPrevPage: page > 1, + }, + }; + } catch (error) { + console.error("사용자 검색 오류:", error); + return { + success: false, + data: [], + pagination: { + page: 1, + perPage, + total: 0, + pageCount: 0, + hasNextPage: false, + hasPrevPage: false, + }, + error: "사용자 검색 중 오류가 발생했습니다.", + }; + } +} + |
