"use server" import { getKnoxConfig, createJsonHeaders, createFormHeaders } from '../common'; import { randomUUID } from 'crypto'; import { saveApprovalToDatabase, deleteApprovalFromDatabase, upsertApprovalStatus } from './service'; import { debugLog, debugError } from '@/lib/debug-utils' // Knox API Approval 서버 액션들 // 가이드: lib/knox-api/approval/guide.html // ========== 타입 정의 ========== // 공통 응답 타입 export interface BaseResponse { result: string; } // 결재 경로 타입 export interface ApprovalLine { epId?: string; userId?: string; // eVCP ID라서 사용하지 않음! emailAddress?: string; seq: string; role: string; // 기안(0), 결재(1), 합의(2), 후결(3), 병렬합의(4), 병렬결재(7), 통보(9) aplnStatsCode: string; // 미결(0), 결재(1), 반려(2), 전결(3), 자동결재(5) arbPmtYn: string; // 전결권한여부 contentsMdfyPmtYn: string; // 본문수정권한여부 aplnMdfyPmtYn: string; // 경로변경권한여부 opinion?: string; // 상신의견 (상신자만) } // 결재 상신 요청 타입 export interface SubmitApprovalRequest { contents: string; // 결재본문 contentsType: string; // 본문종류 (TEXT, HTML, MIME) docSecuType: string; // 보안문서타입 (PERSONAL, CONFIDENTIAL, CONFIDENTIAL_STRICT) notifyOption: string; // 통보옵션 (0-3) urgYn: string; // 긴급여부 (Y/N) sbmDt: string; // 상신일시 (YYYYMMDDHHMMSS) timeZone: string; // 타임존 (GMT, GMT+9 등) docMngSaveCode: string; // 문서관리저장코드 (0: 안함, 1: 저장) subject: string; // 결재제목 sbmLang: string; // 상신언어 (ko, ja, zh, en) apInfId: string; // 연계ID (32자리 고유값) importantYn?: string; // 중요여부 (Y/N) aplns: ApprovalLine[]; // 결재경로 attachments?: File[]; // 첨부파일 } // 결재 상신 응답 타입 export interface SubmitApprovalResponse extends BaseResponse { data: { apInfId: string; }; } // 결재 상세 조회 응답 타입 export interface ApprovalDetailResponse extends BaseResponse { data: { contentsType: string; sbmDt: string; sbmLang: string; apInfId: string; systemId: string; notifyOption: string; urgYn: string; docSecuType: string; status: string; // 암호화실패(-3), 암호화중(-2), 예약상신(-1), 보류(0), 진행중(1), 완결(2), 반려(3), 상신취소(4), 전결(5), 후완결(6) timeZone: string; subject: string; aplns: ApprovalLine[]; attachments?: File[]; }; } // 결재 본문 조회 응답 타입 export interface ApprovalContentResponse extends BaseResponse { data: { contents: string; contentsType: string; apInfId: string; }; } // 결재 상황 조회 요청 타입 export interface ApprovalStatusRequest { apinfids: { apinfid: string }[]; } // 결재 상황 조회 응답 타입 export interface ApprovalStatusResponse extends BaseResponse { data: { apInfId: string; docChgNum: string; status: string; }[]; } // 상신 취소 응답 타입 export interface CancelApprovalResponse extends BaseResponse { data: { apInfId: string; }; } // 개인 결재경로 목록 조회 응답 타입 export interface OwnApprovalLineListResponse extends BaseResponse { data: ApprovalLine[]; } // 개인 결재경로 상세 조회 응답 타입 export interface OwnApprovalLineDetailResponse extends BaseResponse { data: ApprovalLine; } // 상신함 리스트 조회 응답 타입 export interface SubmissionListResponse extends BaseResponse { data: SubmitApprovalRequest[]; } // 연계 이력 조회 응답 타입 export interface ApprovalHistoryResponse extends BaseResponse { data: SubmitApprovalRequest[]; } // 연계 ID 조회 응답 타입 export interface ApprovalIdsResponse extends BaseResponse { data: string[]; } // ========== 서버 액션 함수들 ========== /** * 결재 상신 * POST /approval/api/v2.0/approvals/submit */ export async function submitApproval( request: SubmitApprovalRequest, userInfo: { userId: string; epId: string; emailAddress: string } ): Promise { try { const config = await getKnoxConfig(); const formData = new FormData(); // JSON 데이터 생성 const approvalData = { contents: request.contents, contentsType: request.contentsType, docSecuType: request.docSecuType, notifyOption: request.notifyOption, urgYn: request.urgYn, sbmDt: request.sbmDt, timeZone: request.timeZone, docMngSaveCode: request.docMngSaveCode, subject: request.subject, sbmLang: request.sbmLang || 'ko', apInfId: request.apInfId, // 고정값, 환경변수로 설정해 common 에서 가져오기 importantYn: request.importantYn, aplns: request.aplns }; formData.append('approval', JSON.stringify(approvalData)); // 첨부파일 처리 if (request.attachments) { request.attachments.forEach((file) => { formData.append('attachments', file); }); } const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/submit`, { method: 'POST', headers: await createFormHeaders(), body: formData, }); if (!response.ok) { const errorText = await response.text(); debugError('API Error Response', errorText); throw new Error(`결재 상신 실패: ${response.status} - ${errorText}`); } // 응답의 Content-Type 확인 및 charset 처리 const contentType = response.headers.get('content-type'); let result; if (contentType && contentType.includes('application/json')) { result = await response.json(); } else { // JSON이 아닌 경우 텍스트로 읽고 JSON 파싱 시도 const text = await response.text(); debugLog('Raw response text', text); try { result = JSON.parse(text); } catch (parseError) { console.error('JSON parse error:', parseError); throw new Error(`응답 파싱 실패: ${text}`); } } // Knox API 성공 시 데이터베이스에 저장 if (result.result === 'success') { try { await saveApprovalToDatabase( request.apInfId, // 개별 결재의 ID (결재연계ID) userInfo.userId, // eVCP 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); throw error; } } /** * 보안 결재 상신 * POST /approval/api/v2.0/approvals/secu-submit */ export async function submitSecurityApproval( request: SubmitApprovalRequest, userInfo?: { userId: string; epId: string; emailAddress: string } ): Promise { try { const config = await getKnoxConfig(); const formData = new FormData(); // JSON 데이터 생성 const approvalData = { contents: request.contents, contentsType: request.contentsType, docSecuType: request.docSecuType, // CONFIDENTIAL 또는 CONFIDENTIAL_STRICT notifyOption: request.notifyOption, urgYn: request.urgYn, sbmDt: request.sbmDt, timeZone: request.timeZone, docMngSaveCode: request.docMngSaveCode, subject: request.subject, sbmLang: request.sbmLang, apInfId: request.apInfId, importantYn: request.importantYn, aplns: request.aplns }; formData.append('approval', JSON.stringify(approvalData)); // 첨부파일 처리 if (request.attachments) { request.attachments.forEach((file) => { formData.append('attachments', file); }); } const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/secu-submit`, { method: 'POST', headers: await createFormHeaders(), body: formData, }); if (!response.ok) { const errorText = await response.text(); debugError('Security API Error Response', errorText); throw new Error(`보안 결재 상신 실패: ${response.status} - ${errorText}`); } // 응답의 Content-Type 확인 및 charset 처리 const contentType = response.headers.get('content-type'); let result; if (contentType && contentType.includes('application/json')) { result = await response.json(); } else { // JSON이 아닌 경우 텍스트로 읽고 JSON 파싱 시도 const text = await response.text(); debugLog('Raw security response text', text); try { result = JSON.parse(text); } catch (parseError) { console.error('Security JSON parse error:', parseError); throw new Error(`보안 응답 파싱 실패: ${text}`); } } // 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); throw error; } } /** * 결재 상세 상황 조회 * GET /approval/api/v2.0/approvals/{apInfId}/detail */ export async function getApprovalDetail( apInfId: string ): Promise { try { const config = await getKnoxConfig(); const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/detail`, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`결재 상세 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('결재 상세 조회 오류:', error); throw error; } } /** * 결재 본문 조회 * GET /approval/api/v2.0/approvals/{apInfId}/content */ export async function getApprovalContent( apInfId: string ): Promise { try { const config = await getKnoxConfig(); const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/content`, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`결재 본문 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('결재 본문 조회 오류:', error); throw error; } } /** * 결재 상황 조회 * POST /approval/api/v2.0/approvals/status */ export async function getApprovalStatus( request: ApprovalStatusRequest ): Promise { try { const config = await getKnoxConfig(); const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/status`, { method: 'POST', headers: await createJsonHeaders(), body: JSON.stringify(request.apinfids), }); if (!response.ok) { throw new Error(`결재 상황 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('결재 상황 조회 오류:', error); throw error; } } /** * 결재 연계 ID 조회 * GET /approval/api/v2.0/approvals/apinfids */ export async function getApprovalIds( apIds?: string[] ): Promise { try { const config = await getKnoxConfig(); let url = `${config.baseUrl}/approval/api/v2.0/approvals/apinfids`; if (apIds && apIds.length > 0) { const params = new URLSearchParams(); apIds.forEach(id => params.append('apId', id)); url += `?${params.toString()}`; } const response = await fetch(url, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`결재 연계 ID 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('결재 연계 ID 조회 오류:', error); throw error; } } // 상신함 리스트 조회 요청 타입 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( userParams: SubmissionListRequest, additionalParams?: Record ): Promise { try { // 사용자 식별 파라미터 중 하나는 필수 if (!userParams.epId && !userParams.userId && !userParams.emailAddress) { throw new Error('epId, userId, emailAddress 중 최소 하나는 필요합니다.'); } const config = await getKnoxConfig(); const url = new URL(`${config.baseUrl}/approval/api/v2.0/approvals/submission`); // 사용자 식별 파라미터 추가 (우선순위에 따라) 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.toString(), { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`상신함 리스트 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('상신함 리스트 조회 오류:', error); throw error; } } /** * 연계 이력 조회 * GET /approval/api/v2.0/approvals/apinfidinfos */ export async function getApprovalHistory( params?: Record ): Promise { try { const config = await getKnoxConfig(); let url = `${config.baseUrl}/approval/api/v2.0/approvals/apinfidinfos`; if (params) { const searchParams = new URLSearchParams(params); url += `?${searchParams.toString()}`; } const response = await fetch(url, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`연계 이력 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('연계 이력 조회 오류:', error); throw error; } } /** * 상신 취소 * POST /approval/api/v2.0/approvals/{apInfId}/cancel?opinion={opinion} */ export async function cancelApproval( apInfId: string, opinion: string ): Promise { try { const config = await getKnoxConfig(); const encodedOpinion = encodeURIComponent(opinion); const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/cancel?opinion=${encodedOpinion}`, { method: 'POST', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`상신 취소 실패: ${response.status}`); } 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; } } /** * 저장된 결재경로 목록 조회 * GET /approval/api/v2.0/approvals/ownaplnlist */ export async function getOwnApprovalLineList( params?: Record ): Promise { try { const config = await getKnoxConfig(); let url = `${config.baseUrl}/approval/api/v2.0/approvals/ownaplnlist`; if (params) { const searchParams = new URLSearchParams(params); url += `?${searchParams.toString()}`; } const response = await fetch(url, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`저장된 결재경로 목록 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('저장된 결재경로 목록 조회 오류:', error); throw error; } } /** * 저장된 결재경로 상세 조회 * GET /approval/api/v2.0/approvals/{pslAplnId}/ownaplndetail */ export async function getOwnApprovalLineDetail( pslAplnId: string ): Promise { try { const config = await getKnoxConfig(); const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${pslAplnId}/ownaplndetail`, { method: 'GET', headers: await createJsonHeaders(), }); if (!response.ok) { throw new Error(`저장된 결재경로 상세 조회 실패: ${response.status}`); } return await response.json(); } catch (error) { console.error('저장된 결재경로 상세 조회 오류:', error); throw error; } } // ========== 유틸리티 함수들 ========== /** * 결재 상신 요청 데이터 생성 도우미 */ export async function createSubmitApprovalRequest( contents: string, subject: string, approvalLines: ApprovalLine[], options: Partial = {} ): Promise { // 요구하는 날짜 형식으로 변환 (YYYYMMDDHHMMSS) - UTC 기준 (타임존 정보를 GTC+9 로 제공하고 있음) const now = new Date(); 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)}`; const result = { contents, subject, aplns: approvalLines, contentsType: options.contentsType || 'TEXT', docSecuType: options.docSecuType || 'PERSONAL', notifyOption: options.notifyOption || '0', urgYn: options.urgYn || 'N', sbmDt, timeZone: options.timeZone || 'GMT+9', docMngSaveCode: options.docMngSaveCode || '0', sbmLang: options.sbmLang || 'ko', apInfId, importantYn: options.importantYn || 'N', ...options }; debugLog('Created Submit Request', result); return result; } /** * 결재 라인 생성 도우미 */ export async function createApprovalLine( userInfo: { epId?: string; userId?: string; emailAddress?: string }, role: string, seq: string, options: Partial = {} ): Promise { return { ...userInfo, seq, role, aplnStatsCode: '0', arbPmtYn: 'Y', contentsMdfyPmtYn: 'Y', aplnMdfyPmtYn: 'Y', ...options }; } /** * 결재 상태 문자열 변환 */ export async function getApprovalStatusText(status: string): Promise { const statusMap: Record = { '-3': '암호화실패', '-2': '암호화중', '-1': '예약상신', '0': '보류', '1': '진행중', '2': '완결', '3': '반려', '4': '상신취소', '5': '전결', '6': '후완결' }; return statusMap[status] || '알 수 없음'; } /** * 결재 역할 문자열 변환 */ export async function getApprovalRoleText(role: string): Promise { const roleMap: Record = { '0': '기안', '1': '결재', '2': '합의', '3': '후결', '4': '병렬합의', '7': '병렬결재', '9': '통보' }; 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 { // 완결(2), 반려(3), 상신취소(4), 전결(5), 후완결(6) return ['2', '3', '4', '5', '6'].includes(status); } /** * 결재 상태가 성공인지 확인 */ export async function isApprovalStatusSuccess(status: string): Promise { // 완결(2), 전결(5), 후완결(6) return ['2', '5', '6'].includes(status); } /** * 결재 상태가 실패인지 확인 */ export async function isApprovalStatusFailure(status: string): Promise { // 반려(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'); // 1. 데이터베이스에서 결재 로그 조회 (삭제되지 않은 것만) 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)); // 2. 상태가 변동될 수 있는 항목들 필터링 (암호화중(-2), 예약상신(-1), 보류(0), 진행중(1)) const pendingApprovals = logs.filter(log => ['-2', '-1', '0', '1'].includes(log.status) ); // 3. 상태 동기화가 필요한 경우 Knox API 호출 (1000건씩 배치 처리) let statusSyncMessage = ''; if (pendingApprovals.length > 0) { try { const apinfids = pendingApprovals.map(approval => ({ apinfid: approval.apInfId })); let updatedCount = 0; const batchSize = 1000; // 1000건씩 배치 처리 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) { const currentLog = logs.find(log => log.apInfId === statusData.apInfId); if (currentLog && currentLog.status !== statusData.status) { try { await upsertApprovalStatus(statusData.apInfId, statusData.status); // 메모리상의 데이터도 업데이트 currentLog.status = statusData.status; updatedCount++; } catch (updateError) { console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); } } } } } catch (batchError) { console.error(`배치 처리 실패 (${i}-${Math.min(i + batchSize, apinfids.length)}):`, batchError); // 배치 실패는 전체 동기화를 중단하지 않고 다음 배치 계속 처리 } } statusSyncMessage = updatedCount > 0 ? ` (${updatedCount}건 상태 동기화)` : ''; } catch (syncError) { console.error('결재 상태 동기화 중 오류:', syncError); // 상태 동기화 실패는 전체 조회 실패로 처리하지 않음 } } // 5. 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}건의 결재 로그를 조회했습니다.${statusSyncMessage}`, data: formattedData, }; } catch (error) { console.error('결재 로그 조회 중 오류:', error); return { success: false, message: `결재 로그 조회 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, data: [], }; } }