diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-05 19:28:49 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-05 19:28:49 +0900 |
| commit | e890fbae0c9c273b825ac808aa516de1f87fb218 (patch) | |
| tree | c2b7bdacd22043a8b4781e9b4c6ea682468abd0f | |
| parent | 35e373fe29a4145d0692ee35ff9e6b0c887df0eb (diff) | |
(김준회) 실사의뢰 결재 오류 수정, 결재 캐시 백그라운드 컨텍스트에서 패스로 무효화 처리, pm2 ecosystem 설정 변경 (npm 레이어 로그 문제), git 줄바꿈 문제 2건 커밋으로 처리
| -rw-r--r-- | app/api/pos/download-on-demand-partners/route.ts | 1 | ||||
| -rw-r--r-- | app/api/revalidate/approval/route.ts | 101 | ||||
| -rw-r--r-- | ecosystem.config.js | 4 | ||||
| -rw-r--r-- | lib/approval-log/service.ts | 160 | ||||
| -rw-r--r-- | lib/approval/approval-workflow.ts | 92 | ||||
| -rw-r--r-- | lib/approval/cache-utils.ts | 82 | ||||
| -rw-r--r-- | lib/approval/index.ts | 7 | ||||
| -rw-r--r-- | lib/pq/service.ts | 28 | ||||
| -rw-r--r-- | lib/rfq-last/shared/rfq-items-dialog.tsx | 1 | ||||
| -rw-r--r-- | lib/vendor-investigation/handlers.ts | 24 |
10 files changed, 411 insertions, 89 deletions
diff --git a/app/api/pos/download-on-demand-partners/route.ts b/app/api/pos/download-on-demand-partners/route.ts index d2941537..0e146c80 100644 --- a/app/api/pos/download-on-demand-partners/route.ts +++ b/app/api/pos/download-on-demand-partners/route.ts @@ -241,3 +241,4 @@ export async function GET(request: NextRequest) { } } + diff --git a/app/api/revalidate/approval/route.ts b/app/api/revalidate/approval/route.ts new file mode 100644 index 00000000..ed4da139 --- /dev/null +++ b/app/api/revalidate/approval/route.ts @@ -0,0 +1,101 @@ +import { revalidateTag } from 'next/cache'; +import { NextRequest, NextResponse } from 'next/server'; + +/** + * 결재 시스템 캐시 무효화 API + * + * 백그라운드 프로세스(폴링 서비스)에서 request 컨텍스트 없이 + * 캐시를 무효화하기 위한 API 라우트 + * + * 사용법: + * await fetch('/api/revalidate/approval', { + * method: 'POST', + * headers: { 'Content-Type': 'application/json' }, + * body: JSON.stringify({ tags: ['approval-logs'] }) + * }); + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { tags, secret } = body; + + // 선택적 보안: 환경 변수로 시크릿 키 검증 + // 내부 서버에서만 호출되므로 필수는 아님 + if (process.env.REVALIDATION_SECRET && secret !== process.env.REVALIDATION_SECRET) { + return NextResponse.json( + { success: false, message: 'Invalid secret' }, + { status: 401 } + ); + } + + // 캐시 태그 무효화 + if (Array.isArray(tags)) { + for (const tag of tags) { + revalidateTag(tag); + console.log(`[Cache Revalidation] Tag revalidated: ${tag}`); + } + } else if (typeof tags === 'string') { + revalidateTag(tags); + console.log(`[Cache Revalidation] Tag revalidated: ${tags}`); + } + + return NextResponse.json({ + success: true, + revalidated: true, + tags: Array.isArray(tags) ? tags : [tags], + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('[Cache Revalidation] Error:', error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : 'Revalidation failed', + }, + { status: 500 } + ); + } +} + +// GET 요청으로도 사용 가능 (개발/테스트용) +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const tag = searchParams.get('tag'); + const secret = searchParams.get('secret'); + + if (process.env.REVALIDATION_SECRET && secret !== process.env.REVALIDATION_SECRET) { + return NextResponse.json( + { success: false, message: 'Invalid secret' }, + { status: 401 } + ); + } + + if (!tag) { + return NextResponse.json( + { success: false, message: 'Tag parameter is required' }, + { status: 400 } + ); + } + + try { + revalidateTag(tag); + console.log(`[Cache Revalidation] Tag revalidated: ${tag}`); + + return NextResponse.json({ + success: true, + revalidated: true, + tag, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('[Cache Revalidation] Error:', error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : 'Revalidation failed', + }, + { status: 500 } + ); + } +} + diff --git a/ecosystem.config.js b/ecosystem.config.js index d1059d19..60f1330a 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -2,8 +2,8 @@ module.exports = { apps: [ { name: 'evcp', - script: 'npm', - args: 'run start', + script: './node_modules/next/dist/bin/next', + args: 'start', cwd: './', instances: 1, autorestart: true, diff --git a/lib/approval-log/service.ts b/lib/approval-log/service.ts index 5690e0f9..64ae40a2 100644 --- a/lib/approval-log/service.ts +++ b/lib/approval-log/service.ts @@ -10,6 +10,7 @@ import { or, sql, } from 'drizzle-orm'; +import { unstable_cache } from 'next/cache'; import db from '@/db/db'; import { approvalLogs } from '@/db/schema/knox/approvals'; @@ -39,85 +40,106 @@ interface ListInput { sort?: Array<{ id: string; desc: boolean }>; } +/** + * 결재 로그 목록 조회 (캐시 적용) + * + * 캐시 태그: 'approval-logs' + * 캐시 무효화: /api/revalidate/approval 호출 시 + */ export async function getApprovalLogList(input: ListInput) { - const offset = (input.page - 1) * input.perPage; + // 캐시 키 생성 (검색/필터/정렬 조건 포함) + const cacheKey = `approval-logs-${JSON.stringify(input)}`; + + return unstable_cache( + async () => { + const offset = (input.page - 1) * input.perPage; - /* ------------------------------------------------------------------ - * WHERE 절 구성 - * ----------------------------------------------------------------*/ - const advancedWhere = filterColumns({ - table: approvalLogs, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - filters: (input.filters ?? []) as any, - joinOperator: (input.joinOperator ?? 'and') as 'and' | 'or', - }); + /* ------------------------------------------------------------------ + * WHERE 절 구성 + * ----------------------------------------------------------------*/ + const advancedWhere = filterColumns({ + table: approvalLogs, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + filters: (input.filters ?? []) as any, + joinOperator: (input.joinOperator ?? 'and') as 'and' | 'or', + }); - // 전역 검색 (subject, content, emailAddress, userId) - let globalWhere; - if (input.search) { - const s = `%${input.search}%`; - globalWhere = or( - ilike(approvalLogs.subject, s), - ilike(approvalLogs.content, s), - ilike(approvalLogs.emailAddress, s), - ilike(approvalLogs.userId, s), - ); - } + // 전역 검색 (subject, content, emailAddress, userId) + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(approvalLogs.subject, s), + ilike(approvalLogs.content, s), + ilike(approvalLogs.emailAddress, s), + ilike(approvalLogs.userId, s), + ); + } - let where = eq(approvalLogs.isDeleted, false); // 기본적으로 삭제되지 않은 것만 조회 + let where = eq(approvalLogs.isDeleted, false); // 기본적으로 삭제되지 않은 것만 조회 - if (advancedWhere && globalWhere) { - where = and(where, advancedWhere, globalWhere); - } else if (advancedWhere) { - where = and(where, advancedWhere); - } else if (globalWhere) { - where = and(where, globalWhere); - } + if (advancedWhere && globalWhere) { + const combined = and(where, advancedWhere, globalWhere); + if (combined) where = combined; + } else if (advancedWhere) { + const combined = and(where, advancedWhere); + if (combined) where = combined; + } else if (globalWhere) { + const combined = and(where, globalWhere); + if (combined) where = combined; + } - /* ------------------------------------------------------------------ - * ORDER BY 절 구성 - * ----------------------------------------------------------------*/ - let orderBy; - try { - orderBy = input.sort && input.sort.length > 0 - ? input.sort - .map((item) => { - if (!item || !item.id || typeof item.id !== 'string') return null; - if (!(item.id in approvalLogs)) return null; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const col = approvalLogs[item.id]; - return item.desc ? desc(col) : asc(col); - }) - .filter((v): v is Exclude<typeof v, null> => v !== null) - : [desc(approvalLogs.createdAt)]; - } catch { - orderBy = [desc(approvalLogs.createdAt)]; - } + /* ------------------------------------------------------------------ + * ORDER BY 절 구성 + * ----------------------------------------------------------------*/ + let orderBy; + try { + orderBy = input.sort && input.sort.length > 0 + ? input.sort + .map((item) => { + if (!item || !item.id || typeof item.id !== 'string') return null; + if (!(item.id in approvalLogs)) return null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const col = approvalLogs[item.id]; + return item.desc ? desc(col) : asc(col); + }) + .filter((v): v is Exclude<typeof v, null> => v !== null) + : [desc(approvalLogs.createdAt)]; + } catch { + orderBy = [desc(approvalLogs.createdAt)]; + } - /* ------------------------------------------------------------------ - * 데이터 조회 - * ----------------------------------------------------------------*/ - const data = await db - .select() - .from(approvalLogs) - .where(where) - .orderBy(...orderBy) - .limit(input.perPage) - .offset(offset); + /* ------------------------------------------------------------------ + * 데이터 조회 + * ----------------------------------------------------------------*/ + const data = await db + .select() + .from(approvalLogs) + .where(where) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset); - const totalResult = await db - .select({ count: count() }) - .from(approvalLogs) - .where(where); + const totalResult = await db + .select({ count: count() }) + .from(approvalLogs) + .where(where); - const total = totalResult[0]?.count ?? 0; - const pageCount = Math.ceil(total / input.perPage); + const total = totalResult[0]?.count ?? 0; + const pageCount = Math.ceil(total / input.perPage); - return { - data, - pageCount, - }; + return { + data, + pageCount, + }; + }, + [cacheKey], + { + tags: ['approval-logs'], // 캐시 태그 + revalidate: 60, // 60초마다 자동 재검증 (폴백) + } + )(); } // ---------------------------------------------------- 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'; diff --git a/lib/pq/service.ts b/lib/pq/service.ts index b39bf7bd..fd751b0f 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -2853,6 +2853,9 @@ export async function requestInvestigationAction( forecastedAt: Date,
investigationAddress: string,
investigationNotes?: string
+ },
+ options?: {
+ skipRevalidation?: boolean; // ✅ 핸들러에서 호출 시 revalidation 건너뛰기
}
) {
try {
@@ -2917,11 +2920,13 @@ export async function requestInvestigationAction( return created;
});
- // 캐시 무효화
- revalidateTag("vendor-investigations");
- revalidateTag("pq-submissions");
- revalidateTag("vendor-pq-submissions");
- revalidatePath("/evcp/pq_new");
+ // 캐시 무효화 (핸들러에서 호출 시에는 건너뛰기)
+ if (!options?.skipRevalidation) {
+ revalidateTag("vendor-investigations");
+ revalidateTag("pq-submissions");
+ revalidateTag("vendor-pq-submissions");
+ revalidatePath("/evcp/pq_new");
+ }
return {
success: true,
@@ -2966,9 +2971,11 @@ export async function cancelInvestigationAction(investigationIds: number[]) { return updatedInvestigations
})
- // 캐시 무효화
- revalidateTag("vendor-investigations")
- revalidateTag("pq-submissions")
+ // 캐시 무효화 (핸들러에서 호출 시에는 건너뛰기)
+ if (!options?.skipRevalidation) {
+ revalidateTag("vendor-investigations")
+ revalidateTag("pq-submissions")
+ }
return {
success: true,
@@ -2987,7 +2994,10 @@ export async function cancelInvestigationAction(investigationIds: number[]) { // 실사 재의뢰 서버 액션
export async function reRequestInvestigationAction(
investigationIds: number[],
- currentUser?: { id: number } // ✅ 핸들러에서 호출 시 사용자 정보 전달
+ currentUser?: { id: number }, // ✅ 핸들러에서 호출 시 사용자 정보 전달
+ options?: {
+ skipRevalidation?: boolean; // ✅ 핸들러에서 호출 시 revalidation 건너뛰기
+ }
) {
try {
let userId: number | null = null;
diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index c25670fc..4b41897b 100644 --- a/lib/rfq-last/shared/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -487,3 +487,4 @@ export function RfqItemsDialog({ ) } + diff --git a/lib/vendor-investigation/handlers.ts b/lib/vendor-investigation/handlers.ts index 3165df06..b7da952a 100644 --- a/lib/vendor-investigation/handlers.ts +++ b/lib/vendor-investigation/handlers.ts @@ -37,18 +37,34 @@ export async function requestPQInvestigationInternal(payload: { throw new Error(errorMessage); } + // ✅ Date 문자열을 Date 객체로 변환 + // DB의 jsonb에서 읽을 때 Date 객체가 ISO 문자열로 변환되어 있음 + const forecastedAt = typeof payload.forecastedAt === 'string' + ? new Date(payload.forecastedAt) + : payload.forecastedAt; + + debugLog('[PQInvestigationHandler] Date 변환 완료', { + originalType: typeof payload.forecastedAt, + convertedType: typeof forecastedAt, + forecastedAt: forecastedAt.toISOString(), + }); + try { // 기존 PQ 서비스 함수 사용 (DB 트랜잭션 포함) const { requestInvestigationAction } = await import('@/lib/pq/service'); + debugLog('[PQInvestigationHandler] requestInvestigationAction 호출'); const result = await requestInvestigationAction( payload.pqSubmissionIds, { id: payload.currentUserId, epId: null, email: undefined }, // ✅ 실제 사용자 ID 전달 { qmManagerId: payload.qmManagerId, - forecastedAt: payload.forecastedAt, + forecastedAt: forecastedAt, // ✅ Date 객체 전달 investigationAddress: payload.investigationAddress, investigationNotes: payload.investigationNotes, + }, + { + skipRevalidation: true, // ✅ 폴링 컨텍스트에서는 revalidation 건너뛰기 } ); @@ -133,9 +149,13 @@ export async function reRequestPQInvestigationInternal(payload: { // 기존 PQ 서비스 함수 사용 const { reRequestInvestigationAction } = await import('@/lib/pq/service'); + debugLog('[PQReRequestHandler] reRequestInvestigationAction 호출'); const result = await reRequestInvestigationAction( payload.investigationIds, - { id: payload.currentUserId } // ✅ 실제 사용자 ID 전달 + { id: payload.currentUserId }, // ✅ 실제 사용자 ID 전달 + { + skipRevalidation: true, // ✅ 폴링 컨텍스트에서는 revalidation 건너뛰기 + } ); if (!result.success) { |
