diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 08:24:16 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 08:24:16 +0000 |
| commit | 44bdb81a60d3a44ba7e379f3c20fe6d8fb284339 (patch) | |
| tree | b5c916a1c7ea37573f9bba7fefcef60a3b8aec20 /middleware.ts | |
| parent | 90f79a7a691943a496f67f01c1e493256070e4de (diff) | |
(대표님) 변경사항 20250707 12시 30분
Diffstat (limited to 'middleware.ts')
| -rw-r--r-- | middleware.ts | 200 |
1 files changed, 31 insertions, 169 deletions
diff --git a/middleware.ts b/middleware.ts index e32415dd..559cd038 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,15 +1,11 @@ -/** middleware.ts */ export const runtime = 'nodejs'; import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import acceptLanguage from 'accept-language'; import { getToken } from 'next-auth/jwt'; -// UAParser 임포트 수정 -import { UAParser } from 'ua-parser-js'; import { fallbackLng, languages, cookieName } from '@/i18n/settings'; -import { SessionRepository } from './lib/users/session/repository'; acceptLanguage.languages(languages); @@ -26,37 +22,38 @@ const publicPaths = [ '/api/auth', '/spreadTest', '/auth/reset-password', + ]; -// 페이지 추적에서 제외할 경로들 -const trackingExcludePaths = [ - '/api', - '/_next', - '/favicon.ico', - '/robots.txt', - '/sitemap.xml', - '.png', - '.jpg', - '.jpeg', - '.gif', - '.svg', - '.ico', - '.css', - '.js', - '.woff', - '.woff2', - '.ttf', - '.eot' +const landingPages = [ + '/', ]; // 경로가 공개 경로인지 확인하는 함수 function isPublicPath(path: string, lng: string) { - // 1. 정확한 로그인 페이지 매칭 (/ko/evcp, /en/partners 등) - if (publicPaths.some(publicPath => path === `/${lng}${publicPath}`)) { + // 1. 언어별 루트 경로 체크 (예: /ko, /en) + if (path === `/${lng}` || path === `/${lng}/`) { + return true; + } + + // 2. 랜딩 페이지들 체크 + if (landingPages.some(landingPage => { + if (landingPage === '/') { + return path === `/${lng}` || path === `/${lng}/`; + } + return path === `/${lng}${landingPage}` || path.startsWith(`/${lng}${landingPage}/`); + })) { + return true; + } + + // 3. publicPaths 배열의 경로들과 매칭 + if (publicPaths.some(publicPath => { + return path === `/${lng}${publicPath}` || path.startsWith(`/${lng}${publicPath}/`); + })) { return true; } - // 2. auth API는 별도 처리 + // 4. auth API는 별도 처리 if (path.includes('/api/auth')) { return true; } @@ -64,13 +61,6 @@ function isPublicPath(path: string, lng: string) { return false; } -// 페이지 추적 제외 경로인지 확인 -function shouldExcludeFromTracking(pathname: string): boolean { - return trackingExcludePaths.some(excludePath => - pathname.startsWith(excludePath) || pathname.includes(excludePath) - ); -} - // 도메인별 기본 대시보드 경로 정의 function getDashboardPath(domain: string, lng: string): string { switch (domain) { @@ -202,125 +192,6 @@ function createLoginUrl(pathname: string, detectedLng: string, origin: string, r return redirectUrl; } -// 클라이언트 IP 추출 함수 (수정됨) -function getClientIP(request: NextRequest): string { - const forwarded = request.headers.get('x-forwarded-for'); - const realIP = request.headers.get('x-real-ip'); - const cfConnectingIP = request.headers.get('cf-connecting-ip'); // Cloudflare - - if (cfConnectingIP) { - return cfConnectingIP; - } - - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - - if (realIP) { - return realIP; - } - - // NextRequest에는 ip 프로퍼티가 없으므로 기본값 반환 - return '127.0.0.1'; -} - -// 디바이스 타입 판단 함수 -function getDeviceType(deviceType?: string): string { - if (!deviceType) return 'desktop'; - if (deviceType === 'mobile') return 'mobile'; - if (deviceType === 'tablet') return 'tablet'; - return 'desktop'; -} - -// 페이지 제목 추출 함수 -function extractPageTitle(pathname: string, lng: string): string { - // 언어 코드 제거 - const cleanPath = pathname.replace(`/${lng}`, '') || '/'; - - // 라우트 기반 페이지 제목 매핑 - const titleMap: Record<string, string> = { - '/': 'Home', - '/evcp': 'EVCP Login', - '/evcp/report': 'EVCP Report', - '/evcp/dashboard': 'EVCP Dashboard', - '/procurement': 'Procurement Login', - '/procurement/dashboard': 'Procurement Dashboard', - '/sales': 'Sales Login', - '/sales/dashboard': 'Sales Dashboard', - '/engineering': 'Engineering Login', - '/engineering/dashboard': 'Engineering Dashboard', - '/partners': 'Partners Login', - '/partners/dashboard': 'Partners Dashboard', - '/pending': 'Pending', - '/profile': 'Profile', - '/settings': 'Settings', - }; - - // 정확한 매칭 우선 - if (titleMap[cleanPath]) { - return titleMap[cleanPath]; - } - - // 부분 매칭으로 fallback - for (const [route, title] of Object.entries(titleMap)) { - if (cleanPath.startsWith(route) && route !== '/') { - return title; - } - } - - return cleanPath || 'Unknown Page'; -} - -// 페이지 방문 추적 함수 (비동기, 논블로킹) - UAParser 수정됨 -async function trackPageVisit(request: NextRequest, token: any, detectedLng: string) { - // 백그라운드에서 실행하여 메인 요청을 블로킹하지 않음 - setImmediate(async () => { - try { - const { pathname, searchParams } = request.nextUrl; - - // 추적 제외 경로 체크 - if (shouldExcludeFromTracking(pathname)) { - return; - } - - const userAgent = request.headers.get('user-agent') || ''; - // UAParser 사용 방법 수정 - const parser = new UAParser(userAgent); - const result = parser.getResult(); - - // 활성 세션 조회 및 업데이트 - let sessionId = null; - if (token?.id && token?.dbSessionId) { - sessionId = token.dbSessionId; - - // 세션 활동 시간 업데이트 (await 없이 비동기 실행) - SessionRepository.updateSessionActivity(sessionId).catch(error => { - console.error('Failed to update session activity:', error); - }); - } - - // 페이지 방문 기록 - await SessionRepository.recordPageVisit({ - userId: token?.id || undefined, - sessionId, - route: pathname, - pageTitle: extractPageTitle(pathname, detectedLng), - referrer: request.headers.get('referer') || undefined, - ipAddress: getClientIP(request), - userAgent, - queryParams: searchParams.toString() || undefined, - deviceType: getDeviceType(result.device.type), - browserName: result.browser.name || undefined, - osName: result.os.name || undefined, - }); - - } catch (error) { - // 추적 실패는 로그만 남기고 메인 플로우에 영향 주지 않음 - console.error('Failed to track page visit:', error); - } - }); -} - export async function middleware(request: NextRequest) { /** * 1. 쿠키에서 언어 가져오기 @@ -370,16 +241,7 @@ export async function middleware(request: NextRequest) { const token = await getToken({ req: request }); /** - * 6. 페이지 방문 추적 (비동기, 논블로킹) - * - 리다이렉트가 발생하기 전에 실행 - * - API나 정적 파일은 제외 - */ - if (!shouldExcludeFromTracking(pathname)) { - trackPageVisit(request, token, detectedLng); // await 하지 않음 (논블로킹) - } - - /** - * 7. 세션 타임아웃 체크 (인증된 사용자에 대해서만) + * 6. 세션 타임아웃 체크 (인증된 사용자에 대해서만) */ if (token && !isPublicPath(pathname, detectedLng)) { const { isExpired, isExpiringSoon } = checkSessionTimeout(token); @@ -392,7 +254,7 @@ export async function middleware(request: NextRequest) { } /** - * 8. 인증된 사용자의 도메인-URL 일치 확인 및 리다이렉션 + * 7. 인증된 사용자의 도메인-URL 일치 확인 및 리다이렉션 */ if (token && token.domain && !isPublicPath(pathname, detectedLng)) { // 사용자의 domain과 URL 경로가 일치하는지 확인 @@ -409,7 +271,7 @@ export async function middleware(request: NextRequest) { } /** - * 9. 이미 로그인한 사용자가 로그인 페이지에 접근할 경우 대시보드로 리다이렉트 + * 8. 이미 로그인한 사용자가 로그인 페이지에 접근할 경우 대시보드로 리다이렉트 */ if (token) { // 세션이 만료되지 않은 경우에만 대시보드로 리다이렉트 @@ -437,7 +299,7 @@ export async function middleware(request: NextRequest) { } /** - * 10. 인증 확인: 공개 경로가 아닌 경우 로그인 체크 및 리다이렉트 + * 9. 인증 확인: 공개 경로가 아닌 경우 로그인 체크 및 리다이렉트 */ if (!isPublicPath(pathname, detectedLng)) { if (!token) { @@ -454,12 +316,12 @@ export async function middleware(request: NextRequest) { } /** - * 11. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다. + * 10. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다. */ const response = NextResponse.next(); /** - * 12. 세션 만료 경고를 위한 헤더 추가 + * 11. 세션 만료 경고를 위한 헤더 추가 */ if (token && !isPublicPath(pathname, detectedLng)) { const { isExpiringSoon } = checkSessionTimeout(token); @@ -472,7 +334,7 @@ export async function middleware(request: NextRequest) { } /** - * 13. 쿠키에 저장된 언어와 현재 lng가 다르면 업데이트 + * 12. 쿠키에 저장된 언어와 현재 lng가 다르면 업데이트 */ const currentCookie = request.cookies.get(cookieName)?.value; if (detectedLng && detectedLng !== currentCookie) { @@ -483,7 +345,7 @@ export async function middleware(request: NextRequest) { } /** - * 14. 매칭할 경로 설정 + * 13. 매칭할 경로 설정 */ export const config = { matcher: [ |
