Skip to content

feat(map): add overlapping feed marker stack#11

Merged
seoJing merged 4 commits into
mainfrom
feature/spot-api-contract-sync
May 29, 2026
Merged

feat(map): add overlapping feed marker stack#11
seoJing merged 4 commits into
mainfrom
feature/spot-api-contract-sync

Conversation

@seoJing
Copy link
Copy Markdown
Member

@seoJing seoJing commented May 29, 2026

Summary

  • Force SPOT into temporary light-only mode and hide the My Page theme selector
  • Group overlapping/nearby map feed markers and open a stacked feed card deck for grouped feeds
  • Share card-deck motion tokens across map/feed overlays and refine tutorial gesture guidance

Checks

  • pnpm lint (passed, existing warnings only)
  • pnpm test (35 files / 148 tests passed)
  • pnpm build (passed)
  • Notjing final gate (passed)

Notjing Final Gate

  • Blocking issues: none
  • Security concerns: none
  • Scope issues: none
  • Notes: bookmark action still uses existing temporary console stub until favorite mutation is wired; feed ID routing follows existing behavior.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 근처의 피드 항목들을 자동으로 그룹화하여 지도에 클러스터로 표시합니다.
    • 카드 스택 덱 UI를 추가하여 여러 항목을 겹쳐서 표시합니다.
    • 좌/우 스와이프 제스처로 카드 상세 정보에 접근합니다.
  • UI/Style

    • 앱 테마를 라이트 모드로 통일했습니다.
    • 지도 마커의 시각화 방식을 개선했습니다.
    • 튜토리얼 인터페이스를 업데이트했습니다.
    • 마이페이지에서 테마 설정 옵션을 제거했습니다.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontend Ready Ready Preview, Comment May 29, 2026 8:09am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Warning

Review limit reached

@seoJing, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a4ab37aa-2c8e-4687-8219-375ada8e115f

📥 Commits

Reviewing files that changed from the base of the PR and between b1ed422 and 3d1ec8b.

📒 Files selected for processing (6)
  • src/features/feed/model/feed-marker-group.test.ts
  • src/features/feed/model/feed-marker-group.ts
  • src/features/map/client/MapClient.tsx
  • src/features/map/model/feed-stack-gesture.test.ts
  • src/features/map/ui/ClusterBlob.tsx
  • src/features/map/ui/MapCardDeckOverlay.tsx
📝 Walkthrough

Walkthrough

PR은 지도에서 근접 피드를 그룹으로 묶어 덱 기반 오버레이로 표시하는 기능을 추가합니다. 하버사인 기반 거리 계산으로 피드를 클러스터링하고, 제스처 인식 및 애니메이션 상수화를 통해 카드 덱 UI를 개선하며, 마커 시각화와 스타일을 feed-group 변형으로 확장합니다.

Changes

피드 마커 그룹화 및 덱 기반 카드 시스템

Layer / File(s) Summary
피드 마커 그룹 모델 및 테스트
src/features/feed/model/feed-marker-group.ts, src/features/feed/model/feed-marker-group.test.ts
하버사인 공식으로 좌표 거리를 계산하고, 임계값(기본 18m) 내 근접 마커를 하나의 그룹으로 묶으며, 그룹 ID와 중심 좌표를 생성합니다. 동일/근접 좌표 병합, 임계값 범위 내 클러스터링, 유효하지 않은 좌표 제외를 검증하는 테스트 포함.
카드 덱 제스처 및 애니메이션 모델
src/features/feed/model/card-deck-animation.ts, src/features/map/model/feed-stack-gesture.ts, src/features/map/model/feed-stack-gesture.test.ts
제스처 입력(dx, dy, velocityX)에서 스택 상태 액션(center, next, detail-left, detail-right)을 결정하는 resolveFeedStackGesture 유틸과, 카드 덱 애니메이션 상수 맵(MAP_FEED_CARD_DECK_ANIMATION) 정의. 축 우세 및 임계값 기반 제스처 분류를 검증하는 테스트 포함.
핫스팟과 피드 마커 겹침 필터링
src/features/map/model/hotspot-feed-overlap.ts, src/features/map/model/hotspot-feed-overlap.test.ts
Discovery 핫스팟 클러스터를 피드 마커 그룹과의 거리 비교로 필터링하여, 겹치는 호스팟을 제거합니다. variant별(discovery/mine/ai-feed) 필터링 로직과 레거시 처리를 테스트로 검증.
카드 덱 오버레이 컴포넌트
src/features/map/ui/MapCardDeckOverlay.tsx
고정 크기(3개) 덱을 렌더링하며, 드래그/제스처로 탑 카드를 전환하고, exit override로 방향별 애니메이션 제어합니다. 클릭 캡처로 컨텐츠 클릭 차단, 상단 포인터 다운으로 덱 닫기 기능 포함.
MapClient 피드 마커 그룹 통합
src/features/map/client/MapClient.tsx
feedMarkerGroups 메모이제이션, filteredClusters 중간 단계 추가, 호스팟 필터링 적용으로 최종 클러스터 구성. 뷰포트 마커 카운트를 그룹 기반으로 재계산하고, 그룹 선택 핸들러(handleFeedMarkerGroupSelect) 추가. MapFeedStackCard 렌더링 분기 추가.
피드 정보 및 스택 카드 컴포넌트
src/features/map/ui/MapFeedInfoCard.tsx, src/features/map/ui/MapFeedStackCard.tsx
MapFeedInfoCard를 덱 오버레이 기반으로 리팩토링하여 북마크 콜백(onBookmarkAction) 지원. MapFeedStackCard는 그룹 내 여러 카드를 덱으로 표시하며, 각 카드에 북마크 버튼 추가.
마커 시각 모드 및 타입 확장
src/features/map/model/marker-visual-mode.ts, src/features/map/model/marker-visual-mode.test.ts, src/features/map/model/types.ts
viewportReady 파라미터를 추가하여 초기 렌더(viewport 미준비)에서 마커 모드를 full로 유지. ActivityCluster variant에 feed-group 추가. 테스트로 초기 렌더 동작 검증.
ClusterBlob feed-group 스타일 및 애니메이션
src/features/map/ui/ClusterBlob.tsx, src/features/map/ui/ClusterBlob.test.tsx
feed-group 변형에 보라 계열 색상, simple 모드에서 그라디언트 데코 렌더링. full 모드에서 코어/위성 원의 커스텀 SVG 키프레임과 CSS 변수 기반 드리프트 애니메이션 추가. 카테고리 라벨 숨김. 애니메이션 계약 테스트 포함.
MapFeedCardPager 애니메이션 상수화 및 제스처 통합
src/features/feed/ui/MapFeedCardPager.tsx
카드 덱 애니메이션 파라미터를 MAP_FEED_CARD_DECK_ANIMATION 상수로 일괄 치환하여 하드코딩 값 제거. onTutorialCardDetail 콜백 추가, 좌/우 스와이프로 디테일 진입 방향 전달(openDetailTopPromoted(dir)). 상단 안내 UI 제거.
SpotInfoCard 오버레이 리팩토링 및 튜토리얼 UI 업데이트
src/features/map/ui/SpotInfoCard.tsx, src/features/map/ui/MapTutorialOverlay.tsx
SpotInfoCard를 motion.div에서 MapCardDeckOverlay로 전환. MapTutorialOverlay의 스팟라이트 영역과 제스처 큐를 클래스 기반에서 스타일 기반(spotlightScopeStyleByStep, gestureCueStyleByStep)으로 재구성. GestureCue 컴포넌트 추가로 방향별 애니메이션 정의. 문구 및 색상 업데이트.
애니메이션 전환 및 테마 정리
src/features/map/ui/MapBottomStack.tsx, src/app/providers/theme-provider.tsx, src/features/my/client/MyPageClient.tsx
MapBottomStack에 mode="wait" 추가로 진입/퇴장 애니메이션 대기 처리. ThemeProvider에서 시스템 테마 제거하고 강제 라이트 테마(forcedTheme="light") 적용. MyPageClient 테마 UI 제거.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • spot-platform/frontend#2: MapFeedCardPager의 카드 덱 exit 처리 및 stagger/타이밍 로직을 함께 수정하여 코드 수준으로 직접 겹칩니다.
🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title clearly summarizes the main feature: adding stacked overlapping feed marker functionality on the map.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/spot-api-contract-sync

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
Copy Markdown

@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: 2

🧹 Nitpick comments (4)
src/features/map/ui/ClusterBlob.tsx (1)

378-398: ⚖️ Poor tradeoff

전역 keyframe 이름 중복 정의

spot-cluster-core-breathe, spot-feed-group-core-shift 등의 keyframe이 각 ClusterBlob 인스턴스마다 <style> 태그로 재정의됩니다. 기능적으로는 문제없지만, 마커가 많을 때 DOM에 중복 스타일이 누적됩니다.

성능에 민감한 경우 keyframe을 전역 CSS 파일로 추출하거나 useInsertionEffect로 한 번만 삽입하는 방식을 고려할 수 있습니다.

🤖 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 `@src/features/map/ui/ClusterBlob.tsx` around lines 378 - 398, The inline
<style> block in the ClusterBlob component is redefining keyframes
(spot-cluster-core-breathe, spot-cluster-discovery-breathe,
spot-feed-group-core-shift, spot-cluster-satellite-drift) for every instance,
causing redundant DOM styles; fix it by extracting these keyframes to a single
global CSS file or inject them once per app lifecycle (e.g., add a one-time
insertion using useInsertionEffect or a module-level flag inside ClusterBlob) so
the keyframes are defined only once and remove the per-instance <style> block.
src/features/feed/model/feed-marker-group.ts (1)

65-83: 💤 Low value

Single-linkage 클러스터링으로 인한 체이닝 효과 가능성

현재 알고리즘은 그룹 내 어떤 좌표라도 임계값 내에 있으면 해당 그룹에 합류합니다. 이로 인해 A↔B(10m), B↔C(10m)인 경우 A와 C가 20m 떨어져 있어도 같은 그룹이 됩니다.

18m 임계값에서는 실제로 큰 문제가 되지 않을 수 있지만, 피드가 밀집된 지역에서 예상보다 큰 그룹이 생성될 수 있습니다. 의도된 동작인지 확인이 필요합니다.

🤖 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 `@src/features/feed/model/feed-marker-group.ts` around lines 65 - 83, The
current single-linkage logic in the grouping loop (using resolveCoord,
getDistanceMeters and thresholdMeters) causes chaining (A–B and B–C within
threshold but A–C may exceed it); change the membership test so a new coord is
compared to the group's representative instead of any member to avoid
chaining—e.g., maintain and update a group's centroid (or representative coord)
on groups entries and use getDistanceMeters(representative, coord) <=
thresholdMeters when deciding membership, then update the representative when
adding the new coord; ensure the representative is initialized when pushing a
new group and recalculated (average or weighted) after pushing.
src/features/map/client/MapClient.tsx (1)

187-191: 💤 Low value

덱 열림 상태에서 클러스터 변경 시 pager 상태 리셋 누락 가능성

isMapMarkerDeckOpen만 의존성에 있어서 덱이 이미 열린 상태(true)에서 다른 클러스터를 선택해도 이 effect가 재실행되지 않습니다.

현재 UX상 덱 간 전환 시 pager 상태 유지가 의도된 것이라면 문제없지만, 매 클러스터 선택마다 리셋이 필요하다면 selectedClusterId도 의존성에 추가해야 합니다.

🤖 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 `@src/features/map/client/MapClient.tsx` around lines 187 - 191, The effect
that resets pager state (useEffect containing setPagerPromotedCount and
setPagerSnap) only depends on isMapMarkerDeckOpen, so when the deck is already
open and selected cluster changes the effect won’t rerun; if you want to reset
pager on every cluster selection as well, include selectedClusterId in the
dependency array (i.e., change the hook to depend on [isMapMarkerDeckOpen,
selectedClusterId]) so that setPagerPromotedCount(0) and setPagerSnap('peek')
run whenever the open deck and selectedClusterId change.
src/features/map/model/feed-stack-gesture.test.ts (1)

14-46: ⚡ Quick win

속도 임계값 분기도 테스트로 고정해두는 편이 좋습니다.

지금 스위트는 거리 기반 좌우 스와이프만 검증해서, absVx >= SWIPE_SIDE_VELOCITY_THRESHOLD 분기가 깨져도 놓칩니다. 거리 부족 + 고속 스와이프 케이스 하나를 추가해 두면 회귀를 막기 쉽습니다.

🤖 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 `@src/features/map/model/feed-stack-gesture.test.ts` around lines 14 - 46, Add
tests covering the velocity branch of resolveFeedStackGesture: the suite
currently only checks distance-based side swipes and can miss the absVx >=
SWIPE_SIDE_VELOCITY_THRESHOLD path. Add at least one short-distance,
high-velocity case (simulate dx small, dy small, and set vx such that absVx >=
SWIPE_SIDE_VELOCITY_THRESHOLD) for both left and right to assert 'detail-left'
and 'detail-right' respectively; reference resolveFeedStackGesture, absVx and
SWIPE_SIDE_VELOCITY_THRESHOLD to locate the logic to protect against
regressions.
🤖 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 `@src/features/map/ui/ClusterBlob.tsx`:
- Around line 542-554: The CSS animation duration is hardcoded to 2.8s while the
framer-motion transition uses isFeedGroup ? 3.2 : 2.8, causing mismatch; in the
ClusterBlob component compute a single animationDuration variable (e.g., const
animationDuration = isFeedGroup ? 3.2 : 2.8) and use that variable for both the
framer-motion transition.duration and the inline CSS animation string (replace
the hardcoded 2.8 in the template `${2.8 + i * 0.22}s ...` with
`${animationDuration + i * 0.22}s ...`) so both animations run at the same base
speed.

In `@src/features/map/ui/MapCardDeckOverlay.tsx`:
- Around line 72-79: closeDeck and dismissTop should not call setTimeout to
guess when exit animations finish; instead they should mark the "closing" intent
and let AnimatePresence's onExitComplete execute the parent close. Change
closeDeck/dismissTop to only call setExitOverride(...) and
setIsClosingDeck(true) (preserve prefersReducedMotion path by calling
onCloseAction immediately if prefersReducedMotion is true), and remove the
window.setTimeout(onCloseAction, ...) logic; then update onExitComplete to check
the isClosingDeck flag and, when true, call onCloseAction and clear closing
state (setIsClosingDeck(false) and setExitOverride(null)) so the actual close
happens only after the exit animation completes. Ensure you reference the same
symbols: closeDeck, dismissTop, setExitOverride, setIsClosingDeck,
onExitComplete, onCloseAction, prefersReducedMotion, visibleItems, and
DECK_ANIMATION.

---

Nitpick comments:
In `@src/features/feed/model/feed-marker-group.ts`:
- Around line 65-83: The current single-linkage logic in the grouping loop
(using resolveCoord, getDistanceMeters and thresholdMeters) causes chaining (A–B
and B–C within threshold but A–C may exceed it); change the membership test so a
new coord is compared to the group's representative instead of any member to
avoid chaining—e.g., maintain and update a group's centroid (or representative
coord) on groups entries and use getDistanceMeters(representative, coord) <=
thresholdMeters when deciding membership, then update the representative when
adding the new coord; ensure the representative is initialized when pushing a
new group and recalculated (average or weighted) after pushing.

In `@src/features/map/client/MapClient.tsx`:
- Around line 187-191: The effect that resets pager state (useEffect containing
setPagerPromotedCount and setPagerSnap) only depends on isMapMarkerDeckOpen, so
when the deck is already open and selected cluster changes the effect won’t
rerun; if you want to reset pager on every cluster selection as well, include
selectedClusterId in the dependency array (i.e., change the hook to depend on
[isMapMarkerDeckOpen, selectedClusterId]) so that setPagerPromotedCount(0) and
setPagerSnap('peek') run whenever the open deck and selectedClusterId change.

In `@src/features/map/model/feed-stack-gesture.test.ts`:
- Around line 14-46: Add tests covering the velocity branch of
resolveFeedStackGesture: the suite currently only checks distance-based side
swipes and can miss the absVx >= SWIPE_SIDE_VELOCITY_THRESHOLD path. Add at
least one short-distance, high-velocity case (simulate dx small, dy small, and
set vx such that absVx >= SWIPE_SIDE_VELOCITY_THRESHOLD) for both left and right
to assert 'detail-left' and 'detail-right' respectively; reference
resolveFeedStackGesture, absVx and SWIPE_SIDE_VELOCITY_THRESHOLD to locate the
logic to protect against regressions.

In `@src/features/map/ui/ClusterBlob.tsx`:
- Around line 378-398: The inline <style> block in the ClusterBlob component is
redefining keyframes (spot-cluster-core-breathe, spot-cluster-discovery-breathe,
spot-feed-group-core-shift, spot-cluster-satellite-drift) for every instance,
causing redundant DOM styles; fix it by extracting these keyframes to a single
global CSS file or inject them once per app lifecycle (e.g., add a one-time
insertion using useInsertionEffect or a module-level flag inside ClusterBlob) so
the keyframes are defined only once and remove the per-instance <style> block.
🪄 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: CHILL

Plan: Pro

Run ID: 4263806c-121c-4fe4-ae43-4033a019cf8e

📥 Commits

Reviewing files that changed from the base of the PR and between 9f0bbe6 and b1ed422.

📒 Files selected for processing (22)
  • src/app/providers/theme-provider.tsx
  • src/features/feed/model/card-deck-animation.ts
  • src/features/feed/model/feed-marker-group.test.ts
  • src/features/feed/model/feed-marker-group.ts
  • src/features/feed/ui/MapFeedCardPager.tsx
  • src/features/map/client/MapClient.tsx
  • src/features/map/model/feed-stack-gesture.test.ts
  • src/features/map/model/feed-stack-gesture.ts
  • src/features/map/model/hotspot-feed-overlap.test.ts
  • src/features/map/model/hotspot-feed-overlap.ts
  • src/features/map/model/marker-visual-mode.test.ts
  • src/features/map/model/marker-visual-mode.ts
  • src/features/map/model/types.ts
  • src/features/map/ui/ClusterBlob.test.tsx
  • src/features/map/ui/ClusterBlob.tsx
  • src/features/map/ui/MapBottomStack.tsx
  • src/features/map/ui/MapCardDeckOverlay.tsx
  • src/features/map/ui/MapFeedInfoCard.tsx
  • src/features/map/ui/MapFeedStackCard.tsx
  • src/features/map/ui/MapTutorialOverlay.tsx
  • src/features/map/ui/SpotInfoCard.tsx
  • src/features/my/client/MyPageClient.tsx
💤 Files with no reviewable changes (1)
  • src/features/my/client/MyPageClient.tsx

Comment thread src/features/map/ui/ClusterBlob.tsx
Comment thread src/features/map/ui/MapCardDeckOverlay.tsx Outdated
@seoJing seoJing merged commit c3c8827 into main May 29, 2026
5 checks 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.

1 participant