/** 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'; import { fallbackLng, languages, cookieName } from '@/i18n/settings'; acceptLanguage.languages(languages); // 로그인이 필요 없는 공개 경로 const publicPaths = [ '/evcp', '/partners', '/partners/repository', '/partners/signup', '/api/auth', ]; // 경로가 공개 경로인지 확인하는 함수 function isPublicPath(path: string, lng: string) { // 1. 정확한 로그인 페이지 매칭 (/ko/evcp, /en/partners 등) if (publicPaths.some(publicPath => path === `/${lng}${publicPath}`)) { return true; } // 2. auth API는 별도 처리 if (path.includes('/api/auth')) { return true; } return false; } // 도메인-URL 일치 여부 확인 및 올바른 리다이렉트 경로 반환 function getDomainRedirectPath(path: string, domain: string, lng: string) { // 도메인이 없는 경우 리다이렉트 없음 if (!domain) return null; // URL에 partners가 있는지 확인 const hasPartnersInPath = path.includes('/partners'); // URL에 evcp가 있는지 확인 const hasEvcpInPath = path.includes('/evcp'); // 1. 도메인이 'partners'인데 URL에 '/evcp/'가 있으면 if (domain === 'partners' && hasEvcpInPath) { // URL에서 '/evcp/'를 '/partners/'로 교체 return path.replace('/evcp/', '/partners/'); } // 2. 도메인이 'evcp'인데 URL에 '/partners/'가 있으면 if (domain === 'evcp' && hasPartnersInPath) { // URL에서 '/partners/'를 '/evcp/'로 교체 return path.replace('/partners/', '/evcp/'); } // 불일치가 없으면 null 반환 (리다이렉트 필요 없음) return null; } export async function middleware(request: NextRequest) { /** * 1. 쿠키에서 언어 가져오기 */ let lng = request.cookies.get(cookieName)?.value; /** * 2. 쿠키가 없다면 브라우저의 Accept-Language 헤더에서 언어를 추론 */ if (!lng) { const headerLang = request.headers.get('accept-language'); lng = acceptLanguage.get(headerLang) || fallbackLng; } const { pathname, searchParams, origin } = request.nextUrl; /** * 3. "/" 경로로 들어온 경우 -> "/{lng}"로 리다이렉트 */ if (pathname === '/') { const redirectUrl = new URL(`/${lng}`, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } /** * 4. 현재 pathname이 언어 경로를 포함하고 있는지 확인 */ const hasValidLngInPath = languages.some( (language) => pathname === `/${language}` || pathname.startsWith(`/${language}/`), ); /** * 5. 언어 경로가 누락된 경우 -> "/{lng}" + 기존 pathname 으로 리다이렉트 */ if (!hasValidLngInPath) { const redirectUrl = new URL(`/${lng}${pathname}`, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } // 언어 코드 추출 const pathnameParts = pathname.split('/'); const detectedLng = pathnameParts[1]; // 예: /ko/partners -> ko // 토큰 가져오기 (인증 상태 확인 및 도메인 검증에 사용) const token = await getToken({ req: request }); /** * 6. 인증된 사용자의 도메인-URL 일치 확인 및 리다이렉션 */ if (token && token.domain && !isPublicPath(pathname, detectedLng)) { // 사용자의 domain과 URL 경로가 일치하는지 확인 const redirectPath = getDomainRedirectPath(pathname, token.domain as string, detectedLng); // 도메인과 URL이 일치하지 않으면 리다이렉트 if (redirectPath) { const redirectUrl = new URL(redirectPath, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } } /** * 6.5 이미 로그인한 사용자가 로그인 페이지에 접근할 경우 대시보드로 리다이렉트 */ if (token) { // 로그인 페이지 경로 확인 (정확한 /ko/evcp 또는 /en/partners 등) const isEvcpLoginPage = pathname === `/${detectedLng}/evcp`; const isPartnersLoginPage = pathname === `/${detectedLng}/partners`; if (isEvcpLoginPage) { // EVCP 로그인 페이지에 접근한 경우 report 페이지로 리다이렉트 const redirectUrl = new URL(`/${detectedLng}/evcp/report`, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } else if (isPartnersLoginPage) { // Partners 로그인 페이지에 접근한 경우 dashboard 페이지로 리다이렉트 const redirectUrl = new URL(`/${detectedLng}/partners/dashboard`, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } } /** * 7. 인증 확인: 공개 경로가 아닌 경우 로그인 체크 및 리다이렉트 */ if (!isPublicPath(pathname, detectedLng)) { if (!token) { // 어떤 로그인 페이지로 리다이렉트할지 결정 let loginPath; // 경로에 따라 적절한 로그인 페이지 선택 if (pathname.includes('/partners') || pathname.startsWith(`/${detectedLng}/vendor`)) { loginPath = `/${detectedLng}/partners`; } else { loginPath = `/${detectedLng}/evcp`; } // 로그인 후 원래 페이지로 리다이렉트하기 위해 callbackUrl 추가 const redirectUrl = new URL(loginPath, origin); redirectUrl.searchParams.set('callbackUrl', request.nextUrl.pathname + request.nextUrl.search); return NextResponse.redirect(redirectUrl); } } /** * 8. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다. */ const response = NextResponse.next(); /** * 9. 쿠키에 저장된 언어와 현재 lng가 다르면 업데이트 */ const currentCookie = request.cookies.get(cookieName)?.value; if (detectedLng && detectedLng !== currentCookie) { response.cookies.set(cookieName, detectedLng, { path: '/' }); } return response; } /** * 10. 매칭할 경로 설정 */ export const config = { matcher: [ '/((?!_next|.*\\..*|api|viewer).*)', // API 경로 전체 제외 ], };