summaryrefslogtreecommitdiff
path: root/lib/approval/CRONJOB_CONTEXT_FIX.md
diff options
context:
space:
mode:
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)
+