summaryrefslogtreecommitdiff
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
parent35e373fe29a4145d0692ee35ff9e6b0c887df0eb (diff)
(김준회) 실사의뢰 결재 오류 수정, 결재 캐시 백그라운드 컨텍스트에서 패스로 무효화 처리, pm2 ecosystem 설정 변경 (npm 레이어 로그 문제), git 줄바꿈 문제 2건 커밋으로 처리
-rw-r--r--app/api/pos/download-on-demand-partners/route.ts1
-rw-r--r--app/api/revalidate/approval/route.ts101
-rw-r--r--ecosystem.config.js4
-rw-r--r--lib/approval-log/service.ts160
-rw-r--r--lib/approval/approval-workflow.ts92
-rw-r--r--lib/approval/cache-utils.ts82
-rw-r--r--lib/approval/index.ts7
-rw-r--r--lib/pq/service.ts28
-rw-r--r--lib/rfq-last/shared/rfq-items-dialog.tsx1
-rw-r--r--lib/vendor-investigation/handlers.ts24
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) {