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 | |
| parent | f828b24261b0e3661d4ab0ac72b63431887f35bd (diff) | |
(김준회) 결재 이력조회 기능 추가 및 로그 테이블 확장, 테스트모듈 작성
Diffstat (limited to 'lib/knox-api')
| -rw-r--r-- | lib/knox-api/approval/approval.ts | 412 | ||||
| -rw-r--r-- | lib/knox-api/approval/service.ts | 107 |
2 files changed, 500 insertions, 19 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: [], + }; + } +} diff --git a/lib/knox-api/approval/service.ts b/lib/knox-api/approval/service.ts index 0bd817a6..4908f984 100644 --- a/lib/knox-api/approval/service.ts +++ b/lib/knox-api/approval/service.ts @@ -9,7 +9,8 @@ import { eq, and } from 'drizzle-orm'; /** - * 결재 상신 데이터를 데이터베이스에 저장 + * 결재 상신 데이터를 데이터베이스에 저장 (upsert) + * 상신, 상세조회 업데이트, 리스트조회 상태 업데이트 시 모두 사용 */ export async function saveApprovalToDatabase( apInfId: string, @@ -18,22 +19,56 @@ export async function saveApprovalToDatabase( emailAddress: string, subject: string, content: string, - aplns: ApprovalLine[] + aplns: ApprovalLine[], + additionalData?: { + contentsType?: string; + urgYn?: string; + importantYn?: string; + docSecuType?: string; + notifyOption?: string; + docMngSaveCode?: string; + sbmLang?: string; + timeZone?: string; + sbmDt?: string; + status?: string; // 상태 업데이트를 위한 옵션 + } ): Promise<void> { try { - await db.insert(approvalLogs).values({ + const now = new Date(); + const dataToUpsert = { apInfId, userId, epId, emailAddress, subject, content, - status: '1', // 진행중 상태로 초기 설정 + contentsType: additionalData?.contentsType || 'HTML', + status: additionalData?.status || '1', // 기본값: 진행중 + urgYn: additionalData?.urgYn || 'N', + importantYn: additionalData?.importantYn || 'N', + docSecuType: additionalData?.docSecuType || 'PERSONAL', + notifyOption: additionalData?.notifyOption || '0', + docMngSaveCode: additionalData?.docMngSaveCode || '0', + sbmLang: additionalData?.sbmLang || 'ko', + timeZone: additionalData?.timeZone || 'GMT+9', + sbmDt: additionalData?.sbmDt, aplns, isDeleted: false, - createdAt: new Date(), - updatedAt: new Date(), - }); + updatedAt: now, + }; + + await db.insert(approvalLogs) + .values({ + ...dataToUpsert, + createdAt: now, + }) + .onConflictDoUpdate({ + target: approvalLogs.apInfId, + set: { + ...dataToUpsert, + // createdAt은 업데이트하지 않음 (최초 생성 시점 유지) + } + }); } catch (error) { console.error('결재 데이터 저장 실패:', error); throw new Error( @@ -43,7 +78,7 @@ export async function saveApprovalToDatabase( } /** - * 결재 상태 업데이트 + * 결재 상태만 업데이트 (기존 레코드가 있는 경우에만) */ export async function updateApprovalStatus( apInfId: string, @@ -64,6 +99,62 @@ export async function updateApprovalStatus( } /** + * 결재 상태를 upsert로 업데이트 (상세정보 없이 상태만 알고 있는 경우) + * Knox API에서 상태 조회 시 상세정보가 없을 때 사용 + */ +export async function upsertApprovalStatus( + apInfId: string, + status: string, + fallbackData?: { + userId?: string; + epId?: string; + emailAddress?: string; + subject?: string; + content?: string; + } +): Promise<void> { + try { + const now = new Date(); + + // 먼저 기존 레코드 조회 + const existingRecord = await getApprovalFromDatabase(apInfId, true); + + if (existingRecord) { + // 기존 레코드가 있으면 상태만 업데이트 + await updateApprovalStatus(apInfId, status); + } else if (fallbackData?.userId && fallbackData?.epId && fallbackData?.emailAddress) { + // 기존 레코드가 없고 fallback 데이터가 있으면 새로 생성 + await db.insert(approvalLogs).values({ + apInfId, + userId: fallbackData.userId, + epId: fallbackData.epId, + emailAddress: fallbackData.emailAddress, + subject: fallbackData.subject || `결재 ${apInfId}`, + content: fallbackData.content || `상태 동기화로 생성된 결재`, + contentsType: 'TEXT', + status, + urgYn: 'N', + importantYn: 'N', + docSecuType: 'PERSONAL', + notifyOption: '0', + docMngSaveCode: '0', + sbmLang: 'ko', + timeZone: 'GMT+9', + aplns: [], + isDeleted: false, + createdAt: now, + updatedAt: now, + }); + } else { + console.warn(`결재 상태 업데이트 건너뜀: ${apInfId} - 기존 레코드 없음, fallback 데이터 부족`); + } + } catch (error) { + console.error('결재 상태 upsert 실패:', error); + throw new Error('결재 상태를 upsert하는 중 오류가 발생했습니다.'); + } +} + +/** * 결재 상세 정보 조회 */ export async function getApprovalFromDatabase( |
