summaryrefslogtreecommitdiff
path: root/lib/knox-api/approval
diff options
context:
space:
mode:
Diffstat (limited to 'lib/knox-api/approval')
-rw-r--r--lib/knox-api/approval/approval.ts412
-rw-r--r--lib/knox-api/approval/service.ts107
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(