diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-10 08:59:19 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-10 08:59:19 +0000 |
| commit | 26bd5a0af8f69fd693c16d2eacb35cf138a360d1 (patch) | |
| tree | 1d770ede1824dee37c0bff651b8844b6551284e6 /lib/knox-api/approval/approval.ts | |
| parent | f828b24261b0e3661d4ab0ac72b63431887f35bd (diff) | |
(김준회) 결재 이력조회 기능 추가 및 로그 테이블 확장, 테스트모듈 작성
Diffstat (limited to 'lib/knox-api/approval/approval.ts')
| -rw-r--r-- | lib/knox-api/approval/approval.ts | 412 |
1 files changed, 401 insertions, 11 deletions
diff --git a/lib/knox-api/approval/approval.ts b/lib/knox-api/approval/approval.ts index fe78c8be..fba7ef04 100644 --- a/lib/knox-api/approval/approval.ts +++ b/lib/knox-api/approval/approval.ts @@ -2,7 +2,7 @@ import { getKnoxConfig, createJsonHeaders, createFormHeaders } from '../common'; import { randomUUID } from 'crypto'; -import { saveApprovalToDatabase, deleteApprovalFromDatabase } from './service'; +import { saveApprovalToDatabase, deleteApprovalFromDatabase, upsertApprovalStatus } from './service'; import { debugLog, debugError } from '@/lib/debug-utils' // Knox API Approval 서버 액션들 @@ -202,13 +202,24 @@ export async function submitApproval( if (result.result === 'success') { try { await saveApprovalToDatabase( - request.apInfId, - userInfo.userId, + request.apInfId, // 개별 결재의 ID (결재연계ID) + userInfo.userId, // eVCP userInfo.epId, userInfo.emailAddress, request.subject, request.contents, - request.aplns + request.aplns, + { + contentsType: request.contentsType, + urgYn: request.urgYn, + importantYn: request.importantYn, + docSecuType: request.docSecuType, + notifyOption: request.notifyOption, + docMngSaveCode: request.docMngSaveCode, + sbmLang: request.sbmLang, + timeZone: request.timeZone, + sbmDt: request.sbmDt, + } ); } catch (dbError) { console.error('데이터베이스 저장 실패:', dbError); @@ -229,7 +240,8 @@ export async function submitApproval( * POST /approval/api/v2.0/approvals/secu-submit */ export async function submitSecurityApproval( - request: SubmitApprovalRequest + request: SubmitApprovalRequest, + userInfo?: { userId: string; epId: string; emailAddress: string } ): Promise<SubmitApprovalResponse> { try { const config = await getKnoxConfig(); @@ -291,6 +303,36 @@ export async function submitSecurityApproval( } } + // Knox API 성공 시 데이터베이스에 저장 (사용자 정보가 있는 경우만) + if (result.result === 'success' && userInfo) { + try { + await saveApprovalToDatabase( + request.apInfId, + userInfo.userId, + userInfo.epId, + userInfo.emailAddress, + request.subject, + request.contents, + request.aplns, + { + contentsType: request.contentsType, + urgYn: request.urgYn, + importantYn: request.importantYn, + docSecuType: request.docSecuType, + notifyOption: request.notifyOption, + docMngSaveCode: request.docMngSaveCode, + sbmLang: request.sbmLang, + timeZone: request.timeZone, + sbmDt: request.sbmDt, + } + ); + } catch (dbError) { + console.error('보안 결재 데이터베이스 저장 실패:', dbError); + // 데이터베이스 저장 실패는 Knox API 성공을 무효화하지 않음 + // 필요시 별도 처리 로직 추가 + } + } + return result; } catch (error) { debugError('보안 결재 상신 오류', error); @@ -407,23 +449,54 @@ export async function getApprovalIds( } } +// 상신함 리스트 조회 요청 타입 +export interface SubmissionListRequest { + epId?: string; + userId?: string; + emailAddress?: string; + [key: string]: string | undefined; // 추가 파라미터 지원 +} + /** * 상신함 리스트 조회 * GET /approval/api/v2.0/approvals/submission + * + * epId, userId, emailAddress 중 최소 하나는 필수 + * 우선순위: userId > epId > emailAddress + * 여기서의 userId는 knox email 주소 앞부분을 지칭함 */ export async function getSubmissionList( - params?: Record<string, string> + userParams: SubmissionListRequest, + additionalParams?: Record<string, string> ): Promise<SubmissionListResponse> { try { + // 사용자 식별 파라미터 중 하나는 필수 + if (!userParams.epId && !userParams.userId && !userParams.emailAddress) { + throw new Error('epId, userId, emailAddress 중 최소 하나는 필요합니다.'); + } + const config = await getKnoxConfig(); - let url = `${config.baseUrl}/approval/api/v2.0/approvals/submission`; + const url = new URL(`${config.baseUrl}/approval/api/v2.0/approvals/submission`); - if (params) { - const searchParams = new URLSearchParams(params); - url += `?${searchParams.toString()}`; + // 사용자 식별 파라미터 추가 (우선순위에 따라) + if (userParams.userId) { + url.searchParams.set('userId', userParams.userId); + } else if (userParams.epId) { + url.searchParams.set('epId', userParams.epId); + } else if (userParams.emailAddress) { + url.searchParams.set('emailAddress', userParams.emailAddress); + } + + // 추가 파라미터가 있으면 추가 + if (additionalParams) { + Object.entries(additionalParams).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.set(key, value); + } + }); } - const response = await fetch(url, { + const response = await fetch(url.toString(), { method: 'GET', headers: await createJsonHeaders(), }); @@ -681,3 +754,320 @@ export async function getApprovalRoleText(role: string): Promise<string> { return roleMap[role] || '알 수 없음'; } + +// ========== 서버 액션 함수들 ========== + +/** + * 결재상황 일괄 조회 및 데이터베이스 업데이트 서버 액션 + * 데이터베이스에 저장된 모든 결재건들의 상태를 Knox API로 조회하여 업데이트 + */ +export async function syncApprovalStatusAction(): Promise<{ + success: boolean; + message: string; + updated: number; + failed: string[]; +}> { + "use server"; + + try { + const db = (await import('@/db/db')).default; + const { approvalLogs } = await import('@/db/schema/knox/approvals'); + const { eq, and, inArray } = await import('drizzle-orm'); + + // 1. 진행중인 결재건들 조회 (암호화중부터 진행중까지) + const pendingApprovals = await db + .select({ + apInfId: approvalLogs.apInfId, + status: approvalLogs.status, + }) + .from(approvalLogs) + .where( + and( + eq(approvalLogs.isDeleted, false), + // 암호화중(-2), 예약상신(-1), 보류(0), 진행중(1) 상태만 조회 + // 완결(2), 반려(3), 상신취소(4), 전결(5), 후완결(6)은 제외 + inArray(approvalLogs.status, ['-2', '-1', '0', '1']) + ) + ); + + if (pendingApprovals.length === 0) { + return { + success: true, + message: "업데이트할 결재건이 없습니다.", + updated: 0, + failed: [], + }; + } + + // 2. Knox API 호출을 위한 요청 데이터 구성 + const apinfids = pendingApprovals.map(approval => ({ + apinfid: approval.apInfId + })); + + // 3. Knox API로 결재 상황 조회 (최대 1000건씩 처리) + const batchSize = 1000; + let updated = 0; + const failed: string[] = []; + + for (let i = 0; i < apinfids.length; i += batchSize) { + const batch = apinfids.slice(i, i + batchSize); + + try { + const statusResponse = await getApprovalStatus({ + apinfids: batch + }); + + if (statusResponse.result === 'success' && statusResponse.data) { + // 4. 조회된 상태로 데이터베이스 업데이트 + for (const statusData of statusResponse.data) { + try { + // 기존 상태 조회 + const currentApproval = pendingApprovals.find( + approval => approval.apInfId === statusData.apInfId + ); + + if (currentApproval && currentApproval.status !== statusData.status) { + // upsert를 사용한 상태 업데이트 + await upsertApprovalStatus(statusData.apInfId, statusData.status); + updated++; + } + } catch (updateError) { + console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); + failed.push(statusData.apInfId); + } + } + } else { + console.error('Knox API 결재상황조회 실패:', statusResponse); + // 배치 전체를 실패로 처리 + batch.forEach(item => failed.push(item.apinfid)); + } + } catch (batchError) { + console.error('배치 처리 실패:', batchError); + // 배치 전체를 실패로 처리 + batch.forEach(item => failed.push(item.apinfid)); + } + } + + const successMessage = `결재상황 동기화 완료: ${updated}건 업데이트${failed.length > 0 ? `, ${failed.length}건 실패` : ''}`; + + console.log(successMessage, { + totalRequested: pendingApprovals.length, + updated, + failedCount: failed.length, + failedApinfIds: failed + }); + + return { + success: true, + message: successMessage, + updated, + failed, + }; + + } catch (error) { + console.error('결재상황 동기화 중 오류:', error); + return { + success: false, + message: `결재상황 동기화 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + updated: 0, + failed: [], + }; + } +} + +/** + * 특정 결재건들의 상태만 조회 및 업데이트하는 서버 액션 + */ +export async function syncSpecificApprovalStatusAction( + apInfIds: string[] +): Promise<{ + success: boolean; + message: string; + updated: number; + failed: string[]; +}> { + "use server"; + + try { + if (!apInfIds || apInfIds.length === 0) { + return { + success: false, + message: "조회할 결재 ID가 없습니다.", + updated: 0, + failed: [], + }; + } + + // Knox API 호출을 위한 요청 데이터 구성 + const apinfids = apInfIds.map(apInfId => ({ + apinfid: apInfId + })); + + let updated = 0; + const failed: string[] = []; + + // Knox API로 결재 상황 조회 + try { + const statusResponse = await getApprovalStatus({ + apinfids + }); + + if (statusResponse.result === 'success' && statusResponse.data) { + // 조회된 상태로 데이터베이스 업데이트 + for (const statusData of statusResponse.data) { + try { + // upsert를 사용한 상태 업데이트 + await upsertApprovalStatus(statusData.apInfId, statusData.status); + updated++; + } catch (updateError) { + console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); + failed.push(statusData.apInfId); + } + } + } else { + console.error('Knox API 결재상황조회 실패:', statusResponse); + apInfIds.forEach(id => failed.push(id)); + } + } catch (apiError) { + console.error('Knox API 호출 실패:', apiError); + apInfIds.forEach(id => failed.push(id)); + } + + const successMessage = `지정된 결재건 상태 동기화 완료: ${updated}건 업데이트${failed.length > 0 ? `, ${failed.length}건 실패` : ''}`; + + console.log(successMessage, { + requestedIds: apInfIds, + updated, + failedCount: failed.length, + failedApinfIds: failed + }); + + return { + success: true, + message: successMessage, + updated, + failed, + }; + + } catch (error) { + console.error('특정 결재상황 동기화 중 오류:', error); + return { + success: false, + message: `결재상황 동기화 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + updated: 0, + failed: apInfIds, + }; + } +} + +/** + * 데이터베이스에서 결재 로그 목록 조회 서버 액션 + */ + +/** + * 결재 상태가 완료/실패로 변경되었는지 확인 + */ +export async function isApprovalStatusFinal(status: string): Promise<boolean> { + // 완결(2), 반려(3), 상신취소(4), 전결(5), 후완결(6) + return ['2', '3', '4', '5', '6'].includes(status); +} + +/** + * 결재 상태가 성공인지 확인 + */ +export async function isApprovalStatusSuccess(status: string): Promise<boolean> { + // 완결(2), 전결(5), 후완결(6) + return ['2', '5', '6'].includes(status); +} + +/** + * 결재 상태가 실패인지 확인 + */ +export async function isApprovalStatusFailure(status: string): Promise<boolean> { + // 반려(3), 상신취소(4) + return ['3', '4'].includes(status); +} + +export async function getApprovalLogsAction(): Promise<{ + success: boolean; + message: string; + data: Array<{ + apInfId: string; + subject: string; + sbmDt: string; + status: string; + urgYn?: string; + docSecuType?: string; + userId?: string; + epId?: string; + emailAddress?: string; + }>; +}> { + "use server"; + + try { + const db = (await import('@/db/db')).default; + const { approvalLogs } = await import('@/db/schema/knox/approvals'); + const { eq, desc } = await import('drizzle-orm'); + + // 데이터베이스에서 결재 로그 조회 (삭제되지 않은 것만) + const logs = await db + .select({ + apInfId: approvalLogs.apInfId, + userId: approvalLogs.userId, + epId: approvalLogs.epId, + emailAddress: approvalLogs.emailAddress, + subject: approvalLogs.subject, + content: approvalLogs.content, + contentsType: approvalLogs.contentsType, + status: approvalLogs.status, + urgYn: approvalLogs.urgYn, + importantYn: approvalLogs.importantYn, + docSecuType: approvalLogs.docSecuType, + notifyOption: approvalLogs.notifyOption, + docMngSaveCode: approvalLogs.docMngSaveCode, + sbmLang: approvalLogs.sbmLang, + timeZone: approvalLogs.timeZone, + sbmDt: approvalLogs.sbmDt, + createdAt: approvalLogs.createdAt, + updatedAt: approvalLogs.updatedAt, + }) + .from(approvalLogs) + .where(eq(approvalLogs.isDeleted, false)) + .orderBy(desc(approvalLogs.createdAt)); + + // ApprovalList 컴포넌트에서 기대하는 형식으로 데이터 변환 + const formattedData = logs.map(log => ({ + apInfId: log.apInfId, + subject: log.subject, + sbmDt: log.sbmDt || log.createdAt.toISOString().replace(/[-:T]/g, '').slice(0, 14), // YYYYMMDDHHMMSS 형식 + status: log.status, + urgYn: log.urgYn || undefined, + docSecuType: log.docSecuType || undefined, + userId: log.userId || undefined, + epId: log.epId, + emailAddress: log.emailAddress, + // 추가 정보 + contentsType: log.contentsType, + importantYn: log.importantYn || undefined, + notifyOption: log.notifyOption, + docMngSaveCode: log.docMngSaveCode, + sbmLang: log.sbmLang, + timeZone: log.timeZone, + })); + + return { + success: true, + message: `${formattedData.length}건의 결재 로그를 조회했습니다.`, + data: formattedData, + }; + + } catch (error) { + console.error('결재 로그 조회 중 오류:', error); + return { + success: false, + message: `결재 로그 조회 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + data: [], + }; + } +} |
