summaryrefslogtreecommitdiff
path: root/lib/approval/CRONJOB_CONTEXT_FIX.md
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-07 09:40:41 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-07 09:40:41 +0900
commit98e86ada15b2a867374188c79f78f5578018a911 (patch)
tree65a1004c59feb7e4497d79563f3ead095dfe9a06 /lib/approval/CRONJOB_CONTEXT_FIX.md
parentaac4e61398ed829e9dfa2c038f76405f92563d14 (diff)
(김준회) 공통 컴포넌트 이해를 위한 문서 추가
Diffstat (limited to 'lib/approval/CRONJOB_CONTEXT_FIX.md')
-rw-r--r--lib/approval/CRONJOB_CONTEXT_FIX.md278
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)
+