summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/layout.tsx32
-rw-r--r--components/common/permission-checker.tsx36
-rw-r--r--lib/nonsap/auth-service.ts175
-rw-r--r--lib/nonsap/db.ts68
-rw-r--r--lib/nonsap/nonsap-auth-plan.md181
-rw-r--r--middleware.ts12
6 files changed, 501 insertions, 3 deletions
diff --git a/app/[lng]/evcp/(evcp)/layout.tsx b/app/[lng]/evcp/(evcp)/layout.tsx
index 82b53307..7fe7f3e7 100644
--- a/app/[lng]/evcp/(evcp)/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/layout.tsx
@@ -1,12 +1,40 @@
import { ReactNode } from 'react';
import { Header } from '@/components/layout/Header';
import { SiteFooter } from '@/components/layout/Footer';
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { verifyNonsapPermission } from "@/lib/nonsap/auth-service";
+import { PermissionChecker } from "@/components/common/permission-checker";
+
+export default async function EvcpLayout({ children }: { children: ReactNode }) {
+ const session = await getServerSession(authOptions);
+
+ let isAuthorized = true;
+ let authMessage = "";
+
+ // Only check permission if user is logged in
+ if (session?.user?.id) {
+ try {
+ const result = await verifyNonsapPermission(
+ parseInt(session.user.id),
+ ['SEARCH']
+ );
+ isAuthorized = result.authorized;
+ authMessage = result.message || "";
+ } catch (error) {
+ console.error("Permission check failed:", error);
+ // Default to true in case of error to avoid blocking access due to system error
+ // but logic could be changed to false for strict security
+ isAuthorized = true;
+ authMessage = "Permission check error";
+ }
+ }
-export default function EvcpLayout({ children }: { children: ReactNode }) {
return (
<div className="relative flex min-h-svh flex-col bg-background">
{/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */}
<Header />
+ <PermissionChecker authorized={isAuthorized} message={authMessage} />
<main className="flex flex-1 flex-col">
<div className='container-wrapper'>
{children}
@@ -15,4 +43,4 @@ export default function EvcpLayout({ children }: { children: ReactNode }) {
<SiteFooter/>
</div>
);
-} \ No newline at end of file
+}
diff --git a/components/common/permission-checker.tsx b/components/common/permission-checker.tsx
new file mode 100644
index 00000000..209e0022
--- /dev/null
+++ b/components/common/permission-checker.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect } from "react";
+import { toast } from "sonner";
+import { usePathname } from "next/navigation";
+
+interface PermissionCheckerProps {
+ authorized: boolean;
+ message?: string;
+}
+
+export function PermissionChecker({ authorized, message }: PermissionCheckerProps) {
+ const pathname = usePathname();
+
+ useEffect(() => {
+ // Only show toast if authorization failed
+ if (!authorized) {
+ toast.error("Permission Denied", {
+ description: message || "You do not have permission to view this page. (Dev Mode: Viewing anyway)",
+ duration: 5000,
+ action: {
+ label: "Close",
+ onClick: () => toast.dismiss(),
+ },
+ });
+ } else {
+ // Optional: Show success toast only if explicitly needed,
+ // but usually we don't show toast for success to avoid noise.
+ // Uncomment for debugging:
+ toast.success("Authorized", { description: "Access granted.", duration: 1000 });
+ }
+ }, [authorized, message, pathname]);
+
+ return null;
+}
+
diff --git a/lib/nonsap/auth-service.ts b/lib/nonsap/auth-service.ts
new file mode 100644
index 00000000..5a338eea
--- /dev/null
+++ b/lib/nonsap/auth-service.ts
@@ -0,0 +1,175 @@
+import { headers } from 'next/headers';
+import db from '@/db/db';
+import { users } from '@/db/schema/users';
+import { organization } from '@/db/schema/knox/organization';
+import { eq } from 'drizzle-orm';
+import {
+ getAllScreens,
+ getAuthsByScreenId,
+ getUserRoles,
+} from './db';
+
+export type AuthAction = 'SEARCH' | 'ADD' | 'DEL' | 'SAVE' | 'PRINT' | 'DOWN' | 'UP' | 'APPROVAL' | 'PREV' | 'NEXT';
+
+interface AuthCheckResult {
+ authorized: boolean;
+ message?: string;
+}
+
+async function getCurrentUrlFromHeaders(): Promise<string | null> {
+ const headerList = headers();
+ return headerList.get('x-pathname') || null;
+}
+
+/**
+ * NONSAP 권한 검증 함수
+ * @param userId 사용자 ID (Postgres users.id)
+ * @param requiredActions 필요한 권한 목록 (예: ['SEARCH'])
+ * @param url (Optional) 검증할 URL. 생략 시 현재 요청의 URL을 자동으로 감지 시도.
+ */
+export async function verifyNonsapPermission(
+ userId: number,
+ requiredActions: AuthAction[],
+ url?: string,
+ options: { logic?: 'AND' | 'OR' } = { logic: 'AND' }
+): Promise<AuthCheckResult> {
+ // 1. URL 결정
+ const targetUrl = url || await getCurrentUrlFromHeaders();
+ if (!targetUrl) {
+ return { authorized: false, message: "URL을 확인할 수 없습니다. URL 인자를 명시해주세요." };
+ }
+
+ // URL 정규화 (언어 코드 제거)
+ // 예: /en/partners/dashboard -> /partners/dashboard
+ const normalizedUrl = targetUrl.replace(/^\/[a-z]{2}(\/|$)/, '/');
+
+ // 2. 화면 식별
+ const allScreens = await getAllScreens();
+
+ // DB URL과 현재 URL 매칭 (Longest Prefix Match)
+ const screen = allScreens
+ .filter(s => {
+ const dbUrl = s.SCR_URL.startsWith('/') ? s.SCR_URL : `/${s.SCR_URL}`;
+ // 정확히 일치하거나 하위 경로인 경우
+ return normalizedUrl === dbUrl || normalizedUrl.startsWith(`${dbUrl}/`);
+ })
+ .sort((a, b) => b.SCR_URL.length - a.SCR_URL.length)[0];
+
+ if (!screen) {
+ // 관리되지 않는 페이지 -> Pass
+ return { authorized: true, message: "관리되지 않는 페이지입니다." };
+ }
+
+ if (screen.DEL_YN === 'Y') {
+ return { authorized: false, message: "삭제된 화면입니다." };
+ }
+
+ if (screen.SCRT_CHK_YN === 'N') {
+ return { authorized: true, message: "권한 체크가 필요 없는 화면입니다." };
+ }
+
+ // 3. 사용자 정보 조회 (Postgres)
+ const user = await db.query.users.findFirst({
+ where: eq(users.id, userId),
+ columns: {
+ nonsapUserId: true,
+ deptCode: true,
+ employeeNumber: true,
+ }
+ });
+
+ if (!user) {
+ return { authorized: false, message: "사용자 정보를 찾을 수 없습니다." };
+ }
+
+ // 4. 권한 정보 조회
+ const auths = await getAuthsByScreenId(screen.SCR_ID);
+
+ // 5. 권한 검증 (Merging)
+ const finalAuth = {
+ SEARCH: 'N', ADD: 'N', DEL: 'N', SAVE: 'N', PRINT: 'N', DOWN: 'N', UP: 'N', APPROVAL: 'N', PREV: 'N', NEXT: 'N'
+ };
+
+ // Role 정보 조회
+ const myRoles = await getUserRoles(user.employeeNumber || user.nonsapUserId || '');
+
+ for (const auth of auths) {
+ let isMatch = false;
+
+ if (auth.ACSR_GB_CD === 'U') {
+ // 사용자 직접 매핑
+ if (auth.ACSR_ID === user.nonsapUserId) isMatch = true;
+ } else if (auth.ACSR_GB_CD === 'R') {
+ // 역할 매핑
+ if (myRoles.some(r => r.ROLE_ID === auth.ACSR_ID)) isMatch = true;
+ } else if (auth.ACSR_GB_CD === 'D') {
+ // 부서 매핑 (정확히 일치)
+ if (auth.ACSR_ID === user.deptCode) isMatch = true;
+ } else if (auth.ACSR_GB_CD === 'E') {
+ // 부서 매핑 (하위 포함 - 상위 부서로 거슬러 올라가며 확인)
+ if (user.deptCode) {
+ let currentDept = user.deptCode;
+ let depth = 0;
+ // 최대 20단계까지만 확인 (무한루프 방지)
+ while (currentDept && depth < 20) {
+ if (currentDept === auth.ACSR_ID) {
+ isMatch = true;
+ break;
+ }
+
+ // 상위 부서 조회
+ // 성능 최적화를 위해 캐싱 고려 가능하나, 일단 직접 조회
+ const org = await db.query.organization.findFirst({
+ where: eq(organization.departmentCode, currentDept),
+ columns: { uprDepartmentCode: true }
+ });
+
+ if (!org || !org.uprDepartmentCode || org.uprDepartmentCode === 'TOP' || org.uprDepartmentCode === currentDept) {
+ break;
+ }
+ currentDept = org.uprDepartmentCode;
+ depth++;
+ }
+ }
+ }
+
+ if (isMatch) {
+ // 권한 합치기 (OR)
+ if (auth.AUTH_CD_SEARCH === 'Y') finalAuth.SEARCH = 'Y';
+ if (auth.AUTH_CD_ADD === 'Y') finalAuth.ADD = 'Y';
+ if (auth.AUTH_CD_DEL === 'Y') finalAuth.DEL = 'Y';
+ if (auth.AUTH_CD_SAVE === 'Y') finalAuth.SAVE = 'Y';
+ if (auth.AUTH_CD_PRINT === 'Y') finalAuth.PRINT = 'Y';
+ if (auth.AUTH_CD_DOWN === 'Y') finalAuth.DOWN = 'Y';
+ if (auth.AUTH_CD_UP === 'Y') finalAuth.UP = 'Y';
+ if (auth.AUTH_CD_APPROVAL === 'Y') finalAuth.APPROVAL = 'Y';
+ if (auth.AUTH_CD_PREV === 'Y') finalAuth.PREV = 'Y';
+ if (auth.AUTH_CD_NEXT === 'Y') finalAuth.NEXT = 'Y';
+ }
+ }
+
+ // 6. 요구사항 검증
+ const check = (action: AuthAction) => {
+ switch (action) {
+ case 'SEARCH': return finalAuth.SEARCH === 'Y';
+ case 'ADD': return finalAuth.ADD === 'Y';
+ case 'DEL': return finalAuth.DEL === 'Y';
+ case 'SAVE': return finalAuth.SAVE === 'Y';
+ case 'PRINT': return finalAuth.PRINT === 'Y';
+ case 'DOWN': return finalAuth.DOWN === 'Y';
+ case 'UP': return finalAuth.UP === 'Y';
+ case 'APPROVAL': return finalAuth.APPROVAL === 'Y';
+ case 'PREV': return finalAuth.PREV === 'Y';
+ case 'NEXT': return finalAuth.NEXT === 'Y';
+ default: return false;
+ }
+ };
+
+ if (options.logic === 'OR') {
+ const authorized = requiredActions.some(action => check(action));
+ return { authorized };
+ } else {
+ const authorized = requiredActions.every(action => check(action));
+ return { authorized };
+ }
+}
diff --git a/lib/nonsap/db.ts b/lib/nonsap/db.ts
new file mode 100644
index 00000000..2b3cbda3
--- /dev/null
+++ b/lib/nonsap/db.ts
@@ -0,0 +1,68 @@
+import { oracleKnex } from '@/lib/oracle-db/db';
+import { unstable_cache } from 'next/cache';
+
+// Types
+export interface ScreenEvcp {
+ SCR_ID: string;
+ SCR_URL: string;
+ SCRT_CHK_YN: string; // 'Y' | 'N'
+ DEL_YN: string; // 'Y' | 'N'
+}
+
+export interface ScreenAuthEvcp {
+ SCR_ID: string;
+ ACSR_GB_CD: string; // 'U' | 'R' | 'D' | 'E'
+ ACSR_ID: string;
+ AUTH_CD_SEARCH: string; // 'Y' | 'N'
+ AUTH_CD_ADD: string;
+ AUTH_CD_DEL: string;
+ AUTH_CD_SAVE: string;
+ AUTH_CD_PRINT: string;
+ AUTH_CD_DOWN: string;
+ AUTH_CD_UP: string;
+ AUTH_CD_APPROVAL: string;
+ AUTH_CD_PREV: string;
+ AUTH_CD_NEXT: string;
+}
+
+export interface RoleEvcp {
+ ROLE_ID: string;
+ ROLE_NM: string;
+}
+
+export interface RoleRelEvcp {
+ ROLE_ID: string;
+ EMPNO: string;
+}
+
+// Fetch functions with cache
+
+export const getAllScreens = unstable_cache(
+ async () => {
+ const result = await oracleKnex('CMCVW_SCR_EVCP')
+ .select('*');
+ return result as ScreenEvcp[];
+ },
+ ['nonsap-all-screens'],
+ { revalidate: 60 }
+);
+
+export const getAuthsByScreenId = unstable_cache(
+ async (scrId: string) => {
+ const result = await oracleKnex('CMCVW_SCR_AUTH_EVCP')
+ .where('SCR_ID', scrId);
+ return result as ScreenAuthEvcp[];
+ },
+ ['nonsap-auths-by-screen-id'],
+ { revalidate: 60 }
+);
+
+export const getUserRoles = unstable_cache(
+ async (empNo: string) => {
+ const result = await oracleKnex('CMCVW_ROLE_REL_EVCP')
+ .where('EMPNO', empNo);
+ return result as RoleRelEvcp[];
+ },
+ ['nonsap-user-roles'],
+ { revalidate: 60 }
+);
diff --git a/lib/nonsap/nonsap-auth-plan.md b/lib/nonsap/nonsap-auth-plan.md
new file mode 100644
index 00000000..fab5e7da
--- /dev/null
+++ b/lib/nonsap/nonsap-auth-plan.md
@@ -0,0 +1,181 @@
+# 외부 시스템(NONSAP) 연동 권한 관리 구현 계획 (v2)
+
+본 문서는 외부 오라클 시스템(NONSAP)의 뷰 테이블을 연동하여 프로젝트의 권한 관리를 처리하기 위한 구현 계획입니다.
+
+## 1. 개요
+
+- **목표**: 외부 시스템에서 관리되는 화면 및 권한 정보를 조회하여, 웹 애플리케이션의 페이지 접근 제어 및 기능별 권한 제어를 수행합니다.
+- **환경**: Node.js Runtime (Not Edge/FaaS).
+- **DB 연결**: `lib/oracle-db/db.ts`의 `oracleKnex`를 사용하여 오라클 DB에 접속합니다.
+
+## 2. 데이터 구조 및 관계
+
+제공된 4개의 뷰 테이블을 사용하여 권한을 판단합니다.
+
+### 2.1 테이블 정의
+
+1. **`CMCVW_SCR_EVCP` (화면 목록)**
+ * **역할**: 시스템의 모든 화면과 해당 URL을 정의합니다.
+ * **주요 컬럼**: `SCR_ID` (PK), `SCR_URL`, `SCRT_CHK_YN`, `DEL_YN`
+
+2. **`CMCVW_SCR_AUTH_EVCP` (화면 권한)**
+ * **역할**: 각 화면(`SCR_ID`)에 대해 접근 가능한 대상(`ACSR_ID`)과 권한 상세(`AUTH_CD`)를 정의합니다.
+ * **주요 컬럼**: `SCR_ID` (FK), `ACSR_GB_CD` (접근자 구분), `ACSR_ID`, `AUTH_CD_*` (권한 플래그)
+ * **ACSR_GB_CD 유형**:
+ * `'U'`: 사용자 (User)
+ * `'R'`: 역할 (Role)
+ * `'D'`: 부서 (Department - 하위 미포함)
+ * `'E'`: 부서 (Department - 하위 포함)
+
+3. **`CMCVW_ROLE_EVCP` (역할 목록)**
+ * **역할**: 시스템에 존재하는 역할(Role)을 정의합니다.
+ * **주요 컬럼**: `ROLE_ID` (PK), `ROLE_NM`
+
+4. **`CMCVW_ROLE_REL_EVCP` (역할-사용자 매핑)**
+ * **역할**: 각 역할(`ROLE_ID`)에 소속된 사용자(`EMPNO`)를 정의합니다.
+ * **주요 컬럼**: `ROLE_ID` (FK), `EMPNO`
+
+## 3. 구현 상세
+
+### 3.1 데이터 페칭 및 캐싱 전략
+
+* **DB Client**: `lib/oracle-db/db.ts`의 `oracleKnex` 사용.
+* **캐싱**: `unstable_cache` (Next.js)를 사용하여 DB 부하를 줄이고 응답 속도를 확보합니다. (TTL: 60초)
+
+### 3.2 권한 검증 함수 설계
+
+URL 인자를 선택적으로 받을 수 있도록 설계하여, 호출 편의성을 높입니다.
+
+```typescript
+// lib/nonsap/auth-service.ts
+
+export type AuthAction = 'SEARCH' | 'ADD' | 'DEL' | 'SAVE' | 'PRINT' | 'DOWN' | 'UP' | 'APPROVAL' | 'PREV' | 'NEXT';
+
+/**
+ * 위 Action의 의미는 아래와 같다.
+ * - 조회
+ * - 추가
+ * - 삭제
+ * - 저장
+ * - 출력
+ * - 파일받기
+ * - 파일올림
+ * - 결재상신
+ * - PrevPage
+ * - NextPage
+ */
+
+interface AuthCheckResult {
+ authorized: boolean;
+ message?: string;
+}
+
+/**
+ * NONSAP 권한 검증 함수
+ * @param empNo 사용자 사번
+ * @param requiredActions 필요한 권한 목록 (예: ['SEARCH'])
+ * @param url (Optional) 검증할 URL. 생략 시 현재 요청의 URL을 자동으로 감지 시도.
+ */
+export async function verifyNonsapPermission(
+ empNo: string,
+ requiredActions: AuthAction[],
+ url?: string,
+ options?: { logic?: 'AND' | 'OR' } // default: 'AND'
+): Promise<AuthCheckResult> {
+ // 1. URL 결정
+ const targetUrl = url || await getCurrentUrlFromHeaders();
+ if (!targetUrl) {
+ throw new Error("URL을 확인할 수 없습니다. URL 인자를 명시해주세요.");
+ }
+
+ // 2. 권한 검증 로직 수행 (oracleKnex 사용)
+ // ...
+}
+
+// Helper: 헤더에서 URL 추출 (Middleware 사전 작업 필요)
+import { headers } from 'next/headers';
+async function getCurrentUrlFromHeaders(): Promise<string | null> {
+ const headerList = headers();
+ // Middleware에서 'x-pathname' 헤더를 심어주어야 함
+ return headerList.get('x-pathname') || null;
+}
+```
+
+### 3.3 권한 검증 로직 (Core Logic)
+
+1. **화면 식별**:
+ * `CMCVW_SCR_EVCP`에서 `SCR_URL`이 `targetUrl`과 일치하는 행 조회 (`oracleKnex` 사용).
+ * `DEL_YN` == 'N', `SCRT_CHK_YN` == 'Y' 확인.
+ * 일치 항목 없으면: 관리되지 않는 페이지로 간주 -> **Pass**.
+
+2. **권한 확인 (ACSR_GB_CD 유형별 처리)**:
+ * `CMCVW_SCR_AUTH_EVCP` 조회 (`SCR_ID` 기준). 조회된 각 권한 레코드(`authRecord`)에 대해 다음 로직 수행:
+
+ * **Case U (User)**: `ACSR_GB_CD == 'U'`
+ * `authRecord.ACSR_ID`가 대상 사용자의 `nonsap_user_id` (Postgres `users` 테이블)와 일치하는지 확인.
+
+ * **Case R (Role)**: `ACSR_GB_CD == 'R'`
+ * `authRecord.ACSR_ID` (Role ID)가 `CMCVW_ROLE_REL_EVCP` 테이블에서 해당 사용자의 사번(`EMPNO`)과 매핑되어 있는지 확인.
+
+ * **Case D (Department - Exact)**: `ACSR_GB_CD == 'D'`
+ * `authRecord.ACSR_ID` (Dept Code)가 대상 사용자의 `deptCode` (Postgres `users` 테이블)와 정확히 일치하는지 확인.
+
+ * **Case E (Department - Recursive)**: `ACSR_GB_CD == 'E'`
+ * 사용자의 부서(`user.deptCode`)부터 시작하여 상위 부서로 거슬러 올라가며 확인.
+ * **데이터 소스**: `db/schema/knox/organization.ts`의 `organization` 테이블 (Knox 조직도).
+ * **Logic**:
+ 1. `currentDeptCode = user.deptCode`
+ 2. Loop:
+ * `currentDeptCode == authRecord.ACSR_ID` 이면 **Match (권한 있음)**.
+ * `organization` 테이블에서 `departmentCode == currentDeptCode`인 행 조회.
+ * 상위 부서 코드(`uprDepartmentCode`) 확인.
+ * **종료 조건 (권한 없음)**:
+ * `uprDepartmentCode` is NULL
+ * `uprDepartmentCode == currentDeptCode` (자기 참조)
+ * `uprDepartmentCode == 'TOP'`
+ * `currentDeptCode = uprDepartmentCode` 로 갱신하고 반복.
+
+3. **Action 체크**:
+ * **Step 3-1. 권한 통합 (Merging)**:
+ * 위 단계에서 **Match**된 모든 권한 레코드들의 `AUTH_CD_*` 컬럼 값을 **OR 연산(Union)**하여 사용자의 '최종 보유 권한'을 산출합니다.
+ * 예: Role A(Search=Y, Save=N) + Role B(Search=N, Save=Y) => 최종(Search=Y, Save=Y).
+ * **Step 3-2. 요구사항 검증 (Checking)**:
+ * `requiredActions` 목록과 '최종 보유 권한'을 비교합니다.
+ * **AND 모드 (Default)**: `requiredActions`의 **모든** 항목에 대해 권한이 있어야 통과 (`every`). (보안상 안전)
+ * **OR 모드**: `requiredActions` 중 **하나라도** 권한이 있으면 통과 (`some`). (메뉴 노출 등 UI 제어용)
+
+## 4. 단계별 실행 계획
+
+### Step 1: Middleware 설정 (URL 감지용)
+* `middleware.ts`에서 요청 URL의 pathname을 `x-pathname` 헤더에 담아 넘겨주는 로직 추가.
+* 이 헤더는 Server Component/Action에서 `headers()`를 통해 접근 가능해짐.
+
+### Step 2: 데이터 접근 레이어 구현 (`lib/nonsap/db.ts`)
+* `oracleKnex`를 사용하여 4개 테이블을 조회하는 함수 구현.
+* `unstable_cache` 적용.
+
+### Step 3: 권한 서비스 구현 (`lib/nonsap/auth-service.ts`)
+* `verifyNonsapPermission` 함수 구현.
+* URL 자동 감지 로직 포함.
+* 부서 계층 구조 조회 로직(Case E) 구현 (Knox Organization 테이블 연동).
+
+### Step 4: 적용 (Layout/Page/Action)
+* **페이지 접근 제어**: `app/[lng]/layout.tsx` 또는 각 Page 컴포넌트 상단에서 `verifyNonsapPermission(..., ['SEARCH'])` 호출.
+* **기능 제어**: Server Action 내부에서 `verifyNonsapPermission(..., ['SAVE'])` 등 호출.
+
+## 5. 이슈 및 고려사항
+
+1. **URL 매핑 정규화**:
+ * DB의 `SCR_URL`은 `/partners/dashboard` 형태이나, 실제 요청은 `/[lng]/partners/dashboard` 일 수 있음.
+ * Middleware 또는 Helper 함수에서 `lng` 파트(예: `/en`, `/ko`)를 제거하고 매칭하는 로직 필요.
+
+2. **Oracle DB 의존성**:
+ * `oracledb` 라이브러리는 Node.js 환경에서만 동작하므로, 권한 체크 로직은 반드시 **Server Component** 또는 **Server Action** 레벨에서 수행되어야 함. (Middleware에서 직접 DB 조회 불가)
+
+3. **부서 계층 정보**:
+ * Knox Organization 테이블(`db/schema/knox/organization.ts`)을 사용하여 상위 부서 정보를 조회합니다.
+
+## 구현 한 부분
+
+evcp 경로의 layout.tsx에서 SEARCH 권한 있는지 authority check 함
+없으면 toast로 없다고 경고만 해줌 (nonsap측에 모든 설정 다 한건 아닐 테니) \ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index e74cb653..6a825e6f 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -332,7 +332,17 @@ export async function middleware(request: NextRequest) {
/**
* 10. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다.
*/
- const response = NextResponse.next();
+ /**
+ * 10. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다.
+ */
+ const requestHeaders = new Headers(request.headers);
+ requestHeaders.set('x-pathname', pathname);
+
+ const response = NextResponse.next({
+ request: {
+ headers: requestHeaders,
+ },
+ });
// 만료된 세션 쿠키 정리 (공개 경로 포함)
if (token) {