프론트엔드백엔드

게스트가 마이페이지도 못 열던 문제 — 탭별 접근 정책 한 곳에서 정의하기 — POP-SPOT v2.8

김동현
··3분 읽기

v2.7 게스트 재설계 후, 게스트가 마이페이지·동행 여권도 차단되던 것을 수정. 지도·마이페이지·동행 여권은 게스트 OK / 코스·음악·메이트는 회원전용. 탭 클릭/sessionStorage 복원/URL 직접 진입 세 곳 모두 같은 함수 호출.

v2.7 에서 게스트 시작 방식을 고쳐놓고 알고 보니 게스트가 마이페이지·동행 여권을 열려고 하는 순간 "로그인이 필요해요" 가 떨어졌다. 게스트 모드가 사실상 무의미해지는 구조.

이 글에서 다루는 것

  • 게스트가 "둘러보는" 권한의 범위 — 어디까지 허용하고 어디부터 막을지 구분
  • "로그인 필요" 대 "회원전용 기능" 을 언어적으로 구분
  • 탭 클릭 / 이전 탭 복원 / URL 으로 직접 진입 세 곳 모두 같은 권한 검사 함수를 쓰도록 단일화
  • 인트로 첫·마지막 화면에 "게스트로 7일" 안내 추가
  • 모르는 단어 한 줄로

    용어한 줄 설명
    sessionStoragelocalStorage 와 비슷하지만 탭/창 닫으면 지워지는 임시 저장소
    SearchParams주소 끝의 <code>?tab=music</code> 같은 쿼리 파라미터
    권한 검사 함수"이 사용자가 이 탭을 열 수 있는가?" 를 단 한 곳에서 판단하는 함수

    무엇이 바뀌었나

    항목v2.7v2.8
    게스트의 탭 접근마이페이지/동행 여권/메이트 모두 차단마이페이지/동행 여권은 게스트도 OK / 코스·음악·메이트는 회원만
    회원전용 탭 시도 시"로그인이 필요합니다" (구분 없음)게스트 → "회원 전용 기능", 비로그인 → "로그인 필요"
    sessionStorage 복원이전 탭이 코스면 게스트도 그대로 진입 가능권한 검사 후 불가면 지도 탭으로 대체
    <code>?tab=music</code> URL 진입주소만으로 진입 가능 (우회)같은 권한 검사로 차단
    인트로 안내게스트 옵션 안 보임첫/마지막 화면에 "게스트로 7일" 안내

    왜 이렇게 했음

    게스트의 "둘러보기" 의미 — 단순 조회 (지도·팝업 리스트) 는 게스트가 어차피 볼 수 있으니 제한하면 도리어 불친절함. 마이페이지·동행 여권은 내 계정 기준 표시이니 비워있는 상태로 보이면 됨. 대신 "계정이 있어야 내용이 생기는" 기능 (코스 저장, 음악 패스포트, 메이트 메시지) 은 게스트는 차단하게.

    메시지를 구분한다 — "로그인이 필요합니다" 는 비로그인 사용자에게 맞는 문구. 게스트 사용자에게는 "회원이어야 한다" 가 있으므로 "회원 전용 기능이에요" 이 정확.

    단일 진실의 원천 — 탭 접근 권한을 메인 파일 최상단에 한 번 정의하고, 탭 클릭·sessionStorage 복원·URL 진입 세 군데가 모두 그 함수를 호출하게 하면 한 곳만 고쳐도 세 곳이 모두 동일.


    코드로는 어떻게 (필요한 부분만)

    파일: popspot-frontend/src/app/page.tsx (메인 페이지 최상단 + 내부 이벤트 핸들러)

    typescript
    // v2.8 — app/page.tsx 최상단. 권한 결정의 단일 진실 원천
    const MEMBER_ONLY = ['course', 'music', 'mate'] as const;
    // 위: 회원만 접근 가능한 탭 이름 목록. 그 외 (map, mypage, passport) 은 게스트도 OK
    //      'as const' — readonly ['course', 'music', 'mate'] 타입으로 고정. 타입 검사가 더 엄격
    
    function canAccess(tab: string, user: User | null, guest: GuestState) {
      // 위: 권한 판단 함수. 세 곳 (탭클릭/storage/URL) 에서 공통으로 호출
      if (user) return true;
      // 위: 로그인한 사용자는 아무 탭이나 접근 가능
    
      if ((MEMBER_ONLY as readonly string[]).includes(tab)) {
        //              ^^^^^^^^^^^^^^^^^^^^^               TS 가 readonly tuple 을 string[] 로 볼 수 있도록 넓힘
        //                                    ^^^^^^^^^^^^   실행 시에는 단순 배열 포함 검사
        return { ok: false, reason: guest.active ? 'memberOnly' : 'needLogin' };
        //                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   메시지가 달라지는 이유
        //                  memberOnly: 게스트 사용자 → "회원 전용 기능"
        //                  needLogin:  비로그인 → "로그인 필요"
      }
      return guest.active ? true : { ok: false, reason: 'needLogin' };
      // 위: 회원 전용은 아닌 탭 (map 등) — 게스트 활성이면 허용, 아니면 로그인 요구
    }
    
    // v2.8 — 호출 지점 1: 탭 클릭 이벤트
    const handleTabClick = (tab: string) => {
      const r = canAccess(tab, user, guest);
      if (r === true) setTab(tab);
      // 위: 허가되면 탭 전환
      else notify.error(r.reason === 'memberOnly'
          ? '회원 전용 기능이에요' : '로그인 필요');
      // 위: 거부 이유에 따라 메시지 구분·표시. r 은 이제 { ok:false, reason } 구조
    };
    
    // v2.8 — 호출 지점 2: 이전 방문 세션의 탭 복원
    useEffect(() => {
      const saved = sessionStorage.getItem('tab');
      // 위: 이전에 마지막에 보고 있던 탭 이름 (세션 유지)
      if (saved && canAccess(saved, user, guest) === true) setTab(saved);
      else setTab('map');
      // 위: 복원하려는 탭에 권한이 있으면 설정, 없으면 기본 'map' 탭으로 대체
      //      v2.7 에서는 권한 검사 없이 복원해서 게스트가 course 탭으로 잠입되는 우회로가 될 수 있었음
    }, []);
    
    // v2.8 — 호출 지점 3: URL 의 ?tab=music 으로 직접 진입
    useEffect(() => {
      const fromUrl = searchParams.get('tab');
      // 위: useSearchParams 훅에서 쿼리 읽기
      if (fromUrl && canAccess(fromUrl, user, guest) !== true) {
        router.replace('/');
        // 위: 권한 없는 URL 은 루트로 교체. replace 는 히스토리 남기지 않음 (뒤로가기로 돌아올 수 없음)
      }
    }, [searchParams]);
    // 세 지점 모두 동일한 canAccess() 를 호출 — 규칙 변경 시 한 곳만 차례 고치면 됨

    핵심 파일: popspot-frontend/src/app/page.tsx, popspot-frontend/src/app/intro/page.tsx


    관련 글

  • 이전 — v2.7, 보안 Critical 3건
  • 다음 — v2.9, 같은 IDOR 패턴이 MyCourse·Wishlist 에 또 남아 있던 일
  • 공유

    댓글