summaryrefslogtreecommitdiff
path: root/lib/approval
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-05 19:28:49 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-05 19:28:49 +0900
commite890fbae0c9c273b825ac808aa516de1f87fb218 (patch)
treec2b7bdacd22043a8b4781e9b4c6ea682468abd0f /lib/approval
parent35e373fe29a4145d0692ee35ff9e6b0c887df0eb (diff)
(김준회) 실사의뢰 결재 오류 수정, 결재 캐시 백그라운드 컨텍스트에서 패스로 무효화 처리, pm2 ecosystem 설정 변경 (npm 레이어 로그 문제), git 줄바꿈 문제 2건 커밋으로 처리
Diffstat (limited to 'lib/approval')
-rw-r--r--lib/approval/approval-workflow.ts92
-rw-r--r--lib/approval/cache-utils.ts82
-rw-r--r--lib/approval/index.ts7
3 files changed, 174 insertions, 7 deletions
diff --git a/lib/approval/approval-workflow.ts b/lib/approval/approval-workflow.ts
index 4c7eec09..fc6a0dcf 100644
--- a/lib/approval/approval-workflow.ts
+++ b/lib/approval/approval-workflow.ts
@@ -180,6 +180,11 @@ export async function withApproval<T>(
* - Knox 상신 성공 후 DB 업데이트 실패 위험 제거
*/
+ // 캐시 무효화 (결재 상신 시)
+ console.log(`[Approval Workflow] Revalidating cache after approval submission`);
+ const { revalidateApprovalLogs } = await import('./cache-utils');
+ await revalidateApprovalLogs();
+
let pendingActionId: number | undefined;
try {
@@ -253,38 +258,81 @@ export async function withApproval<T>(
* @returns 액션 실행 결과
*/
export async function executeApprovedAction(apInfId: string) {
+ // debug-utils import
+ const { debugLog, debugError, debugSuccess } = await import('@/lib/debug-utils');
+
+ debugLog('[executeApprovedAction] 시작', { apInfId });
+
try {
// 핸들러 자동 초기화 (폴링 서비스의 격리 문제 해결)
+ debugLog('[executeApprovedAction] 핸들러 초기화 중');
await ensureHandlersInitialized();
+ debugLog('[executeApprovedAction] 핸들러 초기화 완료');
// 1. apInfId로 pendingAction 조회
+ debugLog('[executeApprovedAction] pendingAction 조회 중', { apInfId });
const pendingAction = await db.query.pendingActions.findFirst({
where: eq(pendingActions.apInfId, apInfId),
});
if (!pendingAction) {
+ debugLog('[executeApprovedAction] pendingAction 없음 (결재만 존재)', { apInfId });
console.log(`[Approval Workflow] No pending action found for approval: ${apInfId}`);
return null; // 결재만 있고 실행할 액션이 없는 경우
}
+ debugLog('[executeApprovedAction] pendingAction 조회 완료', {
+ id: pendingAction.id,
+ actionType: pendingAction.actionType,
+ status: pendingAction.status,
+ createdBy: pendingAction.createdBy,
+ });
+
// 이미 실행되었거나 실패한 액션은 스킵
if (['executed', 'failed'].includes(pendingAction.status)) {
+ debugLog('[executeApprovedAction] 이미 처리된 액션 스킵', {
+ apInfId,
+ status: pendingAction.status,
+ });
console.log(`[Approval Workflow] Pending action already processed: ${apInfId} (${pendingAction.status})`);
return null;
}
// 2. 등록된 핸들러 조회
+ debugLog('[executeApprovedAction] 핸들러 조회 중', {
+ actionType: pendingAction.actionType,
+ });
const handler = actionHandlers.get(pendingAction.actionType);
if (!handler) {
+ debugError('[executeApprovedAction] 핸들러를 찾을 수 없음', {
+ actionType: pendingAction.actionType,
+ availableHandlers: Array.from(actionHandlers.keys()),
+ });
console.error('[Approval Workflow] Available handlers:', Array.from(actionHandlers.keys()));
throw new Error(`Handler not found for action type: ${pendingAction.actionType}`);
}
+ debugLog('[executeApprovedAction] 핸들러 조회 완료', {
+ actionType: pendingAction.actionType,
+ });
// 3. 실제 액션 실행
+ debugLog('[executeApprovedAction] 핸들러 실행 시작', {
+ actionType: pendingAction.actionType,
+ apInfId,
+ payloadKeys: Object.keys(pendingAction.actionPayload || {}),
+ });
console.log(`[Approval Workflow] Executing action: ${pendingAction.actionType} (${apInfId})`);
+
const result = await handler(pendingAction.actionPayload);
+ debugSuccess('[executeApprovedAction] 핸들러 실행 완료', {
+ actionType: pendingAction.actionType,
+ apInfId,
+ resultKeys: result ? Object.keys(result) : [],
+ });
+
// 4. 실행 완료 상태 업데이트
+ debugLog('[executeApprovedAction] 상태 업데이트 중 (executed)');
await db.update(pendingActions)
.set({
status: 'executed',
@@ -292,21 +340,46 @@ export async function executeApprovedAction(apInfId: string) {
executionResult: result,
})
.where(eq(pendingActions.apInfId, apInfId));
+ debugLog('[executeApprovedAction] 상태 업데이트 완료 (executed)');
+ // 5. 캐시 무효화 (백그라운드에서도 동작)
+ debugLog('[executeApprovedAction] 캐시 무효화 중');
+ const { revalidateApprovalLogs } = await import('./cache-utils');
+ await revalidateApprovalLogs();
+ debugLog('[executeApprovedAction] 캐시 무효화 완료');
+
+ debugSuccess('[executeApprovedAction] 전체 프로세스 완료', {
+ actionType: pendingAction.actionType,
+ apInfId,
+ });
console.log(`[Approval Workflow] ✅ Successfully executed: ${pendingAction.actionType} (${apInfId})`);
return result;
} catch (error) {
+ debugError('[executeApprovedAction] 실행 중 에러 발생', {
+ apInfId,
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ });
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));
+ try {
+ debugLog('[executeApprovedAction] 상태 업데이트 중 (failed)');
+ await db.update(pendingActions)
+ .set({
+ status: 'failed',
+ errorMessage: error instanceof Error ? error.message : String(error),
+ executedAt: new Date(),
+ })
+ .where(eq(pendingActions.apInfId, apInfId));
+ debugLog('[executeApprovedAction] 상태 업데이트 완료 (failed)');
+ } catch (updateError) {
+ debugError('[executeApprovedAction] 상태 업데이트 실패', {
+ apInfId,
+ updateError: updateError instanceof Error ? updateError.message : String(updateError),
+ });
+ }
throw error;
}
@@ -337,6 +410,11 @@ export async function handleRejectedAction(apInfId: string, reason?: string) {
})
.where(eq(pendingActions.apInfId, apInfId));
+ // 캐시 무효화 (백그라운드에서도 동작)
+ console.log(`[Approval Workflow] Revalidating cache for rejected action: ${apInfId}`);
+ const { revalidateApprovalLogs } = await import('./cache-utils');
+ await revalidateApprovalLogs();
+
// TODO: 요청자에게 알림 발송 등 추가 처리
} catch (error) {
console.error(`[Approval Workflow] Failed to handle rejected action for approval ${apInfId}:`, error);
diff --git a/lib/approval/cache-utils.ts b/lib/approval/cache-utils.ts
new file mode 100644
index 00000000..f4c56c25
--- /dev/null
+++ b/lib/approval/cache-utils.ts
@@ -0,0 +1,82 @@
+/**
+ * 결재 시스템 캐시 무효화 유틸리티
+ *
+ * 백그라운드 프로세스(폴링 서비스)에서 request 컨텍스트 없이
+ * Next.js 캐시를 무효화하기 위한 헬퍼 함수들
+ */
+
+/**
+ * 결재 관련 캐시 무효화
+ *
+ * @param tags - 무효화할 캐시 태그 배열
+ * @returns 무효화 결과
+ *
+ * @example
+ * // 결재 로그 캐시 무효화
+ * await revalidateApprovalCache(['approval-logs']);
+ *
+ * // 여러 캐시 동시 무효화
+ * await revalidateApprovalCache(['approval-logs', 'pending-actions']);
+ */
+export async function revalidateApprovalCache(tags: string[]) {
+ try {
+ // 내부 API 호출로 캐시 무효화
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
+ const response = await fetch(`${baseUrl}/api/revalidate/approval`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ tags,
+ secret: process.env.REVALIDATION_SECRET, // 선택적 보안
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Cache revalidation failed: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+ console.log(`[Approval Cache] Successfully revalidated tags:`, tags);
+ return result;
+ } catch (error) {
+ // 캐시 무효화 실패는 치명적이지 않으므로 로그만 남기고 진행
+ console.error('[Approval Cache] Failed to revalidate cache:', error);
+ console.warn('[Approval Cache] Continuing despite cache revalidation failure');
+ return { success: false, error };
+ }
+}
+
+/**
+ * 결재 로그 캐시 무효화
+ */
+export async function revalidateApprovalLogs() {
+ return revalidateApprovalCache(['approval-logs']);
+}
+
+/**
+ * Pending Actions 캐시 무효화
+ */
+export async function revalidatePendingActions() {
+ return revalidateApprovalCache(['pending-actions']);
+}
+
+/**
+ * 결재 관련 모든 캐시 무효화
+ */
+export async function revalidateAllApprovalCaches() {
+ return revalidateApprovalCache([
+ 'approval-logs',
+ 'pending-actions',
+ 'approval-templates',
+ ]);
+}
+
+/**
+ * 특정 결재 ID의 상세 캐시 무효화
+ */
+export async function revalidateApprovalDetail(apInfId: string) {
+ return revalidateApprovalCache([`approval-log-${apInfId}`]);
+}
+
diff --git a/lib/approval/index.ts b/lib/approval/index.ts
index 644c5fa8..82abac9a 100644
--- a/lib/approval/index.ts
+++ b/lib/approval/index.ts
@@ -32,3 +32,10 @@ export {
export type { TemplateVariables, ApprovalConfig, ApprovalResult } from './types';
+export {
+ revalidateApprovalCache,
+ revalidateApprovalLogs,
+ revalidatePendingActions,
+ revalidateAllApprovalCaches,
+ revalidateApprovalDetail,
+} from './cache-utils';