[FIX] 홈 새로고침 시 로그인 이탈 수정 및 인증 가드 클라이언트 전환 - #174
Conversation
next.config.js의 rewrites()는 백엔드 응답의 Set-Cookie 헤더 여러 개를
Node fetch의 Headers.get('set-cookie')가 콤마로 합쳐 단일 문자열로 반환해
브라우저가 refreshToken/sessionId를 모두 파싱하지 못하고 유실시켰습니다.
토큰 교환(/api/v1/auth/token)과 재발급(/auth/reissue) 요청을 Route Handler로
직접 프록시하고 getSetCookie()로 각 Set-Cookie를 분리해 독립적인 헤더로
내려주도록 변경했습니다.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[문제] 홈 새로고침 시 /login으로 이탈 accessToken이 Zustand 메모리에만 있어 새로고침 시 사라지고, 개발 환경(HTTP)에서는 Secure 쿠키 제약으로 reissue도 동작하지 않아 AuthGuardProvider가 /login으로 리다이렉트했습니다. [변경] proxy.ts: 쿠키 기반 인증 가드 전체 제거, next-intl 미들웨어만 남김. /auth 경로가 로케일 리다이렉트를 타지 않도록 matcher에서 제외했습니다. axios.ts: reissueAccessToken을 export하고 baseURL: ""를 명시해 재발급 요청이 정확히 /auth/reissue로 나가도록 수정했습니다. useAuthStore: setAccessToken/clearAccessToken 호출 시 sessionStorage에도 저장하도록 변경했습니다. sessionStorage는 새로고침엔 살아남고 탭을 닫으면 삭제되어 localStorage보다 보안적으로 낫습니다. 스토어 초기값은 SSR/CSR hydration mismatch를 방지하기 위해 null로 유지하고 getStoredAccessToken을 export했습니다. AuthGuardProvider: isReady state를 제거(!!accessToken과 중복)하고 sessionStorage 폴백을 추가했습니다. 토큰이 없을 때 sessionStorage를 먼저 확인해 복원하고, 없을 때만 reissue를 호출합니다. OauthCallbackContainer: 로그인 응답의 user.onboardingCompleted를 store에 반영했습니다. OnboardingFunnelContainer: 온보딩 완료 시 setOnboardingCompleted(true) 호출 추가했습니다. onboarding/page.tsx, (main)/layout.tsx: AuthGuardProvider에 온보딩 완료 여부 기반 가드 prop을 적용했습니다. LoginContainer: token이 없을 때만 렌더링하도록 변경했습니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
WalkthroughAPI 요청을 상대 경로 프록시로 전환하고 인증 쿠키 전달 라우트를 추가했습니다. 인증 스토어에 세션 토큰과 온보딩 상태를 저장하며, 인증 가드가 토큰 복원·재발급과 온보딩별 라우팅을 처리하도록 변경했습니다. Changes인증 및 라우팅 흐름
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant AuthGuardProvider
participant AuthStore
participant reissueAccessToken
participant Router
Browser->>AuthGuardProvider: 보호된 라우트 접근
AuthGuardProvider->>AuthStore: sessionStorage 토큰 조회
AuthGuardProvider->>reissueAccessToken: 저장 토큰이 없으면 재발급 요청
reissueAccessToken-->>AuthGuardProvider: accessToken 반환
AuthGuardProvider->>AuthStore: 인증 상태 설정
AuthGuardProvider->>Router: 온보딩 또는 홈으로 이동
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
@repo/timo-design-system/assets/*.svg 임포트에 대한 타입 선언이 없어 tsc --noEmit 실행 시 TS2307 에러가 발생했습니다. global.d.ts에 declare module "*.svg"를 추가했습니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/timo-web/app/`[locale]/login/_containers/LoginContainer.tsx:
- Around line 21-32: Update LoginContainer’s authentication initialization so
getStoredAccessToken() is not called during render. Store the token in useState
with an SSR-safe initial value, then read sessionStorage via a mount-time
useEffect and update the state before applying the existing redirect logic;
ensure SSR and initial hydration render the same login UI while authenticated
users are redirected after mount.
In
`@apps/timo-web/app/`[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx:
- Around line 18-20: OauthCallbackContainer에서 onboardingCompleted를 Zustand에 저장하지
말고, queryClient의 사용자 프로필 캐시를 단일 데이터 소스로 사용하도록 setOnboardingCompleted 호출과 관련 상태
의존성을 제거하세요. 라우팅 가드가 새로고침 후에도 값을 사용할 수 있도록 React Query 캐시에서 파생된 값을 사용하고, 필요한 경우
React Query persist 설정을 재사용하세요.
In `@apps/timo-web/app/api/api/v1/auth/token/route.ts`:
- Around line 10-33: Extract the duplicated backend proxy flow from POST in the
token route into a shared proxyToBackend helper, including request forwarding,
response body/status/content-type copying, and Set-Cookie propagation. Update
both the token and reissue route handlers to delegate to this helper with their
respective backend paths and forwarding options, preserving their current
behavior.
- Around line 10-33: Update the POST route handler’s backend fetch to use an
AbortController with a finite timeout, clearing the timeout after completion.
Wrap the fetch and response forwarding flow in try/catch, returning a 504
response when the request is aborted by the timeout and a 502 response for other
backend connection or proxy errors, while preserving successful response and
cookie forwarding.
In `@apps/timo-web/providers/auth/AuthGuardProvider.tsx`:
- Around line 21-27: Update the AuthGuardProvider session-restoration flow so
reissueAccessToken() also refreshes onboardingCompleted from the server via
getMyProfile, rather than restoring only accessToken. Ensure the login and
token-reissue paths persist the returned profile onboarding state before
routing, preventing completed users from being redirected to /onboarding.
In `@apps/timo-web/stores/auth/useAuthStore.ts`:
- Around line 22-62: Restrict the sessionStorage persistence in setAccessToken
to development-only execution, while continuing to update the in-memory
accessToken state in every environment. Apply the same environment guard to
sessionStorage removal in clearAccessToken so production does not interact with
client storage; leave the onboardingCompleted persistence and merge behavior
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0d52ab16-8143-4048-93c0-7c284519c703
📒 Files selected for processing (12)
apps/timo-web/api/client/axios.tsapps/timo-web/app/[locale]/(main)/layout.tsxapps/timo-web/app/[locale]/login/_containers/LoginContainer.tsxapps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsxapps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsxapps/timo-web/app/[locale]/onboarding/page.tsxapps/timo-web/app/api/api/v1/auth/token/route.tsapps/timo-web/app/auth/reissue/route.tsapps/timo-web/next.config.jsapps/timo-web/providers/auth/AuthGuardProvider.tsxapps/timo-web/proxy.tsapps/timo-web/stores/auth/useAuthStore.ts
| const setOnboardingCompleted = useAuthStore( | ||
| (state) => state.setOnboardingCompleted, | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift
onboardingCompleted를 Zustand에 저장하는 것은 경로 지침에 위배됩니다.
onboardingCompleted는 백엔드 사용자 프로필 응답에서 파생된 API 서버 데이터입니다. 경로 지침에 따르면 "API 서버 데이터를 store에 저장 금지 (React Query가 담당)"해야 합니다. 현재 OauthCallbackContainer에서 queryClient.setQueryData로 React Query에도 저장하므로, Zustand와 React Query 간 상태 불일치 가능성이 있습니다.
새로고침 시 라우팅 가드가 즉시 온보딩 상태를 알아야 하는 실용적 필요성은 이해합니다. @tanstack/react-query-persist-client로 React Query 캐시를 영속화하거나, onboardingCompleted 대신 React Query 캐시에서 파생된 값을 사용하는 방안을 검토해 보세요.
As per path instructions, "API 서버 데이터를 store에 저장 금지 (React Query가 담당)" 및 "파생 값은 store에 저장하지 않고 selector로 계산" 규칙이 apps/timo-web/stores/** 경로에 적용됩니다.
Also applies to: 41-41, 50-57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
around lines 18 - 20, OauthCallbackContainer에서 onboardingCompleted를 Zustand에
저장하지 말고, queryClient의 사용자 프로필 캐시를 단일 데이터 소스로 사용하도록 setOnboardingCompleted 호출과 관련
상태 의존성을 제거하세요. 라우팅 가드가 새로고침 후에도 값을 사용할 수 있도록 React Query 캐시에서 파생된 값을 사용하고, 필요한
경우 React Query persist 설정을 재사용하세요.
Source: Path instructions
| export async function POST(request: NextRequest) { | ||
| const body = await request.text(); | ||
|
|
||
| const backendResponse = await fetch(`${apiBaseUrl}/api/v1/auth/token`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body, | ||
| }); | ||
|
|
||
| const responseBody = await backendResponse.text(); | ||
| const response = new NextResponse(responseBody, { | ||
| status: backendResponse.status, | ||
| headers: { | ||
| "Content-Type": | ||
| backendResponse.headers.get("content-type") ?? "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| for (const cookie of backendResponse.headers.getSetCookie()) { | ||
| response.headers.append("set-cookie", cookie); | ||
| } | ||
|
|
||
| return response; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
reissue/route.ts와 동일한 프록시 로직이 중복됩니다.
두 Route Handler가 fetch → body 복사 → getSetCookie() 반복 패턴을 공유합니다. 공유 헬퍼로 추출하면 에러 처리·타임아웃 추가도 한 곳에서 해결됩니다.
♻️ 공유 프록시 헬퍼 추출 제안
// lib/proxyBackend.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
export async function proxyToBackend(
request: NextRequest,
backendPath: string,
options?: { forwardBody?: boolean; forwardCookie?: boolean },
) {
const fetchOptions: RequestInit = {
method: request.method,
headers: {
...(options?.forwardBody
? { "Content-Type": "application/json" }
: {}),
...(options?.forwardCookie
? { cookie: request.headers.get("cookie") ?? "" }
: {}),
},
};
if (options?.forwardBody) {
fetchOptions.body = await request.text();
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const backendResponse = await fetch(
`${API_BASE_URL}${backendPath}`,
{ ...fetchOptions, signal: controller.signal },
);
const responseBody = await backendResponse.text();
const response = new NextResponse(responseBody, {
status: backendResponse.status,
headers: {
"Content-Type":
backendResponse.headers.get("content-type") ?? "application/json",
},
});
for (const cookie of backendResponse.headers.getSetCookie()) {
response.headers.append("set-cookie", cookie);
}
return response;
} catch (error) {
/* 에러 처리 */
} finally {
clearTimeout(timeout);
}
}이 헬퍼를 두 Route Handler에서 사용:
// token/route.ts
export const POST = (request: NextRequest) =>
proxyToBackend(request, "/api/v1/auth/token", { forwardBody: true });
// reissue/route.ts
export const POST = (request: NextRequest) =>
proxyToBackend(request, "/api/v1/auth/reissue", { forwardCookie: true });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/api/api/v1/auth/token/route.ts` around lines 10 - 33,
Extract the duplicated backend proxy flow from POST in the token route into a
shared proxyToBackend helper, including request forwarding, response
body/status/content-type copying, and Set-Cookie propagation. Update both the
token and reissue route handlers to delegate to this helper with their
respective backend paths and forwarding options, preserving their current
behavior.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target route file and nearby context
git ls-files 'apps/timo-web/app/api/api/v1/auth/token/route.ts'
wc -l apps/timo-web/app/api/api/v1/auth/token/route.ts
cat -n apps/timo-web/app/api/api/v1/auth/token/route.ts
# Look for similar route handlers and any shared timeout/error handling patterns
rg -n "AbortController|setTimeout\\(|getSetCookie\\(|new NextResponse\\(|fetch\\(" apps/timo-web/app/api -SRepository: Team-Timo/Timo-client
Length of output: 1939
백엔드 프록시 호출에 타임아웃과 예외 처리를 추가하세요
백엔드가 지연되거나 연결 실패하면 이 Route Handler가 오래 대기하거나 예외로 끝날 수 있습니다. AbortController로 제한 시간을 두고 try/catch에서 502/504로 응답을 정리하세요.
Next.js Route Handler의 에러 처리: https://nextjs.org/docs/app/building-your-application/routing/route-handlers
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/api/api/v1/auth/token/route.ts` around lines 10 - 33,
Update the POST route handler’s backend fetch to use an AbortController with a
finite timeout, clearing the timeout after completion. Wrap the fetch and
response forwarding flow in try/catch, returning a 504 response when the request
is aborted by the timeout and a 502 response for other backend connection or
proxy errors, while preserving successful response and cookie forwarding.
| const accessToken = useAuthStore((state) => state.accessToken); | ||
| const setAccessToken = useAuthStore((state) => state.setAccessToken); | ||
| const onboardingCompleted = useAuthStore( | ||
| (state) => state.onboardingCompleted, | ||
| ); | ||
| const router = useRouter(); | ||
| const { mutate } = useReissue(); | ||
| const hasChecked = useRef(false); | ||
| const [isReady, setIsReady] = useState(!!accessToken); | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "setOnboardingCompleted|onboardingCompleted" apps/timo-web -g '*.ts*' -B2 -A5Repository: Team-Timo/Timo-client
Length of output: 11812
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== useAuthStore =="
sed -n '1,220p' apps/timo-web/stores/auth/useAuthStore.ts
echo
echo "== AuthGuardProvider =="
sed -n '1,220p' apps/timo-web/providers/auth/AuthGuardProvider.tsx
echo
echo "== search reissue / profile restore flows =="
rg -n "reissueAccessToken|refreshToken|getMyProfile|setOnboardingCompleted|onboardingCompleted|setAccessToken|getStoredAccessToken" apps/timo-web -g '*.ts*' -B2 -A5Repository: Team-Timo/Timo-client
Length of output: 33871
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== AuthGuardProvider usages =="
rg -n "AuthGuardProvider|requireOnboardingCompleted|redirectIfOnboardingCompleted" apps/timo-web -g '*.ts*' -B2 -A4
echo
echo "== files around AuthGuardProvider call sites =="
for f in $(rg -l "requireOnboardingCompleted|redirectIfOnboardingCompleted" apps/timo-web -g '*.ts*'); do
echo "--- $f ---"
sed -n '1,220p' "$f"
echo
doneRepository: Team-Timo/Timo-client
Length of output: 10065
세션 복원 시 onboardingCompleted도 다시 채워주세요. 상태 분리는 깔끔한데, reissueAccessToken()은 accessToken만 복원해서 onboardingCompleted가 비어 있으면 이미 온보딩을 끝낸 사용자도 /onboarding으로 잘못 보낼 수 있습니다. 로그인/재발급 시 getMyProfile로 서버 값을 다시 반영하는 흐름이 필요해요. 참고: zustand persist middleware
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/providers/auth/AuthGuardProvider.tsx` around lines 21 - 27,
Update the AuthGuardProvider session-restoration flow so reissueAccessToken()
also refreshes onboardingCompleted from the server via getMyProfile, rather than
restoring only accessToken. Ensure the login and token-reissue paths persist the
returned profile onboarding state before routing, preventing completed users
from being redirected to /onboarding.
next typegen 등 빌드 외 명령도 next.config.js를 모듈 로드 시 실행하므로, NEXT_PUBLIC_API_BASE_URL이 없는 CI type-check 환경에서 최상단 throw가 발생해 next typegen이 크래시나고 tsc가 SVG 모듈을 찾지 못하는 문제가 있었습니다. env 체크를 rewrites() 호출 시점으로 미뤄 빌드/서버 시작에서만 검증합니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reissue가 배포 환경에서만 동작하므로 sessionStorage 폴백은 불필요합니다. setAccessToken/clearAccessToken을 원래 메모리 전용 방식으로 되돌립니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LoginContainer에 남아있던 getStoredAccessToken 참조를 제거하고 accessToken만으로 인증 여부를 판단하도록 단순화했습니다. next.config.js의 rewrites()에서 NEXT_PUBLIC_API_BASE_URL이 없을 때 throw 대신 빈 배열을 반환하도록 변경했습니다. next typegen이 rewrites()를 호출할 때 env가 없어도 크래시나지 않습니다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ISSUE 🔗
close #166
What is this PR? 🔍
로그인 후 홈에서 새로고침하면
/login으로 튕기는 문제를 수정하고, 미들웨어 기반 라우트 가드를 클라이언트AuthGuardProvider로 전환했습니다.배경
proxy.ts)가 쿠키를 읽어 인증 여부를 판단하고, 비인증 상태면/login으로 리다이렉트하는 구조였습니다./login으로 리다이렉트됐습니다.next.config.js의rewrites()가 백엔드 응답의Set-Cookie헤더 여러 개를 하나의 문자열로 합쳐버려 브라우저가 파싱하지 못해refreshToken/sessionId쿠키가 모두 유실됐습니다.Path=/auth가 지정돼 있어/api경로 요청에는 쿠키가 실리지 않았습니다.AuthGuardProvider로 전환했습니다.Set-Cookie 프록시 —
app/api/api/v1/auth/token/route.ts,app/auth/reissue/route.tsrewrites()는 내부적으로 Nodefetch를 사용하는데,Headers.get('set-cookie')가 여러 값을 콤마로 이어붙인 단일 문자열로 반환합니다. 이를 그대로 클라이언트에 내려보내면 브라우저가 쿠키 2개를 모두 파싱하지 못하고 유실합니다.backendResponse.headers.getSetCookie()로 각 Set-Cookie 값을 배열로 분리한 뒤,response.headers.append('set-cookie', cookie)로 하나씩 추가해 각각이 독립적인 헤더로 내려가도록 합니다.next.config.js/proxy.ts— 미들웨어 및 rewrite 재구성/api/:path*→${apiBaseUrl}/:path*, 인증 경로를/auth/:path*→${apiBaseUrl}/api/v1/auth/:path*rewrite로 전환하고, 미들웨어의 쿠키 기반 인증 가드를 전부 제거했습니다.AuthGuardProvider로 일원화해 역할을 분리했습니다./auth/reissue요청이 미들웨어(next-intl)를 타면 로케일 리다이렉트가 발생할 수 있어proxy.tsmatcher에서auth를 제외했습니다.useAuthStore— 토큰 저장 전략accessToken은 Zustand 메모리에만 저장하고,onboardingCompleted만 localStorage에 persist하도록 유지했습니다.accessToken까지 저장하던 사용자가 남아 있을 수 있어, 기본 merge 대신onboardingCompleted만 명시적으로 복원하는 커스텀merge를 유지했습니다.AuthGuardProvider— 가드 로직 정비isReadystate를 제거했습니다.isReady는!!accessToken과 항상 동일한 값을 추적했습니다. 별도 state를 유지하면 reissue 완료 시점에 두 곳을 동시에 갱신해야 하는 불일치 위험이 있었습니다./login으로 리다이렉트합니다.온보딩 / 로그인 가드
AuthGuardProvider에requireOnboardingCompleted/redirectIfOnboardingCompletedprop을 추가하고 각 페이지에 적용했습니다.OauthCallbackContainer가 로그인 응답의user.onboardingCompleted를 store에 반영하지 않아 온보딩 완료 사용자도 온보딩으로 재진입했습니다./login은 인증된 상태에서 직접 접근하면 폼이 먼저 보이고 이후에 리다이렉트되는 문제가 있었습니다.OauthCallbackContainer에서setOnboardingCompleted(Boolean(data.user?.onboardingCompleted))를 호출하고,LoginContainer는 token이 없을 때만 렌더링하도록 변경했습니다.To Reviewers
axios.ts의 reissue 싱글턴 패턴(중복 호출 방지)은 유지했습니다.AuthGuardProvider의 reissue 경로와 axios interceptor의 401 → reissue 경로가 동시에 실행되는 케이스를 한 번 봐주세요.쿠키
Path=/auth와 로컬 Secure 쿠키 제약은 백엔드 협의가 필요한 영역으로 이번 PR 범위 밖입니다.Screenshot 📷
Test Checklist ✔
/home진입 확인/onboarding직접 접근 시/home으로 리다이렉트 확인/home직접 접근 시/login으로 리다이렉트 확인pnpm build— CI에서 확인 예정