diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-07 09:40:41 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-07 09:40:41 +0900 |
| commit | 98e86ada15b2a867374188c79f78f5578018a911 (patch) | |
| tree | 65a1004c59feb7e4497d79563f3ead095dfe9a06 /lib/approval/CRONJOB_CONTEXT_FIX.md | |
| parent | aac4e61398ed829e9dfa2c038f76405f92563d14 (diff) | |
(김준회) 공통 컴포넌트 이해를 위한 문서 추가
Diffstat (limited to 'lib/approval/CRONJOB_CONTEXT_FIX.md')
| -rw-r--r-- | lib/approval/CRONJOB_CONTEXT_FIX.md | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/lib/approval/CRONJOB_CONTEXT_FIX.md b/lib/approval/CRONJOB_CONTEXT_FIX.md new file mode 100644 index 00000000..a8169a5e --- /dev/null +++ b/lib/approval/CRONJOB_CONTEXT_FIX.md @@ -0,0 +1,278 @@ +# 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) + |
