Skip to content

[Fix] 매장을 여러 그룹에 저장시 다중마커로 띄워주기#117

Merged
KingSeokJin merged 4 commits intodevelopfrom
fix/scrap-bttn
Feb 27, 2026
Merged

[Fix] 매장을 여러 그룹에 저장시 다중마커로 띄워주기#117
KingSeokJin merged 4 commits intodevelopfrom
fix/scrap-bttn

Conversation

@KingSeokJin
Copy link
Collaborator

@KingSeokJin KingSeokJin commented Feb 24, 2026

📝 작업 내용

  • 콜키지 맵에서 띄울 수 있는 화면들에 대하여 하나의 매장이 여러 그룹에 저장된 상태일때 다중마커로 띄우도록 수정하였습니다.

📢 PR Point


이미지 첨부

image

🔧 다음 할 일

  • 지도 콜키지 on/off 모드에 따른 마커 변경

Summary by CodeRabbit

릴리스 노트

  • 새 기능
    • 북마크 아이콘이 저장된 그룹 수에 따라 동적으로 표시됩니다(그룹 없음→미저장, 1개→저장, 2개 이상→다중 저장).
    • 여러 화면의 북마크 버튼에 동적 아이콘과 상황에 맞는 대체 텍스트가 적용되어 북마크 상태를 더 명확히 확인할 수 있습니다.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

세 컴포넌트(DetailHeader, StoreCardInMultiPinList, StoreCardInSave)에서 북마크 아이콘 선택을 정적 isKeep 판정에서 새 getMarkerIcon 로직으로 변경하고 MultiSaveMarker SVG를 추가로 임포트하여 그룹 선택 수에 따라 아이콘을 결정하도록 했습니다.

Changes

Cohort / File(s) Summary
상세 헤더
src/shared/components/restaurant_detail/DetailHeader.tsx
getMarkerIcon 함수 추가 및 MultiSaveMarker 임포트. 기존 isKeep ? save : notsave 정적 렌더링을 getMarkerIcon() 호출과 동적 alt 텍스트로 교체.
멀티핀 리스트 스토어카드
src/shared/components/storecard/StoreCardInMultiPinList.tsx
MultiSaveMarker 임포트 및 내부 getMarkerIcon 헬퍼 추가. 저장 아이콘 소스와 alt 텍스트를 동적 결정 로직으로 대체. 기존 클릭/네비게이션 핸들러는 유지.
저장 리스트 스토어카드
src/shared/components/storecard/StoreCardInSave.tsx
MultiSaveMarker 임포트 및 getMarkerIcon 추가. 정적 아이콘을 동적 아이콘으로 교체. 컴포넌트 외부 동작 변경 없음.

Sequence Diagram(s)

(생성 기준 미충족 — 시각화 생략)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 분

Possibly related PRs

Suggested reviewers

  • TakioN

개요

세 개의 UI 컴포넌트(DetailHeader, StoreCardInMultiPinList, StoreCardInSave)에서 북마크 아이콘 선택 로직을 정적 상태에서 동적 로직으로 변경했습니다. 새로운 getMarkerIcon 함수를 추가하여 선택된 그룹 수에 따라 적절한 아이콘을 렌더링하며, MultiSaveMarker 자산을 새로 임포트했습니다.

변경사항

기능 그룹 / 파일 요약
북마크 아이콘 동적 렌더링
src/shared/components/restaurant_detail/DetailHeader.tsx, src/shared/components/storecard/StoreCardInMultiPinList.tsx, src/shared/components/storecard/StoreCardInSave.tsx
각 컴포넌트에 getMarkerIcon 헬퍼 함수 추가. 사용자 액션/선택 그룹 수에 따라 아이콘 결정(그룹 없음 → NotSave, 2개 이상 → MultiSaveMarker, 1개 → Save). 기존 정적 isKeep 기반 로직을 대체하고 MultiSaveMarker SVG 임포트 추가.

예상 코드 리뷰 소요 시간

🎯 2 (Simple) | ⏱️ ~15 분

관련 가능성 있는 PR

권장 리뷰어

  • TakioN

축하 시

🐰 내 말 좀 들어줘요—
그룹이 모이면 반짝이고, 아니면 조용해요.
save, notsave, 그리고 MultiSave의 춤,
당근 한 입에 코드도 웃네요! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 변경사항을 명확히 설명하고 있습니다. 매장이 여러 그룹에 저장될 때 다중마커를 표시하는 기능을 정확히 요약하고 있습니다.
Description check ✅ Passed PR 설명이 필수 섹션(작업 내용, PR Point, 이미지, 다음 할 일)의 구조를 따르고 있으며, 작업 내용과 다음 할 일이 명확히 작성되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/scrap-bttn

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.

Caution

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

⚠️ Outside diff range comments (3)
src/shared/components/storecard/StoreCardInMultiPinList.tsx (1)

95-124: ⚠️ Potential issue | 🟠 Major

초기 스크랩 상태가 아이콘에 반영되지 않을 수 있음

getMarkerIcon()selectedStores만 기준으로 판단해서, 해당 세션에서 조작 이력이 없는 매장은 scrap=true여도 NotSave가 표시될 수 있습니다. 초기 렌더에서 저장 상태가 뒤집히는 UX 버그로 보입니다. selectedStores에 항목이 없을 때는 isKeep(=scrap 반영)로 폴백해 주세요.

✅ 제안 수정
  const getMarkerIcon = () => {
    const groupIds = selectedStores[resId];

-   if (!groupIds || groupIds.length === 0) {
-     return NotSave;
-   }
+   if (!groupIds) {
+     return isKeep ? Save : NotSave;
+   }
+   if (groupIds.length === 0) {
+     return NotSave;
+   }
    if (groupIds.length >= 2) {
      return MultiSaveMarker;
    }
    return Save;
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/storecard/StoreCardInMultiPinList.tsx` around lines 95
- 124, getMarkerIcon currently decides the icon solely from
selectedStores[resId], causing items with no session selection to show NotSave
even when isKeep (scrap) is true; update getMarkerIcon to fall back to isKeep
when groupIds is undefined or empty: if no groupIds, return Save when isKeep is
true otherwise NotSave, keep the existing MultiSaveMarker logic for
groupIds.length >= 2, and ensure the component references resId, selectedStores
and isKeep as before.
src/shared/components/restaurant_detail/DetailHeader.tsx (1)

114-144: ⚠️ Potential issue | 🟠 Major

서버 스크랩 상태가 아이콘에 반영되지 않을 수 있음

getMarkerIcon()selectedStores만 보고 판단해, 아직 store에 반영되지 않은 isScrap=true 상태를 notsave로 렌더링할 수 있습니다. 초기 진입 시 아이콘이 반대로 보일 수 있어 폴백이 필요합니다.

✅ 제안 수정
  const getMarkerIcon = () => {
    const groupIds = selectedStores[resId];

-   if (!groupIds || groupIds.length === 0) {
-     return notsave;
-   }
+   if (!groupIds) {
+     return isKeep ? save : notsave;
+   }
+   if (groupIds.length === 0) {
+     return notsave;
+   }
    if (groupIds.length >= 2) {
      return MultiSaveMarker;
    }
    return save;
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/restaurant_detail/DetailHeader.tsx` around lines 114 -
144, getMarkerIcon currently decides the marker solely from
selectedStores[resId], which can miss a pending server scrap state (isScrap /
isKeep) and render the wrong icon; update getMarkerIcon to use
selectedStores[resId] if present but fall back to the local prop/state (isScrap
or isKeep) when groupIds is empty/undefined so the UI reflects the most recent
scrap state, and ensure the function still returns notsave, MultiSaveMarker, or
save as before (refer to getMarkerIcon, selectedStores, resId, isScrap/isKeep).
src/shared/components/storecard/StoreCardInSave.tsx (1)

46-103: ⚠️ Potential issue | 🟠 Major

초기 로딩 시 저장 아이콘이 잘못 표시될 수 있음

getMarkerIcon()selectedStores만 기준이라 초기 로딩에서 데이터가 아직 없으면 저장 리스트임에도 NotSave가 표시될 수 있습니다. selectedStores 미존재 시 isKeep 폴백으로 처리하는 것이 안전합니다.

✅ 제안 수정
  const getMarkerIcon = () => {
    const groupIds = selectedStores[restaurantId];

-   if (!groupIds || groupIds.length === 0) {
-     return NotSave;
-   }
+   if (!groupIds) {
+     return isKeep ? Save : NotSave;
+   }
+   if (groupIds.length === 0) {
+     return NotSave;
+   }
    if (groupIds.length >= 2) {
      return MultiSaveMarker;
    }
    return Save;
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/storecard/StoreCardInSave.tsx` around lines 46 - 103,
getMarkerIcon currently only checks selectedStores and returns NotSave when
selectedStores[restaurantId] is missing, causing wrong icon on initial load;
update getMarkerIcon to fall back to the isKeep state when selectedStores is
undefined or has no entry for restaurantId (i.e., if !groupIds use isKeep ? Save
: NotSave), keeping the existing multi-item and single-item checks
(groupIds.length >= 2 -> MultiSaveMarker, else Save) and ensure getMarkerIcon
reads the isKeep boolean so the initial render reflects the local saved state
until selectedStores populates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/shared/components/restaurant_detail/DetailHeader.tsx`:
- Around line 114-144: getMarkerIcon currently decides the marker solely from
selectedStores[resId], which can miss a pending server scrap state (isScrap /
isKeep) and render the wrong icon; update getMarkerIcon to use
selectedStores[resId] if present but fall back to the local prop/state (isScrap
or isKeep) when groupIds is empty/undefined so the UI reflects the most recent
scrap state, and ensure the function still returns notsave, MultiSaveMarker, or
save as before (refer to getMarkerIcon, selectedStores, resId, isScrap/isKeep).

In `@src/shared/components/storecard/StoreCardInMultiPinList.tsx`:
- Around line 95-124: getMarkerIcon currently decides the icon solely from
selectedStores[resId], causing items with no session selection to show NotSave
even when isKeep (scrap) is true; update getMarkerIcon to fall back to isKeep
when groupIds is undefined or empty: if no groupIds, return Save when isKeep is
true otherwise NotSave, keep the existing MultiSaveMarker logic for
groupIds.length >= 2, and ensure the component references resId, selectedStores
and isKeep as before.

In `@src/shared/components/storecard/StoreCardInSave.tsx`:
- Around line 46-103: getMarkerIcon currently only checks selectedStores and
returns NotSave when selectedStores[restaurantId] is missing, causing wrong icon
on initial load; update getMarkerIcon to fall back to the isKeep state when
selectedStores is undefined or has no entry for restaurantId (i.e., if !groupIds
use isKeep ? Save : NotSave), keeping the existing multi-item and single-item
checks (groupIds.length >= 2 -> MultiSaveMarker, else Save) and ensure
getMarkerIcon reads the isKeep boolean so the initial render reflects the local
saved state until selectedStores populates.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70ee2e7 and 54f8b1d.

⛔ Files ignored due to path filters (1)
  • src/shared/assets/common/multiSaveMarker.svg is excluded by !**/*.svg
📒 Files selected for processing (3)
  • src/shared/components/restaurant_detail/DetailHeader.tsx
  • src/shared/components/storecard/StoreCardInMultiPinList.tsx
  • src/shared/components/storecard/StoreCardInSave.tsx

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

🧹 Nitpick comments (1)
src/shared/components/restaurant_detail/DetailHeader.tsx (1)

114-131: getMarkerIcon은 공용 유틸/훅으로 추출을 권장합니다.

현재 동일 판정 로직이 카드/상세 컴포넌트에 반복되어 있어 이후 조건 변경 시 누락 위험이 큽니다. 공용화하면 유지보수성이 좋아집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/restaurant_detail/DetailHeader.tsx` around lines 114 -
131, The getMarkerIcon logic is duplicated across components; extract it into a
shared utility or custom hook (e.g., useMarkerIcon or getMarkerIcon in a shared
utils file) and replace local implementations in DetailHeader (getMarkerIcon)
and the Card component to call the single shared function; ensure the shared
function accepts the same inputs used here (resId, selectedStores, isKeep) and
returns the same marker values (notsave, save, MultiSaveMarker) so existing
callers keep behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/shared/components/restaurant_detail/DetailHeader.tsx`:
- Around line 114-131: getMarkerIcon currently decides image source from
selectedStores/resId while elsewhere the alt text relies on isKeep, causing a
one-frame mismatch; unify the decision by extracting a single canonical
"isSaved" (or markerState) computed from selectedStores, resId, and isKeep
(reusing the existing logic in getMarkerIcon) and then derive both the icon
(MultiSaveMarker/save/notsave) and alt text from that single value; update
getMarkerIcon (or replace it with getMarkerState) to return a simple
enum/boolean and use that to pick the image and alt so src and alt are always
computed from the same criteria (referencing getMarkerIcon, selectedStores,
resId, isKeep, MultiSaveMarker, save, notsave).

In `@src/shared/components/storecard/StoreCardInMultiPinList.tsx`:
- Around line 95-112: getMarkerIcon() computes the icon to render but the image
alt text is using isKeep directly, causing a brief mismatch between src and alt
during state transitions; update the component to derive both the img src and
alt from the same computed value by calling getMarkerIcon() (or storing its
result in a local variable) and use that value for the image source and the alt
string, ensuring consistency for identifiers like getMarkerIcon, resId,
selectedStores, isKeep, NotSave, Save, and MultiSaveMarker.

---

Nitpick comments:
In `@src/shared/components/restaurant_detail/DetailHeader.tsx`:
- Around line 114-131: The getMarkerIcon logic is duplicated across components;
extract it into a shared utility or custom hook (e.g., useMarkerIcon or
getMarkerIcon in a shared utils file) and replace local implementations in
DetailHeader (getMarkerIcon) and the Card component to call the single shared
function; ensure the shared function accepts the same inputs used here (resId,
selectedStores, isKeep) and returns the same marker values (notsave, save,
MultiSaveMarker) so existing callers keep behavior unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54f8b1d and 1dc179a.

📒 Files selected for processing (3)
  • src/shared/components/restaurant_detail/DetailHeader.tsx
  • src/shared/components/storecard/StoreCardInMultiPinList.tsx
  • src/shared/components/storecard/StoreCardInSave.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/shared/components/storecard/StoreCardInSave.tsx

Comment on lines +114 to +131
const getMarkerIcon = () => {
const hasUserAction = resId in selectedStores;
const groupIds = selectedStores[resId];

// 사용자가 조작을 한 기록이 있다면, 스토어의 데이터만 보고 판단합니다.
if (hasUserAction) {
if (!groupIds || groupIds.length === 0) {
return notsave; // 모든 그룹 해제 시 확실하게 notsave 반환
}
if (groupIds.length >= 2) {
return MultiSaveMarker;
}
return save;
}

// 2. 조작 기록이 전혀 없는 초기 상태일 때만 부모의 scrap(isKeep) 상태를 따릅니다.
return isKeep ? save : notsave;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

아이콘 소스와 alt 판정 기준이 달라 접근성 상태가 일시적으로 어긋날 수 있습니다.

Line 148은 getMarkerIcon()(스토어 직접 기준), Line 149는 isKeep(상태 동기화 기준)를 사용해서, 상태 전환 직후 한 프레임 동안 srcalt가 불일치할 수 있습니다. 같은 기준값으로 계산하는 게 안전합니다.

수정 예시
+  const markerIcon = getMarkerIcon();
+  const isSavedIcon = markerIcon !== notsave;
+
   return (
     <div className="relative flex w-full flex-col">
@@
               <img
-                src={getMarkerIcon()}
-                alt={isKeep ? 'saved' : 'unsaved'}
+                src={markerIcon}
+                alt={isSavedIcon ? 'saved' : 'unsaved'}
                 className="h-[32px] w-[32px]"
               />

Also applies to: 147-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/restaurant_detail/DetailHeader.tsx` around lines 114 -
131, getMarkerIcon currently decides image source from selectedStores/resId
while elsewhere the alt text relies on isKeep, causing a one-frame mismatch;
unify the decision by extracting a single canonical "isSaved" (or markerState)
computed from selectedStores, resId, and isKeep (reusing the existing logic in
getMarkerIcon) and then derive both the icon (MultiSaveMarker/save/notsave) and
alt text from that single value; update getMarkerIcon (or replace it with
getMarkerState) to return a simple enum/boolean and use that to pick the image
and alt so src and alt are always computed from the same criteria (referencing
getMarkerIcon, selectedStores, resId, isKeep, MultiSaveMarker, save, notsave).

Comment on lines +95 to +112
const getMarkerIcon = () => {
const hasUserAction = resId in selectedStores;
const groupIds = selectedStores[resId];

// 사용자가 조작을 한 기록이 있다면, 스토어의 데이터만 보고 판단합니다.
if (hasUserAction) {
if (!groupIds || groupIds.length === 0) {
return NotSave; // 모든 그룹 해제 시 확실하게 NotSave 반환
}
if (groupIds.length >= 2) {
return MultiSaveMarker;
}
return Save;
}

// 2. 조작 기록이 전혀 없는 초기 상태일 때만 부모의 scrap(isKeep) 상태를 따릅니다.
return isKeep ? Save : NotSave;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

img srcalt가 서로 다른 상태 소스를 사용합니다.

Line 128은 getMarkerIcon() 결과, Line 129는 isKeep를 사용해 전환 시점에 접근성 텍스트가 잠시 틀릴 수 있습니다. alt도 아이콘 계산 결과와 동일 기준으로 맞춰 주세요.

수정 예시
+  const markerIcon = getMarkerIcon();
+  const isSavedIcon = markerIcon !== NotSave;
@@
             <img
-              src={getMarkerIcon()}
-              alt={isKeep ? 'saved' : 'unsaved'}
+              src={markerIcon}
+              alt={isSavedIcon ? 'saved' : 'unsaved'}
               className="h-[25px] w-[25px]"
             />

Also applies to: 127-130

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/storecard/StoreCardInMultiPinList.tsx` around lines 95
- 112, getMarkerIcon() computes the icon to render but the image alt text is
using isKeep directly, causing a brief mismatch between src and alt during state
transitions; update the component to derive both the img src and alt from the
same computed value by calling getMarkerIcon() (or storing its result in a local
variable) and use that value for the image source and the alt string, ensuring
consistency for identifiers like getMarkerIcon, resId, selectedStores, isKeep,
NotSave, Save, and MultiSaveMarker.

Copy link
Member

@TakioN TakioN left a comment

Choose a reason for hiding this comment

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

승인이 늦었네요 ㅠ 고생하셨습니다!

@KingSeokJin KingSeokJin merged commit 3562f89 into develop Feb 27, 2026
3 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.

2 participants