-
-
+ <>
+ {/* 모바일 스켈레톤 (md 미만) */}
+
+ {/* 모바일 헤더 스켈레톤 */}
+
+
+ {/* 왼쪽: 햄버거 + 로고 */}
+
+ {/* 오른쪽: 버튼들 */}
+
- {/* 콘텐츠 카드 */}
-
-
- {[1, 2, 3, 4, 5, 6].map((i) => (
-
-
-
+
+ {/* 모바일 콘텐츠 스켈레톤 */}
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+
+ {/* 데스크톱 스켈레톤 (md 이상) */}
+
+ {/* 헤더 스켈레톤 - 상세 버전 */}
+
+
+ {/* 사이드바 + 콘텐츠 스켈레톤 */}
+
+ {/* 사이드바 스켈레톤 - 상세 버전 */}
+
+
+
+ {/* 메뉴 아이콘 스켈레톤 (13개) */}
+ {[...Array(13)].map((_, i) => (
+
+ ))}
+
+
+
+
+ {/* 콘텐츠 스켈레톤 */}
+
+ {/* 페이지 헤더 */}
+
+ {/* 콘텐츠 카드 */}
+
+
+ {[1, 2, 3, 4, 5, 6].map((i) => (
+
+ ))}
+
-
+ >
);
}
@@ -814,6 +892,9 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{children}
+
+ {/* 메뉴 검색 Command Palette (Ctrl+K / Cmd+K) */}
+
);
}
@@ -850,13 +931,18 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
- {/* 검색바 */}
-
+ {/* 검색바 - 클릭 시 Command Palette 열기 */}
+
commandMenuRef.current?.open()}
+ >
-
+
+ 메뉴 검색...
+
+ ⌘K
+
+
@@ -1060,7 +1146,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 데스크톱 사이드바 */}
+
+ {/* 메뉴 검색 Command Palette (Ctrl+K / Cmd+K) */}
+
);
}
\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
index 24926a8c..3f4fef80 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -482,8 +482,41 @@ export async function middleware(request: NextRequest) {
// 7️⃣.5️⃣ 🔄 토큰 사전 갱신 (Race Condition 방지)
// access_token이 없고 refresh_token만 있는 경우, 페이지 렌더링 전에 미리 갱신
- // 이렇게 하면 auth/check와 serverFetch가 동시에 refresh_token을 사용하는 문제 방지
+ //
+ // 🔴 중요: refresh 성공 후 같은 페이지로 리다이렉트해야 함!
+ // - 미들웨어에서 Set-Cookie를 설정해도 동시에 발생하는 API 요청은 이전 쿠키 사용
+ // - 리다이렉트하면 브라우저가 새 쿠키를 적용한 후 다시 요청
+ // - 이렇게 해야 클라이언트의 API 호출이 새 토큰을 사용
if (needsRefresh && refreshToken) {
+ // 🔄 무한 리다이렉트 방지: 이미 refresh 시도 후 돌아온 요청인지 확인
+ const url = new URL(request.url);
+ if (url.searchParams.has('_refreshed')) {
+ // 이미 리프레시 시도 후 돌아왔는데도 needsRefresh=true면 쿠키 저장 실패
+ // 무한 루프 방지를 위해 로그인 페이지로 리다이렉트
+ console.warn(`🔴 [Middleware] Cookie not saved after refresh, redirecting to login`);
+
+ const isProduction = process.env.NODE_ENV === 'production';
+ const loginUrl = new URL('/login', request.url);
+
+ const response = NextResponse.redirect(loginUrl);
+
+ // 쿠키 삭제
+ response.headers.append('Set-Cookie', [
+ 'access_token=', 'HttpOnly', ...(isProduction ? ['Secure'] : []),
+ 'SameSite=Lax', 'Path=/', 'Max-Age=0',
+ ].join('; '));
+ response.headers.append('Set-Cookie', [
+ 'refresh_token=', 'HttpOnly', ...(isProduction ? ['Secure'] : []),
+ 'SameSite=Lax', 'Path=/', 'Max-Age=0',
+ ].join('; '));
+ response.headers.append('Set-Cookie', [
+ 'is_authenticated=', ...(isProduction ? ['Secure'] : []),
+ 'SameSite=Lax', 'Path=/', 'Max-Age=0',
+ ].join('; '));
+
+ return response;
+ }
+
console.log(`🔄 [Middleware] Pre-refreshing token before page render: ${pathname}`);
const refreshResult = await refreshTokenInMiddleware(refreshToken);
@@ -491,31 +524,12 @@ export async function middleware(request: NextRequest) {
if (refreshResult.success && refreshResult.accessToken) {
const isProduction = process.env.NODE_ENV === 'production';
- // 🆕 request headers에 새 토큰 설정 (같은 요청 내 서버 컴포넌트가 읽을 수 있도록)
- // Set-Cookie는 응답 헤더에만 설정되어 같은 요청 내 cookies()로 읽을 수 없음
- // 따라서 request headers로 새 토큰을 전달하여 serverFetch에서 사용하도록 함
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-refreshed-access-token', refreshResult.accessToken);
- requestHeaders.set('x-refreshed-refresh-token', refreshResult.refreshToken || '');
+ // 🆕 리다이렉트로 새 쿠키 적용 후 다시 로드
+ // 이렇게 해야 클라이언트의 useEffect에서 호출하는 API들이 새 토큰을 사용
+ url.searchParams.set('_refreshed', '1');
+ const response = NextResponse.redirect(url);
- // intlMiddleware 효과를 먼저 가져옴
- const intlResponse = intlMiddleware(request);
-
- // 새 response 생성: request headers 전달 + intlResponse 헤더 복사
- const response = NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- });
-
- // intlResponse의 헤더를 복사 (locale 관련 헤더 등)
- intlResponse.headers.forEach((value, key) => {
- if (key.toLowerCase() !== 'set-cookie') {
- response.headers.set(key, value);
- }
- });
-
- // 새 access_token 쿠키 설정 (클라이언트의 다음 요청을 위해)
+ // 새 access_token 쿠키 설정
const accessTokenCookie = [
`access_token=${refreshResult.accessToken}`,
'HttpOnly',
@@ -532,16 +546,7 @@ export async function middleware(request: NextRequest) {
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
- 'Max-Age=604800', // 7 days (하드코딩 유지)
- ].join('; ');
-
- // 토큰 갱신 신호 쿠키 (클라이언트에서 감지용)
- const tokenRefreshedCookie = [
- `token_refreshed_at=${Date.now()}`,
- ...(isProduction ? ['Secure'] : []),
- 'SameSite=Lax',
- 'Path=/',
- 'Max-Age=60',
+ 'Max-Age=604800', // 7 days
].join('; ');
// 인증 상태 쿠키
@@ -555,16 +560,9 @@ export async function middleware(request: NextRequest) {
response.headers.append('Set-Cookie', accessTokenCookie);
response.headers.append('Set-Cookie', refreshTokenCookie);
- response.headers.append('Set-Cookie', tokenRefreshedCookie);
response.headers.append('Set-Cookie', isAuthenticatedCookie);
- // 보안 헤더 추가
- response.headers.set('X-Robots-Tag', 'noindex, nofollow, noarchive, nosnippet');
- response.headers.set('X-Content-Type-Options', 'nosniff');
- response.headers.set('X-Frame-Options', 'DENY');
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
-
- console.log(`✅ [Middleware] Pre-refresh complete, new tokens set in cookies and request headers`);
+ console.log(`✅ [Middleware] Pre-refresh complete, redirecting to apply new cookies`);
return response;
} else {
// 갱신 실패 시 쿠키 삭제 후 로그인 페이지로