1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
/** 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',
'/spreadTest'
];
// 경로가 공개 경로인지 확인하는 함수
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 경로 전체 제외
],
};
|