middleware.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import { type NextRequest, NextResponse } from 'next/server';
  2. import { FEATURE_CHANNEL } from '@/constants/features';
  3. function isTokenValid(token: string): boolean {
  4. try {
  5. const payload = JSON.parse(atob(token.split('.')[1]));
  6. return payload.exp * 1000 > Date.now();
  7. } catch {
  8. return false;
  9. }
  10. }
  11. export default function middleware(req: NextRequest)
  12. {
  13. // 인증 페이지는 로그인 상태에서도 접근 가능
  14. const skipRoutes = ['/approval', '/verify-email'];
  15. if (skipRoutes.some((path) => req.nextUrl.pathname.startsWith(path))) {
  16. return NextResponse.next();
  17. }
  18. const { pathname } = req.nextUrl;
  19. // 채널 기능 OFF: 채널/방송 관련 라우트는 메인으로 리다이렉트 (페이지 코드는 보존)
  20. if (!FEATURE_CHANNEL) {
  21. const channelRoutes = ['/channel', '/watch', '/live', '/creators', '/studio'];
  22. if (channelRoutes.some((route) => pathname === route || pathname.startsWith(route + '/'))) {
  23. return NextResponse.redirect(new URL('/', req.url));
  24. }
  25. }
  26. const accessToken = req.cookies.get('accessToken')?.value;
  27. const refreshToken = req.cookies.get('refreshToken')?.value;
  28. const isLoggedIn = !!accessToken && isTokenValid(accessToken);
  29. const publicRoutes = ['/', '/login', '/register', '/forgot-password', '/reset-password', '/welcome'];
  30. if (publicRoutes.includes(pathname) || pathname.startsWith('/login/google/') || pathname.startsWith('/auth/sns/')) {
  31. if (isLoggedIn && publicRoutes.filter(r => r !== '/').includes(pathname)) {
  32. return NextResponse.redirect(new URL('/', req.url)); // 로그인 상태에서 접근 불가
  33. }
  34. return NextResponse.next();
  35. }
  36. // 로그인이 필요한 경로 (startsWith로 하위 경로까지 포함)
  37. const protectedRoutes = [
  38. '/profile', // 내 정보
  39. '/change-email', // 이메일 변경
  40. '/change-name',// 별명 변경
  41. '/change-password', // 비밀번호 변경
  42. '/change-approve', // 알림 설정
  43. '/change-thumb', // 사진 변경
  44. '/change-summary', // 한마디 변경
  45. '/change-intro', // 자기소개 변경
  46. '/certificate', // 본인 인증
  47. '/my-post', // 작성 게시글
  48. '/my-comment', // 작성 댓글
  49. '/exp-log', // 경험치 내역
  50. '/login-log', // 로그인 기록
  51. '/withdraw', // 회원탈퇴
  52. '/valid-email', // 이메일 변경 인증
  53. '/sns-link', // SNS 연결 관리
  54. '/studio' // 크리에이터 스튜디오
  55. ];
  56. const isProtected = protectedRoutes.some(route => pathname === route || pathname.startsWith(route + '/'));
  57. if (!isProtected) {
  58. return NextResponse.next(); // 404
  59. }
  60. // 보호된 경로에 토큰이 없으면 로그인으로
  61. if (!accessToken || !refreshToken) {
  62. return NextResponse.redirect(new URL('/login', req.url));
  63. }
  64. return NextResponse.next();
  65. }
  66. export const config =
  67. {
  68. matcher: [
  69. '/((?!_next|api|static|public|resources|editor|favicon.ico).*)'
  70. ]
  71. }