Skip to content

Conversation

@leeleeleeleejun
Copy link
Member

@leeleeleeleejun leeleeleeleejun commented Nov 24, 2025

#️⃣연관된 이슈

📝작업 내용

1. 카테고리 별 맛집 리스트 스와이프 전환 구현

목적:
모바일 배달 앱(예: 배민)과 유사한 네이티브 사용자 경험을 웹에서도 제공
구현 내용:
motion/react을 사용하여 좌우 드래그 제스처를 감지하고 적용했습니다.

2. 캠퍼스 변경 시 지도 중심 좌표 로직 개선

변경 내용:
메인 화면에서 캠퍼스(천안/공주/예산)를 변경할 때, 해당 캠퍼스에 맞는 초기 위치로 지도가 설정되도록 useLastMapCenterStore의 상태 업데이트 로직을 수정했습니다.
효과:
캠퍼스를 변경하고 지도로 진입했을 때, 엉뚱한 위치(이전 마지막 좌표)가 아닌 선택한 캠퍼스의 중심을 바로 보여줍니다.

스크린샷 (선택)

2025-11-24.3.55.03.mov

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 카테고리 간 스와이프 제스처 네비게이션 추가
    • 캠퍼스 선택 시 지도 중심 위치 자동 동기화
    • 마지막 지도 위치 저장 및 복원 기능
  • 스타일

    • 배너 텍스트 반응형 크기 조정
    • 빈 결과 화면 레이아웃 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Walkthrough

맵 센터 위치 추적 기능을 추가하고 카테고리 선택 시 스와이프 제스처를 구현했습니다. 기존 prevMapCenter 스토어를 lastMapCenter로 교체하고, 배너 텍스트 크기를 조정했으며, 카테고리 페이지에 동적 ID 업데이트 기능을 추가했습니다.

Changes

cohort / 파일 변경 요약
맵 센터 위치 상태 관리 리팩토링
apps/web/app/_store/prevMapCenter.ts, apps/web/app/_store/lastMapCenter.ts, apps/web/app/map/MapComponent.tsx
기존 prevMapCenter 스토어를 제거하고 새로운 lastMapCenter Zustand 스토어로 교체. MapComponent의 임포트 경로 업데이트
캠퍼스 선택 시 맵 센터 동기화
apps/web/app/_components/CampusSelector/CampusSelector.tsx
CAMPUS_LOCATIONuseLastMapCenterStore 임포트 추가. onSelectionChange에서 캠퍼스 변경 시 해당 위치로 맵 센터 업데이트
이벤트 배너 텍스트 크기 조정
apps/web/app/_components/eventBanners/FoodSlotMachineBanner.tsx, apps/web/app/_components/eventBanners/LuckyDrawBanner.tsx
Text 엘리먼트의 fontSize를 xl에서 lg로 변경하고, 반응형 className text-main sm:text-xl 추가
카테고리 상세 페이지 ID 업데이트 기능
apps/web/app/categories/[id]/CategoryDetailPage.tsx
Places 컴포넌트에 setIdFunc prop 추가하여 동적 ID 변경 기능 제공
장소 목록 스와이프 제스처 구현
apps/web/app/categories/[id]/_components/Places/Places.tsx
motion/react 기반 드래그/스와이프 제스처 추가. 컴포넌트 signature 업데이트로 setIdFunc prop 수용. 수평 드래그로 카테고리 전환 기능 구현
빈 장소 목록 레이아웃 수정
apps/web/app/categories/[id]/_components/Places/EmptyPlaces.tsx
className을 my-auto items-center gap-2 pb-40에서 h-full items-center justify-center gap-2 pb-40으로 변경. 전체 높이 컨테이너에 중앙 정렬 적용
카테고리 템플릿 export 제거
apps/web/app/categories/template.tsx
Template 컴포넌트의 기본 export 및 관련 임포트 제거

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Campus as CampusSelector
    participant Store as lastMapCenterStore
    participant Map as MapComponent
    
    User->>Campus: 캠퍼스 선택
    activate Campus
    Campus->>Store: setLastMapCenter(위치)
    activate Store
    Store->>Map: 상태 업데이트
    deactivate Store
    Map->>Map: 맵 센터 이동
    deactivate Campus
    
    Note over Campus,Map: 사용자가 캠퍼스를 변경하면<br/>해당 위치가 저장되어<br/>맵 페이지에서 복원 가능
Loading
sequenceDiagram
    participant User as 사용자
    participant Place as Places Component
    participant Handler as onDragEnd Handler
    participant Callback as setIdFunc
    
    User->>Place: 드래그/스와이프
    activate Place
    Place->>Handler: onDragEnd 호출<br/>(offset, velocity)
    activate Handler
    Handler->>Handler: 스와이프 파워 계산<br/>threshold 확인
    alt 스와이프 감지
        Handler->>Callback: 새로운 ID로 호출<br/>(범위: 1-15)
    end
    deactivate Handler
    deactivate Place
    
    Note over Place,Callback: 수평 드래그 제스처로<br/>카테고리 전환 구현
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

추가 검토 필요 영역:

  • Places.tsx의 드래그 제스처 로직: 스와이프 파워 계산, 임계값 설정, ID 범위 경계 처리(1-15)의 정합성 검증 필요
  • lastMapCenter 스토어 마이그레이션: 기존 prevMapCenter 제거와 새로운 스토어 도입이 다른 모듈과의 의존성을 완전히 대체하는지 확인
  • CampusSelector와 Maps 통합: CAMPUS_LOCATION 데이터와 실제 지도 좌표 간 일관성 검증
  • 반응형 텍스트 스타일: 배너 텍스트의 sm breakpoint 동작이 의도대로 렌더링되는지 확인

Possibly related PRs

Poem

🐰 캠퍼스마다 위치를 기억하고,
손가락 한번의 스와이프로 카테고리를 넘나들며,
배너 글씨도 다듬어져 더욱 읽기 좋으니,
지도는 돌아와도 마지막 자리를 잊지 않고,
사용자의 발걸음을 따라가는 앱이 되었네! 🗺️✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/#74 카테고리 별 맛집 페이지에서 스와이프로 카테고리 전환' accurately describes the main feature: adding swipe gesture navigation for category switching on the restaurant list page, which aligns with the primary changes in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/#74

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/categories/[id]/CategoryDetailPage.tsx (1)

17-24: window.history.replaceStatenull을 전달하면 Next Router 상태가 손상됩니다

검증 결과, 현재 코드의 근본 문제는 다음과 같습니다:

window.history.replaceState(null, '', `/categories/${id}`)

Next.js App Router는 window.history.state에 내부 라우터 상태를 저장하는데, null을 전달하면 이 상태가 덮어씌워집니다. 그 결과:

  • usePathname()이 제대로 업데이트되지 않을 수 있음
  • 브라우저 뒤로가기/앞으로가기 동작이 깨질 수 있음
  • 라우터 상태 불일치로 인한 예기치 않은 렌더링 문제 발생

해결 방안 (권장):

useRouter().replace()를 사용하거나, window.history.state를 보존해서 호출합니다:

-import { usePathname } from 'next/navigation'
+import { usePathname, useRouter } from 'next/navigation'

export const CategoryDetailPage = () => {
+  const router = useRouter()
   const { data: categories } = useSuspenseQuery(useCategoryQueries.list())
   const activeCategoryId = usePathname().split('/')[2] || '0'

   const setIdFunc = (id: string) => {
-    window.history.replaceState(null, '', `/categories/${id}`)
+    router.replace(`/categories/${id}`, { scroll: false })
   }

이렇게 하면 Next Router 상태와 usePathname()이 동기화되어, 스와이프 시 Places와 RowCategories 모두 올바른 카테고리로 재렌더링됩니다.

lines 17-24, 45-52

🧹 Nitpick comments (2)
apps/web/app/categories/[id]/_components/Places/Places.tsx (2)

17-47: 스와이프 ID 증감 로직의 하드코딩된 범위(1~15)와 숫자 변환은 향후 변경에 취약할 수 있습니다

  • currentCategoryIdNumber(id)로 파싱하고, 범위를 1~15로 고정해 두어서 카테고리 개수나 ID 스키마가 바뀌면 이 파일만 따로 수정해야 합니다.
  • 또한 id가 숫자가 아닌 문자열(슬러그 등)로 변경되면 Number(id)NaN이 되어 비교가 전부 실패하고, 스와이프가 묵살되는 문제가 생길 수 있습니다.

카테고리 범위와 타입이 고정이라면 지금도 동작은 맞지만, 유지보수성을 위해서:

  • 카테고리 최소/최대 ID를 상수로 선언해 import 하거나, 상위에서 prop으로 전달받는 형태로 magic number(1, 15)를 제거하는 것,
  • 혹은 id 타입을 type CategoryId = '1' | '2' | ...처럼 좁혀 두거나, Number(id) 결과가 NaN일 때는 조기 return 하는 방어 로직

을 고려해 보면 좋겠습니다.


2-3: 스와이프 임계값 미만 드래그 후 리스트가 옆으로 살짝 밀린 채 남지 않는지 확인이 필요합니다

motion.divdrag='x'만 걸려 있고 별도의 제약이나 복귀 애니메이션이 없어서, SWIPE_CONFIDENCE_THRESHOLD(50)를 넘지 못한 짧은 드래그 이후에도 콘텐츠가 약간 좌우로 이동된 상태로 남을 수 있습니다. 그렇게 되면 스와이프에 실패했는데도 리스트가 한쪽으로 치우쳐 보이는 어색한 UX가 나올 수 있습니다.

실제 기기에서 한번 확인해 보시고, 문제가 있다면 예를 들어:

  • dragConstraints로 이동 가능한 범위를 제한하거나,
  • onDragEnd 안에서 항상 x: 0으로 되돌리는 애니메이션을 트리거하는 방식

으로 드래그 실패 시에는 원위치로 자연스럽게 복귀시키는 걸 검토해 보시면 좋겠습니다.

Also applies to: 65-75

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1291fc7 and c4e2cca.

📒 Files selected for processing (10)
  • apps/web/app/_components/CampusSelector/CampusSelector.tsx (1 hunks)
  • apps/web/app/_components/eventBanners/FoodSlotMachineBanner.tsx (1 hunks)
  • apps/web/app/_components/eventBanners/LuckyDrawBanner.tsx (1 hunks)
  • apps/web/app/_store/lastMapCenter.ts (1 hunks)
  • apps/web/app/_store/prevMapCenter.ts (0 hunks)
  • apps/web/app/categories/[id]/CategoryDetailPage.tsx (1 hunks)
  • apps/web/app/categories/[id]/_components/Places/EmptyPlaces.tsx (1 hunks)
  • apps/web/app/categories/[id]/_components/Places/Places.tsx (1 hunks)
  • apps/web/app/categories/template.tsx (0 hunks)
  • apps/web/app/map/MapComponent.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/app/categories/template.tsx
  • apps/web/app/_store/prevMapCenter.ts
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/app/categories/[id]/CategoryDetailPage.tsx (1)
apps/web/app/categories/[id]/_components/Places/Places.tsx (1)
  • Places (17-77)
apps/web/app/_components/CampusSelector/CampusSelector.tsx (3)
apps/web/app/_store/campus.ts (1)
  • useCampusStore (9-12)
apps/web/app/_store/lastMapCenter.ts (1)
  • useLastMapCenterStore (20-23)
apps/web/app/_constants/campus.ts (1)
  • CAMPUS_LOCATION (9-13)
apps/web/app/categories/[id]/_components/Places/Places.tsx (5)
apps/web/app/_store/campus.ts (1)
  • useCampusStore (9-12)
apps/web/app/_apis/queries/place.ts (1)
  • usePlaceQueries (27-69)
apps/web/app/categories/[id]/_components/Places/EmptyPlaces.tsx (1)
  • EmptyPlaces (9-103)
packages/ui/src/components/Layout/VerticalScrollArea/VerticalScrollArea.tsx (1)
  • VerticalScrollArea (4-20)
apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (1)
  • PlaceListItem (15-55)
🔇 Additional comments (6)
apps/web/app/_components/eventBanners/LuckyDrawBanner.tsx (1)

23-27: 배너 본문 타이포그래피 조정 일관성 좋습니다.

기존 구조는 그대로 두고 폰트 사이즈만 조정하면서 sm 이상에서만 크게 보이도록 처리해, 다른 배너들과의 시각적 일관성이 잘 맞습니다.

apps/web/app/_components/eventBanners/FoodSlotMachineBanner.tsx (1)

22-26: 슬롯머신 배너 텍스트도 LuckyDraw와 일관되게 정리됨

폰트 크기/굵기와 반응형 클래스 구성이 LuckyDrawBanner와 동일 패턴이라, 두 이벤트 배너 간 UI 톤이 잘 맞습니다.

apps/web/app/categories/[id]/_components/Places/EmptyPlaces.tsx (1)

80-80: Empty 상태를 화면 전체 기준으로 중앙 정렬하도록 바뀐 점 좋습니다

상위가 h-full을 갖는 컨테이너라는 전제에서, auto margin 대신 전체 높이 + justify-center를 쓰는 편이 뷰포트 높이 변화에도 더 안정적으로 동작할 것 같습니다.

apps/web/app/_components/CampusSelector/CampusSelector.tsx (1)

13-31: 캠퍼스 변경 시 맵 센터를 즉시 동기화하는 로직이 요구사항에 잘 맞습니다

isCampusType으로 키를 안전하게 좁힌 뒤 setLastMapCenter(CAMPUS_LOCATION[newKey])를 호출해서, 이후 맵 진입 시 선택한 캠퍼스 중심으로 바로 초기화될 수 있게 잘 연결돼 있습니다. setTimeout으로 캠퍼스 상태를 지연 설정하는 기존 패턴과도 충돌 없이 동작할 것으로 보입니다.

apps/web/app/map/MapComponent.tsx (1)

7-35: lastMapCenter 스토어 경로 변경으로 구현이 새 스토어와 잘 정합됩니다

useLastMapCenterStore를 새 파일(@/_store/lastMapCenter)에서 불러오되, lastMapCenter || CAMPUS_LOCATION[campus] 기본 센터 계산과 언마운트 시 setLastMapCenter 호출 로직은 그대로라 기존 동작을 그대로 유지하면서 스토어만 교체했습니다.

apps/web/app/_store/lastMapCenter.ts (1)

1-23: 지도의 마지막 중심 좌표를 위한 최소한의 Zustand 스토어 설계가 명확합니다

Coord | null을 허용하는 단일 상태와 setter만 노출해서 MapComponent/CampusSelector에서 읽고 쓰기 쉬운 구조입니다. 주석에 주요 사용 시나리오(이탈/복귀, 캠퍼스 변경 시 처리)가 잘 설명돼 있어 다른 개발자가 용도를 이해하기도 좋겠습니다.

@leeleeleeleejun leeleeleeleejun merged commit a5ab118 into develop Nov 24, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 카테고리 별 맛집 페이지에서 스와이프로 카테고리 전환

2 participants