Skip to content

feat: 접근 제어 구현#67

Merged
dew102938 merged 6 commits into
developfrom
feat/route-guard
Feb 11, 2026
Merged

feat: 접근 제어 구현#67
dew102938 merged 6 commits into
developfrom
feat/route-guard

Conversation

@dew102938

@dew102938 dew102938 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

💡 개요

접근 제어 구현

🔢 관련 이슈 링크

💻 작업내용

  • 비로그인 유저의 접근을 차단하고 메인 페이지(/)로 리다이렉트하는 RouteGuards 컴포넌트 구현
  • 식당 '예약하기' 버튼 클릭 시, 로그인 상태를 체크하여 비로그인 유저에게 알림을 띄우고 메인으로 이동시킴

📌 변경사항PR

  • FEAT: 새로운 기능 추가
  • FIX: 버그/오류 수정
  • CHORE: 코드/내부 파일/설정 수정
  • DOCS: 문서 수정(README 등)
  • REFACTOR: 코드 리팩토링 (기능 변경 없음)
  • TEST: 테스트 코드 추가/수정
  • STYLE: 스타일 변경(포맷, 세미콜론 등)

🤔 추가 논의하고 싶은 내용

✅ 체크리스트

  • 브랜치는 잘 맞게 올렸는지
  • 관련 이슈를 맞게 연결했는지
  • 로컬에서 정상 동작을 확있했는지
  • 충돌이 없다(또는 브랜치에서 충돌 해결 후 PR 업데이트 완료)

Summary by CodeRabbit

  • 새로운 기능
    • 내페이지, 매장, 결제, 예약 등 특정 경로를 인증된 사용자 전용으로 이동
    • 보호된 페이지 접근 시 로그인 모달을 자동으로 열도록 리다이렉트 상태 전달
    • 예약 버튼 클릭 시 인증 여부 확인 후 흐름 진행(미인증 시 로그인 유도)
    • 공개 경로와 보호된 경로를 분리한 라우팅 구조 재조정
    • 인증 상태(hydration) 완료 전 렌더링 대기 처리 추가

@dew102938 dew102938 self-assigned this Feb 11, 2026
@dew102938 dew102938 added the feat label Feb 11, 2026
@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

비로그인 사용자의 접근 제어를 위해 PrivateRoute를 도입하고 라우팅을 재구성했습니다. mypage, 상점 관련, 결제, 예약 완료 경로를 인증 보호로 이동시키고, 공개 경로(/search 등)는 PublicLayout에 남깁니다. 헤더는 location.state?.openLogin 신호를 받아 로그인 다이얼로그를 자동으로 엽니다. 예약 버튼과 상세 모달에도 클라이언트 인증 게이트가 추가되었습니다. 인증 상태는 hasHydrated 플래그로 초기화 완료 여부를 확인합니다.

변경사항

Cohort / File(s) Summary
라우팅 재구성 & 보호 경로
src/App.tsx, src/components/RouteGuards.tsx
PrivateRoute 추가 및 라우팅 구조 변경: mypage, 상점 등록/상세, 결제(success/fail), 예약 완료 등을 PrivateRoute로 래핑해 비인증 사용자는 /로 리다이렉트하고 state: { openLogin: true } 전송. NotFound는 최상위 유지.
헤더: 로그인 프롬프트 처리
src/components/main/Header.tsx
useLocation 의존성 추가. location.state?.openLogin 감지 시 로그인 다이얼로그 자동 오픈 후 history.replace로 상태 초기화(타이머 사용). 타이머 클린업 포함.
예약 버튼 인증 게이트
src/components/restaurant/RestaurantDetailModal.tsx
예약(Reserve) 클릭 핸들러를 인증 체크로 래핑: 미인증 시 alert, 모달 닫기, 홈으로 네비게이트(state.openLogin) 후 중단; 인증 시 기존 콜백 실행.
인증 상태 관리 변경
src/stores/useAuthStore.ts
hasHydrated: boolean 필드 추가 및 persist의 onRehydrateStorage에서 hydration 완료 시 hasHydrated = true 설정. PrivateRoute는 hydration 완료 전 렌더링을 보류(렌더 null).
매니페스트 참고
package.json
persist 관련 영향 가능성 표기(간접).

Sequence Diagram(s)

sequenceDiagram
    actor User as 사용자
    participant Browser as 브라우저
    participant App as App (Router)
    participant PrivateRoute as PrivateRoute
    participant AuthStore as AuthStore
    participant Header as Header

    Browser->>App: 요청 /mypage (또는 보호된 경로)
    App->>PrivateRoute: 라우트 검사 시작
    PrivateRoute->>AuthStore: hasHydrated?, isAuthenticated? 조회
    alt hasHydrated = false
        AuthStore-->>PrivateRoute: 아직 초기화 중 → 렌더 지연 (null)
    else hasHydrated = true and isAuthenticated = false
        PrivateRoute->>Browser: navigate('/', { state: { openLogin: true } })
        Browser->>Header: location.state.openLogin 감지
        Header->>Header: setLoginOpen(true) 및 history.replace(path, {}) 실행
    else isAuthenticated = true
        PrivateRoute->>App: Outlet 렌더 (요청 컴포넌트 표시)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • yooseolhee
  • jjjsun

Poem

PrivateRoute가 문지기 되어,
문 앞의 길을 살피네 🚪🔐
헤더는 신호 받아 창을 열고,
모달은 예약 전에 묻네,
작은 플래그 하나로 길이 열리다 🌙✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: 접근 제어 구현' clearly summarizes the main change - implementing access control - which aligns with the PR's primary objective of adding route guards and authentication checks.
Description check ✅ Passed The PR description follows the required template structure with overview, linked issues, work content, change type, and checklist. All critical sections are completed with relevant details.
Linked Issues check ✅ Passed The PR implements the core routing and authentication requirements from issue #66: PrivateRoute component for route protection [RouteGuards.tsx], login state checking on reservation action [RestaurantDetailModal.tsx], and redirects with login prompts [Header.tsx, App.tsx].
Out of Scope Changes check ✅ Passed All code changes are directly scoped to issue #66 objectives. Modified files (App.tsx, Header.tsx, RestaurantDetailModal.tsx, RouteGuards.tsx, useAuthStore.ts) all contribute to access control implementation with no extraneous changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/route-guard

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/components/restaurant/RestaurantDetailModal.tsx (1)

68-79: 인증 게이트 로직은 잘 구성되어 있어요 👍 — 다만 alert() 대신 커스텀 UI 고려해보면 좋겠어요.

전체적으로 흐름이 깔끔합니다:

  • 훅 호출 순서가 early return 전에 위치해서 React 규칙도 잘 지켜졌고,
  • replace: true로 뒤로가기 시 보호된 상태로 복귀하지 않는 점도 좋습니다.

두 가지만 참고해주세요:

  1. alert()는 브라우저 네이티브 다이얼로그라 UX가 일관되지 않아요. 프로젝트에 Toast/Snackbar 같은 알림 컴포넌트가 있다면 그걸로 교체하면 더 자연스러울 것 같아요. 지금 이슈 체크리스트에서 "UI 구성: 미완료"로 되어 있으니, 후속 작업에서 정리해도 괜찮겠습니다.

  2. hasHydrated 미확인: useAuthStore가 localStorage에서 hydration이 완료되기 전에는 isAuthenticated가 기본값(false)일 수 있어요. 모달이 사용자 인터랙션 이후에 열리니 실질적으로 문제가 될 가능성은 낮지만, 만약 hydration이 지연되는 환경(느린 기기 등)을 고려한다면 방어 코드를 넣어도 좋습니다.

🛡️ hasHydrated 방어 코드 예시
  const nav = useNavigate();
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+ const hasHydrated = useAuthStore((state) => state.hasHydrated);

  const handleReserveClick = () => {
+   if (!hasHydrated) return; // 아직 스토어 초기화 중이면 무시
    if (!isAuthenticated) {
      alert("로그인이 필요한 서비스입니다.");
      onOpenChange(false);
      nav("/", { state: { openLogin: true }, replace: true });
      return;
    }
    onClickReserve();
  };

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/App.tsx`:
- Around line 69-94: The StoreRegistrationPage route is outside the PublicLayout
so it renders without the common header; move or wrap the
"/mypage/store/register" route so it is a child of the same PublicLayout used by
the other protected routes (or wrap it in MyPageLayout inside the existing
PrivateRoute block) to ensure the header appears; update the route grouping that
contains PrivateRoute and PublicLayout (references: PrivateRoute, PublicLayout,
StoreRegistrationPage, MyPageLayout) so "/mypage/store/register" is nested with
the other /mypage routes.

In `@src/components/RouteGuards.tsx`:
- Around line 8-12: Remove the blocking browser alert in the useEffect that
watches isAuthenticated; instead rely on the existing Navigate(state={{
openLogin: true }}) flow so Header.tsx opens the login dialog, or replace the
alert with a non-blocking toast if you want an extra hint. Specifically, edit
the useEffect in RouteGuards (the block referencing isAuthenticated and alert)
to delete the alert(...) call and ensure the redirect/Navigate with state={{
openLogin: true }} is preserved so the login dialog in Header.tsx is the single
source of truth for prompting authentication.
- Around line 5-6: PrivateRoute reads isAuthenticated before zustand persist
hydration, causing false redirects; add a hasHydrated flag to the auth store and
wait for it in PrivateRoute. In useAuthStore (the persist config), add
hasHydrated to AuthState default (false), exclude it from partialize, and set
state.hasHydrated = true in onRehydrateStorage; in RouteGuards.tsx read
hasHydrated via useAuthStore((s) => s.hasHydrated) alongside isAuthenticated and
return null (or a loader) while hasHydrated is false, then run the existing
alert/Navigate logic once hydrated. Ensure references: PrivateRoute,
useAuthStore, hasHydrated, onRehydrateStorage, and persist.
🧹 Nitpick comments (3)
src/components/restaurant/RestaurantDetailModal.tsx (2)

71-79: 인증 체크 로직이 PrivateRoute와 중복돼요

alert("로그인이 필요한 서비스입니다.") + nav("/", { state: { openLogin: true } }) 패턴이 RouteGuards.tsx와 동일하게 반복되고 있어요. 여기서도 마찬가지로 alert와 로그인 다이얼로그가 이중으로 표시됩니다.

공통 로직을 커스텀 훅으로 추출하면 일관성 유지가 쉬워지고, 나중에 alert를 토스트로 교체할 때도 한 곳만 수정하면 돼요.

♻️ 커스텀 훅 추출 예시
// src/hooks/useRequireAuth.ts
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/useAuthStore";

export function useRequireAuth() {
  const nav = useNavigate();
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  const requireAuth = (onAuthenticated: () => void) => {
    if (!isAuthenticated) {
      // toast 등으로 교체 가능
      nav("/", { state: { openLogin: true } });
      return;
    }
    onAuthenticated();
  };

  return { isAuthenticated, requireAuth };
}

사용 예:

const { requireAuth } = useRequireAuth();

const handleReserveClick = () => {
  requireAuth(() => {
    onOpenChange(false); // 필요 시 modal close 등 전처리
    onClickReserve();
  });
};

68-69: 모달 컴포넌트에 네비게이션/스토어 의존성 추가에 대해

RestaurantDetailModal은 원래 순수하게 props 기반으로 동작하던 프레젠테이셔널 컴포넌트인데, 이제 useNavigateuseAuthStore를 직접 사용하게 되었어요. 단일 책임 원칙 측면에서 보면, 인증 게이팅 로직은 이 모달을 호출하는 부모 컴포넌트나 위에서 제안한 훅에서 처리하는 게 더 깔끔합니다.

당장은 동작에 문제 없으니 이번 PR에서 꼭 변경할 필요는 없지만, 향후 리팩터링 시 고려해 주세요.

As per coding guidelines, src/components/**: "컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크."

src/components/main/Header.tsx (1)

28-37: useEffect 의존성 배열 최적화 가능

현재 [location, nav]로 되어 있어서, location 객체는 모든 라우트 변경마다 새 참조가 생기기 때문에 이 effect가 매번 재실행돼요. 기능적으로는 location.state?.openLogin 조건 분기 덕분에 문제없지만, 불필요한 실행을 줄이려면 의존성을 좁힐 수 있어요.

💡 개선 예시
+ const openLogin = location.state?.openLogin;
+
  useEffect(() => {
-   if (location.state?.openLogin) {
+   if (openLogin) {
      const timer = setTimeout(() => {
        setLoginOpen(true);
        nav(location.pathname, { replace: true, state: {} });
      }, 0);
 
      return () => clearTimeout(timer);
    }
- }, [location, nav]);
+ }, [openLogin, location.pathname, nav]);

Comment thread src/App.tsx
Comment thread src/components/RouteGuards.tsx
Comment thread src/components/RouteGuards.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/stores/useAuthStore.ts`:
- Around line 55-57: The onRehydrateStorage handler currently mutates state
directly (state!.hasHydrated = true) which won't notify subscribers and can
crash if state is undefined; change it to call the store's setter instead (use
the provided set(...) or useAuthStore.setState(...)) and guard against undefined
rehydrate payload (only set hasHydrated when state param exists), e.g. use the
onRehydrateStorage callback to invoke set({ hasHydrated: true }) or
useAuthStore.setState({ hasHydrated: true }) so subscribers (e.g., PrivateRoute)
receive updates safely without non-null assertions.
🧹 Nitpick comments (1)
src/stores/useAuthStore.ts (1)

62-66: hasHydrated 전용 selector hook이 없어요

다른 상태값들(useAuthToken, useIsAuthenticated, useUserId)은 편의 hook이 있는데 hasHydrated만 빠져있네요. PrivateRoute 등에서 직접 useAuthStore(s => s.hasHydrated) 쓰면 동작은 하지만 일관성 차원에서 추가해두면 좋을 것 같아요.

✨ 제안
 export const useUserId = () => useAuthStore((s) => s.userId);
+export const useHasHydrated = () => useAuthStore((s) => s.hasHydrated);

Comment thread src/stores/useAuthStore.ts Outdated

@jjjsun jjjsun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 고생하셨어요!!

Comment thread src/components/restaurant/RestaurantDetailModal.tsx Outdated
@dew102938 dew102938 merged commit 249c4ca into develop Feb 11, 2026
1 check passed
@jjjsun jjjsun deleted the feat/route-guard branch February 11, 2026 18:39
@jjjsun jjjsun added the ✨ Feature 새로운 기능 추가 label Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 접근 제어 구현

2 participants