# Cronjob Request Context 문제 ν•΄κ²° ## πŸ” 문제 상황 결재 μ™„λ£Œ ν›„ `pendingActions`μ—μ„œ ν•Έλ“€λŸ¬λ₯Ό ν˜ΈμΆœν•  λ•Œ, **node-cron의 cronjob이 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ―€λ‘œ Request Contextκ°€ μ—†μ–΄** λ‹€μŒ λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ``` ❌ Error: `headers` was called outside a request scope. ❌ Error: Invariant: static generation store missing in revalidateTag ``` ### λ°œμƒ μœ„μΉ˜ - `lib/vendors/service.ts` - `approveVendors()`, `rejectVendors()` - Next.js의 `headers()`, `revalidateTag()` λ“±μ˜ API μ‚¬μš© ### κ·Όλ³Έ 원인 ```typescript Cronjob (node-cron) ↓ (Request Context μ—†μŒ) ApprovalExecutionSaga ↓ Handler (예: approveVendorWithMDGInternal) ↓ approveVendors() ↓ headers() ❌ - AsyncLocalStorage에 μ ‘κ·Ό λΆˆκ°€ ↓ revalidateTag() ❌ - Static generation store μ—†μŒ ``` ## βœ… ν•΄κ²° 방법 ### 1. API 기반 Revalidation μ‚¬μš© Request Contextκ°€ μ—†λŠ” ν™˜κ²½μ—μ„œλŠ” **λ‚΄λΆ€ APIλ₯Ό 호좜**ν•˜μ—¬ μΊμ‹œλ₯Ό λ¬΄νš¨ν™”ν•©λ‹ˆλ‹€. ```typescript // ❌ Before: 직접 호좜 (Request Context ν•„μš”) revalidateTag("vendors"); revalidateTag("users"); // βœ… After: API 호좜 (Request Context λΆˆν•„μš”) const { revalidateApprovalRelatedCaches } = await import('@/lib/revalidation-utils'); await revalidateApprovalRelatedCaches(); ``` ### 2. ν™˜κ²½λ³€μˆ˜λ‘œ BaseURL ꡬ성 `headers()`λ₯Ό μ‚¬μš©ν•˜λŠ” λŒ€μ‹  ν™˜κ²½λ³€μˆ˜λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. ```typescript // ❌ Before: headers()둜 동적 ꡬ성 const headersList = await headers(); const host = headersList.get('host') || 'localhost:3000'; const protocol = headersList.get('x-forwarded-proto') || 'http'; const baseUrl = `${protocol}://${host}`; // βœ… After: ν™˜κ²½λ³€μˆ˜ μ‚¬μš© const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; ``` ## πŸ“ λ³€κ²½λœ 파일 ### 1. μƒˆλ‘œ μΆ”κ°€λœ 파일 #### `lib/revalidation-utils.ts` Request Contextκ°€ μ—†λŠ” ν™˜κ²½μ—μ„œ μΊμ‹œλ₯Ό λ¬΄νš¨ν™”ν•˜κΈ° μœ„ν•œ μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜λ“€: ```typescript // λ²”μš© revalidation await revalidateViaCronJob({ tags: ['vendors', 'users'] }); // νŠΉμ • 도메인별 revalidation await revalidateVendorCaches(); await revalidateUserCaches(); await revalidateApprovalRelatedCaches(); ``` ### 2. μˆ˜μ •λœ 파일 #### `lib/vendors/service.ts` **approveVendors() ν•¨μˆ˜:** - βœ… `headers()` 제거 β†’ `process.env.NEXT_PUBLIC_BASE_URL` μ‚¬μš© - βœ… `revalidateTag()` 제거 β†’ `revalidateApprovalRelatedCaches()` μ‚¬μš© **rejectVendors() ν•¨μˆ˜:** - βœ… `headers()` 제거 β†’ `process.env.NEXT_PUBLIC_BASE_URL` μ‚¬μš© - βœ… `revalidateTag()` 제거 β†’ `revalidateVendorCaches()`, `revalidateUserCaches()` μ‚¬μš© ## πŸ”§ μ„€μ • ν•„μš” ### ν™˜κ²½λ³€μˆ˜ μ„€μ • `.env` νŒŒμΌμ— λ‹€μŒ ν™˜κ²½λ³€μˆ˜λ₯Ό μΆ”κ°€ν•˜μ„Έμš”: ```bash # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ Base URL (이메일 링크 등에 μ‚¬μš©) NEXT_PUBLIC_BASE_URL=https://your-domain.com # Revalidation API λ³΄μ•ˆ (선택사항) REVALIDATION_SECRET=your-secret-key ``` **개발 ν™˜κ²½:** ```bash NEXT_PUBLIC_BASE_URL=http://localhost:3000 ``` **ν”„λ‘œλ•μ…˜ ν™˜κ²½:** ```bash NEXT_PUBLIC_BASE_URL=https://evcp.your-company.com ``` ## 🎯 μž‘λ™ 방식 ### 1. Cronjobμ—μ„œ ν•Έλ“€λŸ¬ μ‹€ν–‰ ```typescript // lib/approval/approval-polling-service.ts const saga = new ApprovalExecutionSaga(apInfId); await saga.execute(); // ← Request Context μ—†μŒ ``` ### 2. ν•Έλ“€λŸ¬μ—μ„œ Service ν•¨μˆ˜ 호좜 ```typescript // lib/vendors/approval-handlers.ts export async function approveVendorWithMDGInternal(payload) { // MDG 전솑... const approveResult = await approveVendors({ ids: payload.vendorIds, userId: payload.userId, }); // ← Request Context μ—†λŠ” μƒνƒœλ‘œ 호좜됨 } ``` ### 3. Serviceμ—μ„œ API 기반 Revalidation ```typescript // lib/vendors/service.ts export async function approveVendors(input) { await db.transaction(async (tx) => { // 1. 벀더 승인 처리 // 2. 이메일 λ°œμ†‘ (βœ… ν™˜κ²½λ³€μˆ˜ μ‚¬μš©) const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; const loginUrl = `${baseUrl}/${userLang}/login`; }); // 3. μΊμ‹œ λ¬΄νš¨ν™” (βœ… API 호좜 μ‚¬μš©) const { revalidateApprovalRelatedCaches } = await import('@/lib/revalidation-utils'); await revalidateApprovalRelatedCaches(); } ``` ### 4. Revalidation Utilsμ—μ„œ λ‚΄λΆ€ API 호좜 ```typescript // lib/revalidation-utils.ts export async function revalidateViaCronJob(options) { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; const response = await fetch(`${baseUrl}/api/revalidate/approval`, { method: 'POST', body: JSON.stringify({ tags: options.tags, secret: process.env.REVALIDATION_SECRET, }), }); return response.json(); } ``` ### 5. API Routeμ—μ„œ μ‹€μ œ Revalidation μ‹€ν–‰ ```typescript // app/api/revalidate/approval/route.ts export async function POST(request: NextRequest) { // βœ… μ—¬κΈ°λŠ” HTTP Request Contextκ°€ 있음! const { tags } = await request.json(); for (const tag of tags) { revalidateTag(tag); // βœ… Request Contextκ°€ μžˆμœΌλ―€λ‘œ μž‘λ™ν•¨ } return Response.json({ success: true }); } ``` ## πŸ“Š 흐름도 ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Cronjob (node-cron) - 1λΆ„λ§ˆλ‹€ β”‚ β”‚ Request Context: ❌ μ—†μŒ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ApprovalExecutionSaga β”‚ β”‚ - Knox 결재 μƒνƒœ 확인 β”‚ β”‚ - 승인된 경우 Handler μ‹€ν–‰ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ approveVendorWithMDGInternal() β”‚ β”‚ - MDG 전솑 β”‚ β”‚ - approveVendors() 호좜 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ approveVendors() - lib/vendors/service.ts β”‚ β”‚ βœ… baseUrl = process.env.NEXT_PUBLIC_BASE_URL β”‚ β”‚ βœ… await revalidateViaCronJob({ tags: [...] }) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ revalidateViaCronJob() - lib/revalidation-utils.ts β”‚ β”‚ βœ… fetch('/api/revalidate/approval', ...) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ POST /api/revalidate/approval β”‚ β”‚ Request Context: βœ… 있음! β”‚ β”‚ βœ… revalidateTag() μ‚¬μš© κ°€λŠ₯ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## 🚨 μ£Όμ˜μ‚¬ν•­ ### 1. ν™˜κ²½λ³€μˆ˜ μ„€μ • ν•„μˆ˜ `NEXT_PUBLIC_BASE_URL`이 μ„€μ •λ˜μ§€ μ•ŠμœΌλ©΄ `http://localhost:3000`이 κΈ°λ³Έκ°’μœΌλ‘œ μ‚¬μš©λ©λ‹ˆλ‹€. ν”„λ‘œλ•μ…˜μ—μ„œλŠ” λ°˜λ“œμ‹œ μ„€μ •ν•˜μ„Έμš”. ### 2. 이메일 링크 μ •ν™•μ„± 이메일에 ν¬ν•¨λ˜λŠ” 링크가 μ˜¬λ°”λ₯Έ 도메인을 κ°€λ¦¬ν‚€λŠ”μ§€ ν™•μΈν•˜μ„Έμš”: ```typescript // 둜그인 링크 μ˜ˆμ‹œ https://evcp.your-company.com/ko/login // λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • 링크 μ˜ˆμ‹œ https://evcp.your-company.com/ko/auth/reset-password?token=xxxxx ``` ### 3. Revalidation API λ³΄μ•ˆ `REVALIDATION_SECRET`을 μ„€μ •ν•˜μ—¬ μ™ΈλΆ€μ—μ„œ λ¬΄λ‹¨μœΌλ‘œ μΊμ‹œλ₯Ό λ¬΄νš¨ν™”ν•˜λŠ” 것을 λ°©μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€ (선택사항). ### 4. λ‹€λ₯Έ Service ν•¨μˆ˜μ—λ„ 적용 `lib/vendors/service.ts`의 λ‹€λ₯Έ ν•¨μˆ˜λ“€λ„ λ™μΌν•œ νŒ¨ν„΄μ„ μ‚¬μš©ν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€: - `requestPQVendors()` - line 3210-3215에 `revalidateTag()` μ‚¬μš© - 기타 `headers()` λ˜λŠ” `revalidateTag()`λ₯Ό μ‚¬μš©ν•˜λŠ” ν•¨μˆ˜λ“€ ν•„μš”μ‹œ λ™μΌν•œ νŒ¨ν„΄μœΌλ‘œ μˆ˜μ •ν•˜μ„Έμš”. ## πŸŽ‰ κ²°κ³Ό 이제 cronjobμ—μ„œ ν•Έλ“€λŸ¬κ°€ μ‹€ν–‰λ˜μ–΄λ„ λ‹€μŒκ³Ό 같이 정상 μž‘λ™ν•©λ‹ˆλ‹€: ```bash βœ… [ExecutionSaga] Step 5: Executing action βœ… [Vendor Approval Handler] MDG 전솑 성곡 βœ… [Vendor Approval Handler] 벀더 승인 μ™„λ£Œ βœ… [Revalidation] Cache invalidated: vendors, users, roles βœ… [ExecutionSaga] βœ“ Action executed ``` ## πŸ“š μ°Έκ³  자료 - [Next.js Dynamic APIs](https://nextjs.org/docs/messages/next-dynamic-api-wrong-context) - [Next.js Revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating) - [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage)