summaryrefslogtreecommitdiff
path: root/middleware.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 08:24:16 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 08:24:16 +0000
commit44bdb81a60d3a44ba7e379f3c20fe6d8fb284339 (patch)
treeb5c916a1c7ea37573f9bba7fefcef60a3b8aec20 /middleware.ts
parent90f79a7a691943a496f67f01c1e493256070e4de (diff)
(대표님) 변경사항 20250707 12시 30분
Diffstat (limited to 'middleware.ts')
-rw-r--r--middleware.ts200
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: [