diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
| commit | 78c471eec35182959e0029ded18f144974ccaca2 (patch) | |
| tree | 914cdf1c8f406ca3e2aa639b8bb774f7f4e87023 /lib/approval/approval-workflow.ts | |
| parent | 0be8940580c4a4a4e098b649d198160f9b60420c (diff) | |
(김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰 결재 연결 예시 작성
Diffstat (limited to 'lib/approval/approval-workflow.ts')
| -rw-r--r-- | lib/approval/approval-workflow.ts | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/lib/approval/approval-workflow.ts b/lib/approval/approval-workflow.ts new file mode 100644 index 00000000..cc8914f9 --- /dev/null +++ b/lib/approval/approval-workflow.ts @@ -0,0 +1,272 @@ +/** + * 결재 워크플로우 관리 모듈 + * + * 주요 기능: + * 1. 결재가 필요한 액션을 pending 상태로 저장 + * 2. Knox 결재 시스템에 상신 + * 3. 결재 완료 시 저장된 액션 실행 + * + * 흐름: + * withApproval() → Knox 상신 → [폴링으로 상태 감지] → executeApprovedAction() + */ + +import db from '@/db/db'; +import { eq } from 'drizzle-orm'; +import { pendingActions } from '@/db/schema/knox/pending-actions'; + +import type { ApprovalConfig } from './types'; +import type { ApprovalLine } from '@/lib/knox-api/approval/approval'; + +/** + * 액션 핸들러 타입 정의 + * payload를 받아서 실제 비즈니스 로직을 수행하는 함수 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ActionHandler = (payload: any) => Promise<any>; + +/** + * 액션 타입별 핸들러 저장소 + * registerActionHandler()로 등록된 핸들러들이 여기 저장됨 + */ +const actionHandlers = new Map<string, ActionHandler>(); + +/** + * 특정 액션 타입에 대한 핸들러 등록 + * + * @example + * registerActionHandler('vendor_investigation_request', async (payload) => { + * return await createInvestigation(payload); + * }); + */ +export function registerActionHandler(actionType: string, handler: ActionHandler) { + actionHandlers.set(actionType, handler); +} + +/** + * 등록된 핸들러 조회 (디버깅/테스트용) + */ +export function getRegisteredHandlers() { + return Array.from(actionHandlers.keys()); +} + +/** + * 결재가 필요한 액션을 래핑하는 공통 함수 + * + * 사용법: + * ```typescript + * const result = await withApproval( + * 'vendor_investigation_request', + * { vendorId: 123, reason: '실사 필요' }, + * { + * title: '실사 요청 결재', + * description: 'ABC 협력업체 실사 요청', + * templateName: '협력업체 실사 요청', + * variables: { '수신자이름': '결재자', ... }, + * currentUser: { id: 1, epId: 'EP001' } + * } + * ); + * ``` + * + * @param actionType - 액션 타입 (핸들러 등록 시 사용한 키) + * @param actionPayload - 액션 실행에 필요한 데이터 + * @param approvalConfig - 결재 상신 설정 (템플릿명, 변수 포함) + * @returns pendingActionId, approvalId, status + */ +export async function withApproval<T>( + actionType: string, + actionPayload: T, + approvalConfig: ApprovalConfig +) { + // 핸들러가 등록되어 있는지 확인 + if (!actionHandlers.has(actionType)) { + throw new Error(`No handler registered for action type: ${actionType}`); + } + + try { + // 1. 템플릿 조회 및 변수 치환 + const { getApprovalTemplateByName, replaceTemplateVariables } = await import('./template-utils'); + const template = await getApprovalTemplateByName(approvalConfig.templateName); + + let content: string; + if (!template) { + console.warn(`[Approval Workflow] Template not found: ${approvalConfig.templateName}`); + // 템플릿이 없으면 기본 내용 사용 + content = approvalConfig.description || '결재 요청'; + } else { + // 템플릿 변수 치환 + content = await replaceTemplateVariables(template.content, approvalConfig.variables); + } + + // 2. Knox 결재 상신 (apInfId 생성, 치환된 content 사용) + const { + submitApproval, + createSubmitApprovalRequest, + createApprovalLine + } = await import('@/lib/knox-api/approval/approval'); + + // 결재선 생성 + const aplns: ApprovalLine[] = []; + + // 기안자 (현재 사용자) + if (approvalConfig.currentUser.epId) { + const drafterLine = await createApprovalLine( + { + epId: approvalConfig.currentUser.epId, + emailAddress: approvalConfig.currentUser.email, + }, + '0', // 기안 + '0' // seq + ); + aplns.push(drafterLine); + } + + // 결재자들 + if (approvalConfig.approvers && approvalConfig.approvers.length > 0) { + for (let i = 0; i < approvalConfig.approvers.length; i++) { + const approverLine = await createApprovalLine( + { epId: approvalConfig.approvers[i] }, + '1', // 승인 + String(i + 1) + ); + aplns.push(approverLine); + } + } + + // 결재 요청 생성 + const submitRequest = await createSubmitApprovalRequest( + content, // 치환된 템플릿 content + approvalConfig.title, + aplns, + { + contentsType: 'HTML', // HTML 템플릿 사용 + } + ); + + // Knox 결재 상신 + await submitApproval( + submitRequest, + { + userId: String(approvalConfig.currentUser.id), + epId: approvalConfig.currentUser.epId || '', + emailAddress: approvalConfig.currentUser.email || '', + } + ); + + // 3. Pending Action 생성 (approvalLog의 apInfId로 연결) + // Knox에서 apInfId를 반환하지 않고, 요청 시 생성한 apInfId를 사용 + const [pendingAction] = await db.insert(pendingActions).values({ + apInfId: submitRequest.apInfId, + actionType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + actionPayload: actionPayload as any, + status: 'pending', + createdBy: approvalConfig.currentUser.id, + }).returning(); + + return { + pendingActionId: pendingAction.id, + approvalId: submitRequest.apInfId, + status: 'pending_approval', + }; + + } catch (error) { + console.error('Failed to create approval workflow:', error); + throw error; + } +} + +/** + * 결재가 승인된 액션을 실행 + * 폴링 서비스에서 호출됨 + * + * @param apInfId - Knox 결재 ID (approvalLogs의 primary key) + * @returns 액션 실행 결과 + */ +export async function executeApprovedAction(apInfId: string) { + try { + // 1. apInfId로 pendingAction 조회 + const pendingAction = await db.query.pendingActions.findFirst({ + where: eq(pendingActions.apInfId, apInfId), + }); + + if (!pendingAction) { + console.log(`[Approval Workflow] No pending action found for approval: ${apInfId}`); + return null; // 결재만 있고 실행할 액션이 없는 경우 + } + + // 이미 실행되었거나 실패한 액션은 스킵 + if (['executed', 'failed'].includes(pendingAction.status)) { + console.log(`[Approval Workflow] Pending action already processed: ${apInfId} (${pendingAction.status})`); + return null; + } + + // 2. 등록된 핸들러 조회 + const handler = actionHandlers.get(pendingAction.actionType); + if (!handler) { + throw new Error(`Handler not found for action type: ${pendingAction.actionType}`); + } + + // 3. 실제 액션 실행 + console.log(`[Approval Workflow] Executing action: ${pendingAction.actionType} (${apInfId})`); + const result = await handler(pendingAction.actionPayload); + + // 4. 실행 완료 상태 업데이트 + await db.update(pendingActions) + .set({ + status: 'executed', + executedAt: new Date(), + executionResult: result, + }) + .where(eq(pendingActions.apInfId, apInfId)); + + console.log(`[Approval Workflow] ✅ Successfully executed: ${pendingAction.actionType} (${apInfId})`); + return result; + + } catch (error) { + console.error(`[Approval Workflow] ❌ Failed to execute action for ${apInfId}:`, error); + + // 실패 상태 업데이트 + await db.update(pendingActions) + .set({ + status: 'failed', + errorMessage: error instanceof Error ? error.message : String(error), + executedAt: new Date(), + }) + .where(eq(pendingActions.apInfId, apInfId)); + + throw error; + } +} + +/** + * 결재 반려 시 처리 + * + * @param apInfId - Knox 결재 ID (approvalLogs의 primary key) + * @param reason - 반려 사유 + */ +export async function handleRejectedAction(apInfId: string, reason?: string) { + try { + const pendingAction = await db.query.pendingActions.findFirst({ + where: eq(pendingActions.apInfId, apInfId), + }); + + if (!pendingAction) { + console.log(`[Approval Workflow] No pending action found for rejected approval: ${apInfId}`); + return; // 결재만 있고 실행할 액션이 없는 경우 + } + + await db.update(pendingActions) + .set({ + status: 'rejected', + errorMessage: reason, + executedAt: new Date(), + }) + .where(eq(pendingActions.apInfId, apInfId)); + + // TODO: 요청자에게 알림 발송 등 추가 처리 + } catch (error) { + console.error(`[Approval Workflow] Failed to handle rejected action for approval ${apInfId}:`, error); + throw error; + } +} + |
