feat(WEB): 폴더블 기기(Galaxy Fold) 레이아웃 대응 및 CEO 대시보드 개선

- AuthenticatedLayout: visualViewport API 추가로 폴더블 기기 화면 전환 감지
- globals.css: CSS 변수(--app-width, --app-height) 및 dvw/dvh fallback 추가
- 모바일 레이아웃: h-screen → var(--app-height)로 변경
- CEO 대시보드 및 API 클라이언트 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-09 11:00:39 +09:00
parent 0d539628f3
commit c4412295fa
9 changed files with 255 additions and 75 deletions

View File

@@ -13,6 +13,24 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { createErrorResponse, type ApiErrorResponse } from './errors';
import { refreshAccessToken } from './refresh-token';
/**
* 토큰 쿠키 삭제 (리프레시 실패 또는 인증 만료 시)
*
* ⚠️ 중요: redirect('/login') 호출 전에 반드시 실행해야 함
* 쿠키가 남아있으면 미들웨어가 "인증됨"으로 판단하여 무한 루프 발생
*/
async function clearTokenCookies() {
const cookieStore = await cookies();
// 토큰 쿠키 삭제
cookieStore.delete('access_token');
cookieStore.delete('refresh_token');
cookieStore.delete('token_refreshed_at');
cookieStore.delete('is_authenticated');
console.log('🗑️ [serverFetch] 토큰 쿠키 삭제 완료 (무한 루프 방지)');
}
/**
* 새 토큰을 쿠키에 저장
*/
@@ -152,16 +170,19 @@ export async function serverFetch(
// 재시도도 401이면 로그인으로
if (response.status === 401) {
console.warn('🔴 [serverFetch] Retry failed with 401, redirecting to login...');
await clearTokenCookies(); // ⚠️ 무한 루프 방지: 쿠키 삭제 후 redirect
redirect('/login');
}
} else {
// 리프레시 실패 → 로그인 페이지로
console.warn('🔴 [serverFetch] Token refresh failed, redirecting to login...');
await clearTokenCookies(); // ⚠️ 무한 루프 방지: 쿠키 삭제 후 redirect
redirect('/login');
}
} else if (response.status === 401 && !options?.skipAuthCheck) {
// refresh_token이 없는 경우
console.warn(`[serverFetch] 401 Unauthorized (no refresh token): ${url} → 로그인 페이지로 이동`);
await clearTokenCookies(); // ⚠️ 무한 루프 방지: 쿠키 삭제 후 redirect
redirect('/login');
}