Skip to content

Conversation

@H0ngJu
Copy link
Contributor

@H0ngJu H0ngJu commented Jan 28, 2026

🔗 1. 연관 이슈

🛠 2. 작업 내용

  • 메인 지도 화면 구현 및 바텀시트 제작
2026-01-28.3.09.45.1.mp4

📌 3. 참고 사항

  • 현재 mocking 된 값을 사용하도록 하였습니다.

👀 4. 리뷰 중점 사항

🧪 5. 테스트

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 맵 페이지 추가 - 위치 기반 사진 검색 및 표시
    • 지도에 사진 핀 표시 - 앨범별로 필터링 가능
    • 드래그 가능한 하단 시트 UI - 앨범 목록 및 상세 정보 표시
    • 앨범 검색 기능 추가
    • 현재 위치 자동 감지
  • UI 개선

    • 반응형 앨범 그리드 레이아웃 개선
    • 메뉴 위치 지정 옵션 추가
    • 회전 가능한 앨범 행 스크롤

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

H0ngJu added 30 commits January 24, 2026 15:13
@H0ngJu H0ngJu self-assigned this Jan 28, 2026
@H0ngJu H0ngJu requested a review from salmonco as a code owner January 28, 2026 06:13
@netlify
Copy link

netlify bot commented Jan 28, 2026

Deploy Preview for timely-pudding-5af14b ready!

Name Link
🔨 Latest commit acdd243
🔍 Latest deploy log https://app.netlify.com/projects/timely-pudding-5af14b/deploys/697b456b8a33f7000898c647
😎 Deploy Preview https://deploy-preview-47--timely-pudding-5af14b.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

이 PR은 지도 기반의 앨범 탐색 기능을 추가합니다. Mapbox 통합, 드래그 가능한 하단 시트, 앨범 그리드/상세 뷰, 관련 쿼리 훅 및 모의 API 핸들러를 도입하며, 기존 컴포넌트 스타일링을 조정합니다.

Changes

Cohort / File(s) Summary
지도 페이지 및 뷰
apps/web/src/app/map/page.tsx, apps/web/src/app/map/page.styles.ts, apps/web/src/app/map/constants.ts, apps/web/src/components/map/MapView.tsx, apps/web/src/components/map/MapView.styels.ts
지도 페이지와 MapView 컴포넌트를 추가하여 현재 위치 조회, 줌/위치 상태 관리, 선택된 앨범별 핀 필터링을 구현합니다. DEFAULT_LOCATION과 DEFAULT_ZOOM 상수를 정의합니다.
하단 시트 시스템
apps/web/src/components/bottomSheet/BottomSheet.tsx, apps/web/src/components/bottomSheet/BottomSheet.styles.ts, apps/web/src/components/bottomSheet/_hooks/useBottomSheetController.ts, apps/web/src/components/bottomSheet/constants.ts, apps/web/src/components/bottomSheet/bottomSheetContent/BottomSheetContent.tsx
드래그 가능한 하단 시트 UI를 추가하고 높이 상태 관리, 컨텍스트 기반 렌더링(HOME/ALBUM_LIST/ALBUM_DETAIL), SheetContext 타입 정의를 구현합니다.
앨범 뷰 컴포넌트
apps/web/src/components/bottomSheet/albumDetail/AlbumDetail.tsx, apps/web/src/components/bottomSheet/albumGrid/AlbumGrid.tsx, apps/web/src/components/bottomSheet/albumGrid/AlbumGrid.styles.ts, apps/web/src/components/bottomSheet/albumRow/AlbumRow.tsx, apps/web/src/components/bottomSheet/albumRow/AlbumRow.styles.ts
앨범 목록(그리드/행) 및 상세 뷰 컴포넌트를 추가하고, 검색 기능, 앨범 선택, 가로 스크롤 등을 포함합니다.
이미지 핀 컴포넌트
apps/web/src/components/image/ImagePin.tsx, apps/web/src/components/image/ImagePin.styles.ts, apps/web/src/components/image/ImagePin.stories.ts
지도에서 사용할 이미지 핀 컴포넌트와 관련 스타일, 스토리북 문서를 추가합니다.
쿼리 훅
apps/web/src/hooks/queries/useAlbumPhotos.ts, apps/web/src/hooks/queries/useMapPhotos.ts, apps/web/src/hooks/queries/useSelectableAlbums.ts, apps/web/src/hooks/queries/index.ts
앨범 사진, 지도 사진, 선택 가능한 앨범을 조회하는 React Query 훅을 추가합니다.
타입 정의
apps/web/src/types/album.type.ts, apps/web/src/types/map.type.ts
Album, AlbumDetailData, AlbumDetailPhoto, LocationState, MapPin 타입을 정의합니다.
모의 핸들러 및 데이터
apps/web/src/mocks/handlers/albums/*, apps/web/src/mocks/handlers/map/*, apps/web/src/mocks/handlers/photos/getPhotos/*, apps/web/src/mocks/handlers/index.ts
앨범, 지도 사진, 일반 사진 조회에 대한 MSW 핸들러 및 모의 데이터를 추가합니다.
API 클라이언트 업데이트
packages/api-client/src/generated.ts, packages/api-client/src/model/*
사진 상세, 위치 정보, 앨범 지도 정보, 장소 검색 쿼리 함수와 타입을 추가하고 로그인 응답 타입을 변경합니다.
기존 컴포넌트 개선
apps/web/src/components/album-container/AlbumContainer.styles.ts, apps/web/src/components/album-container/AlbumContainer.tsx, apps/web/src/components/album-grid-container/AlbumGridContainer.styles.ts, apps/web/src/components/buttons/menuButton/MenuButton.tsx, apps/web/src/components/buttons/circleButton/CircleButton.styles.ts, apps/web/src/components/input/Input.tsx
반응형 크기 조정, 타입 안정성(PhotoWrapper 제네릭 추가), MenuButton 배치 옵션, 번들 필터 렌더링 조건을 개선합니다.
의존성 및 유틸
apps/web/package.json, apps/web/src/utils/getCurrentPosition.ts, apps/web/src/theme/effects.ts
mapbox-gl, react-map-gl 의존성 추가, 지리 위치 조회 유틸리티 추가, 백드롭 블러 효과 확장(40px 추가).
포맷팅 변경
apps/web/src/app/photo/_contexts/PhotoContext.tsx, apps/web/src/app/photo/add/_hooks/usePhotoSelect.ts, apps/web/src/app/photo/add/_utils/*, apps/web/src/components/album-grid-container/AlbumGridContainer.stories.tsx, apps/web/src/components/header/photoAdd/PhotoAddHeader.stories.tsx, apps/web/src/components/textarea/Textarea.tsx
다양한 파일의 import 문, 함수 시그니처, 스토리 정의에 대한 일관된 포맷팅 개선(후행 쉼표, 줄바꿈).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MapPage
    participant MapView
    participant BottomSheet
    participant QueryHooks
    participant API

    User->>MapPage: 지도 페이지 방문
    MapPage->>MapPage: 현재 위치 조회
    MapPage->>QueryHooks: useSelectableAlbums() 호출
    QueryHooks->>API: GET /albums/selectable
    API-->>QueryHooks: 앨범 목록 반환
    QueryHooks-->>MapPage: albumList 반환
    MapPage->>BottomSheet: context(HOME) 전달
    BottomSheet-->>MapPage: AlbumRow 렌더링
    
    User->>BottomSheet: 앨범 선택
    BottomSheet->>MapPage: onSelectAlbum(albumId) 콜백
    MapPage->>MapPage: context를 ALBUM_DETAIL로 변경
    MapPage->>QueryHooks: useAlbumPhotos(albumId) 호출
    QueryHooks->>API: GET /photos?albumId=X
    API-->>QueryHooks: 앨범 상세 반환
    QueryHooks-->>MapPage: albumDetail 반환
    MapPage->>QueryHooks: useMapPhotos({zoom, bbox, albumId}) 호출
    QueryHooks->>API: GET /map/photos?zoom=Z&bbox=...&albumId=X
    API-->>QueryHooks: 지도 핀 반환
    QueryHooks-->>MapView: mapPins 반환
    MapView-->>User: 지도에 핀 표시
    BottomSheet-->>User: 앨범 상세 뷰 표시

    User->>MapView: 맵 드래그 (줌/위치 변경)
    MapView->>MapPage: viewState 업데이트
    MapPage->>QueryHooks: useMapPhotos ({zoom, bbox, ...}) 재호출
    QueryHooks->>API: GET /map/photos (업데이트된 파라미터)
    API-->>QueryHooks: 새로운 핀 반환
    QueryHooks-->>MapView: mapPins 업데이트
    MapView-->>User: 지도 리렌더링

    User->>BottomSheet: 하단 시트 드래그
    BottomSheet->>BottomSheet: height 상태 업데이트
    BottomSheet->>BottomSheet: snapHeightOnly() 호출
    BottomSheet-->>User: 높이 스냅
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: add album modal #43: 선택 가능한 앨범 모의 핸들러와 앨범 관련 타입/컴포넌트를 추가하여 이 PR의 앨범 선택 기능과 직접 연관됩니다.
  • feat: add photo view #46: 동일한 모의 파일과 앨범/사진 관련 훅/컴포넌트를 수정하므로 코드 레벨에서 변경사항이 겹칩니다.

Poem

🐰 지도 위에 꼬아박힌 핀들,
앨범들이 반짝이네요!
드래그하고 줌하고, 하단 시트 올려
사진 여행을 떠나세요! 📍✨🗺️

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 연결된 이슈 #25는 구체적인 요구사항이 기재되지 않았으나, PR 변경사항들이 지도 화면 구현과 관련된 기능들을 포함하고 있습니다. 이슈 #25에 구체적인 요구사항(As-Is, To-Be, Task 등)을 명시하여 PR과의 요구사항 일치도를 명확히 해주시기 바랍니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항인 지도 화면과 바텀시트 컴포넌트 추가를 명확하게 반영하고 있습니다.
Out of Scope Changes check ✅ Passed PR의 변경사항들이 지도 화면과 바텀시트 기능 추가, 관련 모의 데이터 및 스타일링으로 범위 내에 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @H0ngJu, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 애플리케이션에 메인 지도 화면과 상호작용 가능한 바텀시트 컴포넌트를 도입합니다. 사용자는 지도를 통해 사진 앨범을 시각화하고, 지도 핀과 상호작용하며, 드래그 가능한 바텀시트를 통해 앨범 상세 정보를 탐색할 수 있습니다. 초기 개발을 위해 목업 데이터를 활용했으며, 새로운 지도 및 앨범 관련 API 엔드포인트와의 통합을 위한 기반을 마련했습니다.

Highlights

  • 메인 지도 화면 구현: 사용자의 현재 위치를 기반으로 지도를 초기화하고, 앨범에 포함된 사진들을 지도 위에 핀으로 표시하는 기능을 구현했습니다.
  • 바텀시트 컴포넌트 개발: 지도와 연동되는 드래그 가능한 바텀시트 컴포넌트를 추가했습니다. 이 바텀시트는 홈, 앨범 목록, 앨범 상세 보기 등 다양한 상태를 가집니다.
  • 지도 핀 및 앨범 목업 데이터 추가: 지도에 표시될 사진 핀과 바텀시트에 사용될 앨범 목록 및 상세 정보를 위한 목업 데이터를 생성하여 초기 개발을 용이하게 했습니다.
  • API 클라이언트 업데이트: 사진 상세 정보 조회, 장소 검색, 위치 정보 조회, 앨범 지도 정보 조회 등 지도 및 앨범 관련 새로운 API 엔드포인트에 대한 클라이언트 코드를 생성했습니다.
  • UI/UX 개선: 앨범 컨테이너의 반응형 스타일을 개선하고, 메뉴 버튼에 'placement' 속성을 추가하여 메뉴 패널의 위치를 제어할 수 있도록 했습니다. 또한, 입력 필드의 글자 수 표시 로직을 조정했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: *.json (1)
    • apps/web/package.json
  • Ignored by pattern: *.yaml (1)
    • pnpm-lock.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이번 PR은 메인 지도 화면과 바텀시트를 추가하는 중요한 기능 구현이네요. 전반적으로 새로운 컴포넌트와 훅을 이용해 구조를 잘 잡으셨습니다. 제 리뷰는 주로 제공된 프론트엔드 디자인 가이드라인에 따라 가독성과 유지보수성을 높이는 방향에 초점을 맞췄습니다. 특히 매직 넘버를 상수로 대체하고, 복잡한 로직을 추상화하는 부분을 중점적으로 보았습니다. 또한, 빌드를 중단시킬 수 있는 치명적인 경로 오류도 발견하여 코멘트를 남겼습니다. 좋은 코드에 기여해주셔서 감사합니다!

Comment on lines 17 to 30
const onMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current!.scrollLeft;
};

const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current) return;
e.preventDefault();

const x = e.pageX;
const walk = x - startX.current;
containerRef.current!.scrollLeft = scrollLeft.current - walk;
};

Choose a reason for hiding this comment

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

high

containerRef.current에 non-null assertion(!)을 사용하고 있습니다. 만약 ref가 할당되기 전에 이벤트가 발생하면 런타임 에러가 발생할 수 있습니다. 각 핸들러 시작 부분에 containerRef.current가 유효한지 확인하는 로직을 추가하여 코드의 안정성을 높이는 것이 좋습니다.

Suggested change
const onMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current!.scrollLeft;
};
const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current) return;
e.preventDefault();
const x = e.pageX;
const walk = x - startX.current;
containerRef.current!.scrollLeft = scrollLeft.current - walk;
};
const onMouseDown = (e: React.MouseEvent) => {
if (!containerRef.current) return;
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current.scrollLeft;
};
const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current || !containerRef.current) return;
e.preventDefault();
const x = e.pageX;
const walk = x - startX.current;
containerRef.current.scrollLeft = scrollLeft.current - walk;
};

Comment on lines 31 to 33
latitude: 37.5665,
longitude: 126.978,
zoom: 12,

Choose a reason for hiding this comment

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

medium

위치 정보 조회에 실패했을 때 사용되는 기본 위치(위도, 경도)와 줌 레벨이 하드코딩되어 있습니다. 이러한 '매직 넘버'는 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.

**Frontend Design Guideline (Readability - Naming Magic Numbers)**에 따라, DEFAULT_LOCATION과 같은 명명된 상수로 추출하여 관리하는 것을 권장합니다.

References
  1. 의미를 알 수 없는 매직 넘버를 명명된 상수로 대체하여 코드의 명확성과 유지보수성을 향상시켜야 합니다. (link)

Comment on lines 5 to 8
const MIN_HEIGHT = 160;
const MID_HEIGHT = 394;

const getMaxHeight = () => (typeof window !== 'undefined' ? window.innerHeight : 800);

Choose a reason for hiding this comment

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

medium

바텀시트의 높이를 결정하는 MIN_HEIGHT, MID_HEIGHTgetMaxHeight의 fallback 값 800이 매직 넘버로 사용되고 있습니다.

**Frontend Design Guideline (Readability - Naming Magic Numbers)**에 따라, 이 값들을 export된 상수로 정의하면 코드의 의도가 명확해지고 다른 곳에서 재사용하기 용이해집니다.

References
  1. 의미를 알 수 없는 매직 넘버를 명명된 상수로 대체하여 코드의 명확성과 유지보수성을 향상시켜야 합니다. (link)

longitude: locationState.longitude,
zoom: locationState.zoom,
}}
mapStyle="mapbox://styles/hongju/cmkruslij000k01sfb7c48qju"

Choose a reason for hiding this comment

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

medium

Mapbox 스타일 URL이 문자열로 하드코딩되어 있습니다. 나중에 스타일을 변경하거나 다른 곳에서 재사용할 경우를 대비해 상수로 추출하는 것이 유지보수 측면에서 유리합니다. 이는 Frontend Design Guideline (Readability - Naming Magic Numbers) 규칙을 따르는 좋은 습관입니다.

References
  1. 의미를 알 수 없는 매직 넘버를 명명된 상수로 대체하여 코드의 명확성과 유지보수성을 향상시켜야 합니다. (link)

return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,

Choose a reason for hiding this comment

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

medium

timeout 값이 10000으로 하드코딩되어 있습니다. GEOLOCATION_TIMEOUT_MS와 같이 의미 있는 이름의 상수로 만들어 사용하면 가독성과 유지보수성을 높일 수 있습니다. 이는 Frontend Design Guideline (Readability - Naming Magic Numbers) 규칙에 해당합니다.

References
  1. 의미를 알 수 없는 매직 넘버를 명명된 상수로 대체하여 코드의 명확성과 유지보수성을 향상시켜야 합니다. (link)

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/components/buttons/menuButton/MenuButton.styles.ts (1)

12-12: 기존 코드 버그: backdropBlur 객체 직접 참조

theme.effects.backdropBlur는 객체이므로 CSS에서 [object Object]로 렌더링됩니다. Line 42의 MenuPanel처럼 특정 키를 참조해야 합니다.

🐛 수정 제안
 export const TriggerButton = styled.button`
   border-radius: 999px;
   background: ${({ theme }) => theme.colors.blueWhite.bg8};
-  backdrop-filter: ${({ theme }) => theme.effects.backdropBlur};
+  backdrop-filter: ${({ theme }) => theme.effects.backdropBlur[25]};
   border: 1px solid ${({ theme }) => theme.colors.blueWhite.border10};
   padding: 11px;
🤖 Fix all issues with AI agents
In `@apps/web/src/app/map/mockData.ts`:
- Around line 174-188: The reduce for albumDetailById currently uses album.id ??
0 which causes collisions when id is undefined; update the logic to either skip
albums with nullish ids or generate safe unique keys instead. Specifically,
before reducing safeAlbums, filter out entries where album.id == null (so
album.id is guaranteed) or, if you need to keep them, derive a stable fallback
key (e.g., a prefixed index or UUID) rather than 0 to avoid overwriting; adjust
references to albumDetailById, safeAlbums, and AlbumDetailData accordingly so
the resulting record keys are unique and type-safe.

In `@apps/web/src/components/bottomSheet/albumRow/AlbumRow.tsx`:
- Around line 17-30: Replace unsafe non-null assertions on containerRef.current
in onMouseDown/onMouseMove with proper null checks and early returns: check
containerRef.current exists before reading/writing scrollLeft. Add corresponding
mouse up/leave handlers to set isDragging.current = false. Implement touch
equivalents (onTouchStart, onTouchMove, onTouchEnd) that mirror
onMouseDown/onMouseMove logic using e.touches[0].pageX, preventDefault in touch
move, and update startX, scrollLeft, and isDragging refs (symbols: onMouseDown,
onMouseMove, containerRef, isDragging, startX, scrollLeft); wire these handlers
into the JSX so dragging works on mobile/tablet.

In
`@apps/web/src/components/bottomSheet/bottomSheetContent/BottomSheetContent.tsx`:
- Line 1: The import for AlbumDetail contains an accidental backslash escape
sequence; open BottomSheetContent.tsx, locate the import statement that
references AlbumDetail and remove the `\b` so the path reads correctly (e.g.,
import AlbumDetail from '../albumDetail/AlbumDetail'), then save and verify the
module resolves and the build error is gone.
- Around line 27-30: The AlbumDetail branch passes
albumDetailById[context.albumId] directly, which can be undefined; update the
BottomSheetContent component to guard against missing IDs by checking
albumDetailById[context.albumId] before rendering (e.g., if falsy return null or
a loading/placeholder) or pass a validated/fallback object to AlbumDetail;
reference the albumDetailById lookup and the AlbumDetail component in your
change and ensure context.albumId is validated first.

In `@apps/web/src/components/image/ImagePin.stories.ts`:
- Line 2: 파일 ImagePin.stories.ts에서 사용되지 않는 import TextButton을 제거하세요: 찾아서 import
TextButton from '@/components/buttons/textButton/TextButton'; 줄을 삭제하면 됩니다 (관련된
컴포넌트/스토리 코드에는 TextButton 참조가 없으므로 다른 변경은 필요 없습니다).

In `@apps/web/src/components/map/MapView.styels.ts`:
- Around line 1-6: 파일명 오타로 MapView.styels.ts를 MapView.styles.ts로 리네임하고, MapView
컴포넌트의 import 구문에서 잘못된 파일명을 사용한 부분을 수정하세요; 구체적으로 현재 파일 MapView.styels.ts →
MapView.styles.ts로 변경하고 MapView.tsx에서 import { Wrapper } from './MapView.styels'
(또는 유사한 경로) 를 './MapView.styles' 로 업데이트하며 프로젝트 전반에서 동일한 잘못된 파일명이 사용된 다른 import가
있는지 확인해 조정하세요.

In `@apps/web/src/components/map/MapView.tsx`:
- Around line 62-66: Remove the debug console.log in the ImagePin onClick and
wire the click to a real handler: replace the inline onClick={() =>
console.log(...)} with a call to a prop handler (e.g., onPinClick) or a local
method that opens the album bottom sheet; update the MapView props/interface
(MapViewProps) to include onPinClick?: (pinId: number) => void and ensure the
ImagePin usage passes onClick={() => props.onPinClick?.(pin.id)} (or calls the
local openAlbumDetail(pin.id) function) so pin clicks trigger the album-detail
flow instead of logging.
🧹 Nitpick comments (13)
apps/web/src/app/map/page.styles.ts (1)

3-7: 모바일 100vh 높이 이슈 완화를 위해 dvh 보조값 고려
주소창 높이 변화로 100vh가 흔들릴 수 있어요. 필요하면 dvh를 후행으로 추가해 보세요.

♻️ 제안 변경
 export const Wrapper = styled.div`
   width: 100%;
-  height: 100vh;
+  height: 100vh;
+  height: 100dvh;
   position: relative;
 `;
apps/web/src/components/bottomSheet/albumRow/AlbumRow.styles.ts (1)

3-19: 스크롤바 숨김을 브라우저별로 보강 고려
현재 WebKit만 숨김 처리되어 Firefox/Edge에서는 스크롤바가 보일 수 있습니다. 필요하면 아래 속성 추가를 고려하세요.

♻️ 제안 변경
   -webkit-overflow-scrolling: touch;
+  -ms-overflow-style: none;
+  scrollbar-width: none;

   &::-webkit-scrollbar {
     display: none;
   }
apps/web/src/components/image/ImagePin.styles.ts (1)

8-8: 테마 색상 일관성 고려

drop-shadow에 하드코딩된 rgba 값을 사용하고 있습니다. 다른 스타일들은 theme.colors를 사용하고 있으므로, 일관성을 위해 테마에 그림자 색상을 정의하는 것을 고려해 볼 수 있습니다.

apps/web/src/components/bottomSheet/albumGrid/AlbumGrid.tsx (1)

20-24: 대소문자 구분 없는 검색을 권장합니다.

현재 album.title.includes(keyword) 구현은 대소문자를 구분하므로, 사용자가 "한강"을 검색할 때 "한강"과 "Han강"이 다르게 처리됩니다. 한국어 앨범 제목의 경우 큰 문제가 아닐 수 있지만, 영문이 섞인 경우 UX가 저하될 수 있습니다.

♻️ 대소문자 무시 검색 제안
 const filteredAlbums = useMemo(() => {
   const keyword = searchValue.trim();
   if (!keyword) return albums;
-  return albums.filter((album) => album.title.includes(keyword));
+  return albums.filter((album) => 
+    album.title.toLowerCase().includes(keyword.toLowerCase())
+  );
 }, [albums, searchValue]);
apps/web/src/app/map/mockData.ts (1)

198-205: 필터 후 불필요한 fallback 값 사용

Line 193-196에서 latitudelongitude가 유효한 number인지 필터링했으므로, Line 201-202의 ?? center.latitude fallback은 실행될 일이 없습니다. 코드 가독성을 위해 non-null assertion 또는 타입 가드 활용을 고려해 주세요.

apps/web/src/components/bottomSheet/BottomSheet.styles.ts (2)

7-23: 하드코딩된 z-index 및 box-shadow 값

z-index: 1000box-shadow의 rgba 값이 하드코딩되어 있습니다. 테마에서 관리되는 theme.zIndextheme.effects를 활용하면 일관성과 유지보수성이 향상됩니다. 또한 Line 9의 주석 처리된 코드는 제거하거나 TODO 주석을 추가하는 것을 권장합니다.


34-39: 하드코딩된 핸들 색상

.handle의 배경색 #e0e0e0이 하드코딩되어 있습니다. 테마 컬러를 사용하면 다크 모드 등 테마 변경 시 자동으로 대응됩니다.

♻️ 테마 컬러 사용 제안
   .handle {
     width: 40px;
     height: 5px;
-    background-color: `#e0e0e0`;
+    background-color: ${({ theme }) => theme.colors.blueWhite.border10};
     border-radius: 10px;
   }
apps/web/src/components/map/MapView.tsx (1)

4-4: 전역 Map 객체 섀도잉 경고

react-map-gl에서 가져온 Map 컴포넌트가 JavaScript 전역 Map 객체를 섀도잉합니다. 혼동을 방지하기 위해 이름을 변경하는 것을 고려해 주세요.

♻️ 제안된 수정
-import { Map, GeolocateControl, Marker } from 'react-map-gl/mapbox';
+import { Map as MapboxMap, GeolocateControl, Marker } from 'react-map-gl/mapbox';

그리고 컴포넌트 사용 시:

-      <Map
+      <MapboxMap
         mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
         ...
-      </Map>
+      </MapboxMap>
apps/web/src/components/bottomSheet/_hooks/useBottomSheetController.ts (1)

5-6: 상수를 훅 외부로 이동 권장

MIN_HEIGHTMID_HEIGHT가 매 렌더링마다 재생성됩니다. 성능 최적화를 위해 훅 외부로 이동하는 것이 좋습니다.

♻️ 제안된 수정
 import { useEffect, useState } from 'react';
 import { SheetContext } from '../_context/SheetContext';

+const MIN_HEIGHT = 160;
+const MID_HEIGHT = 394;
+
 export function useBottomSheetController(context: SheetContext) {
-  const MIN_HEIGHT = 160;
-  const MID_HEIGHT = 394;
apps/web/src/components/bottomSheet/BottomSheet.tsx (1)

91-99: 빈 onClick 핸들러들이 있습니다.

현재 모의 데이터를 사용 중이라는 PR 설명에 따라 placeholder로 이해됩니다. 추후 실제 기능 구현 시 이 핸들러들을 연결해야 합니다.

-        <TextButton text="사진 추가" onClick={() => {}} />
-        <TextButton text="사진 추가" onClick={() => {}} />
-        <TextButton text="앨범 추가" onClick={() => {}} />
+        {/* TODO: 실제 핸들러 연결 필요 */}
+        <TextButton text="사진 추가" onClick={() => {}} />
+        <TextButton text="사진 추가" onClick={() => {}} />
+        <TextButton text="앨범 추가" onClick={() => {}} />
apps/web/src/app/map/page.tsx (2)

28-35: 에러 로깅에 console.error 사용을 권장합니다.

catch 블록에서 에러를 로깅할 때는 console.log 대신 console.error를 사용하는 것이 적절합니다. 이렇게 하면 브라우저 개발자 도구에서 에러로 분류되어 디버깅이 용이합니다.

제안된 수정
       } catch (err) {
-        console.log(err);
+        console.error('위치 정보를 가져오는데 실패했습니다:', err);
         setViewState({
           latitude: 37.5665,
           longitude: 126.978,
           zoom: 12,
         });
       }

63-68: 하드코딩된 위치 문자열

"서울특별시 마포구"가 하드코딩되어 있습니다. 현재는 모의 데이터 단계이므로 괜찮지만, 추후 실제 위치 기반 또는 사용자 설정에 따라 동적으로 변경되어야 할 것으로 보입니다.

apps/web/src/components/bottomSheet/�albumDetail/AlbumDetail.tsx (1)

21-24: TODO 항목과 빈 onClick 핸들러에 대한 후속 작업 확인 필요.

현재 하드코딩된 날짜와 빈 onClick 핸들러가 있습니다:

  • date: TODO 주석으로 API 스펙 제한 사항이 명시되어 있어 이해됩니다.
  • onClick: 빈 함수 () => {}가 전달되고 있는데, 이후 사진 클릭 시 상세 보기 등의 기능이 필요할 수 있습니다.

이 항목들을 추적하기 위한 이슈를 생성하거나, API 스펙이 업데이트될 때 함께 수정할 계획이 있는지 확인해 주세요.

후속 작업을 추적하기 위한 이슈를 생성해 드릴까요?

Comment on lines 174 to 188
export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
(acc, album) => {
const albumId = album.id ?? 0;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
},
{},
);
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

ID 기본값 0 사용 시 데이터 충돌 가능성

album.id ?? 0을 사용하면 여러 앨범의 ID가 undefined일 경우 모두 키 0으로 저장되어 데이터가 덮어씌워질 수 있습니다. 현재는 목 데이터라 문제가 없지만, 실제 API 연동 시 이 패턴이 그대로 사용되면 버그로 이어질 수 있습니다.

💡 고유 ID 생성 또는 필터링 제안
-export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
-  (acc, album) => {
-    const albumId = album.id ?? 0;
+export const albumDetailById = safeAlbums
+  .filter((album) => album.id != null)
+  .reduce<Record<number, AlbumDetailData>>((acc, album) => {
+    const albumId = album.id!;
     acc[albumId] = {
       id: albumId,
       title: album.title ?? '알 수 없는 앨범',
       photos: (album.photos ?? []).map((photo) => ({
         id: photo.id ?? 0,
         url: photo.url ?? '',
       })),
     };
     return acc;
-  },
-  {},
-);
+  }, {});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
(acc, album) => {
const albumId = album.id ?? 0;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
},
{},
);
export const albumDetailById = safeAlbums
.filter((album) => album.id != null)
.reduce<Record<number, AlbumDetailData>>((acc, album) => {
const albumId = album.id!;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
}, {});
🤖 Prompt for AI Agents
In `@apps/web/src/app/map/mockData.ts` around lines 174 - 188, The reduce for
albumDetailById currently uses album.id ?? 0 which causes collisions when id is
undefined; update the logic to either skip albums with nullish ids or generate
safe unique keys instead. Specifically, before reducing safeAlbums, filter out
entries where album.id == null (so album.id is guaranteed) or, if you need to
keep them, derive a stable fallback key (e.g., a prefixed index or UUID) rather
than 0 to avoid overwriting; adjust references to albumDetailById, safeAlbums,
and AlbumDetailData accordingly so the resulting record keys are unique and
type-safe.

Comment on lines 17 to 30
const onMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current!.scrollLeft;
};

const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current) return;
e.preventDefault();

const x = e.pageX;
const walk = x - startX.current;
containerRef.current!.scrollLeft = scrollLeft.current - walk;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Non-null assertion 사용 및 모바일 터치 이벤트 미지원

  1. Line 20, 29의 containerRef.current!는 ref가 아직 연결되지 않은 경우 런타임 에러를 발생시킬 수 있습니다.
  2. 마우스 이벤트만 처리되어 모바일/태블릿에서 드래그 스크롤이 작동하지 않습니다.
🔧 null 체크 및 터치 이벤트 추가 제안
 const onMouseDown = (e: React.MouseEvent) => {
+  if (!containerRef.current) return;
   isDragging.current = true;
   startX.current = e.pageX;
-  scrollLeft.current = containerRef.current!.scrollLeft;
+  scrollLeft.current = containerRef.current.scrollLeft;
 };

 const onMouseMove = (e: React.MouseEvent) => {
-  if (!isDragging.current) return;
+  if (!isDragging.current || !containerRef.current) return;
   e.preventDefault();

   const x = e.pageX;
   const walk = x - startX.current;
-  containerRef.current!.scrollLeft = scrollLeft.current - walk;
+  containerRef.current.scrollLeft = scrollLeft.current - walk;
 };
+
+const onTouchStart = (e: React.TouchEvent) => {
+  if (!containerRef.current) return;
+  isDragging.current = true;
+  startX.current = e.touches[0].pageX;
+  scrollLeft.current = containerRef.current.scrollLeft;
+};
+
+const onTouchMove = (e: React.TouchEvent) => {
+  if (!isDragging.current || !containerRef.current) return;
+  const x = e.touches[0].pageX;
+  const walk = x - startX.current;
+  containerRef.current.scrollLeft = scrollLeft.current - walk;
+};

그리고 JSX에 터치 이벤트 핸들러 추가:

 <S.Wrapper
   ref={containerRef}
   onMouseDown={onMouseDown}
   onMouseMove={onMouseMove}
   onMouseUp={onMouseUp}
   onMouseLeave={onMouseUp}
+  onTouchStart={onTouchStart}
+  onTouchMove={onTouchMove}
+  onTouchEnd={onMouseUp}
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current!.scrollLeft;
};
const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current) return;
e.preventDefault();
const x = e.pageX;
const walk = x - startX.current;
containerRef.current!.scrollLeft = scrollLeft.current - walk;
};
const onMouseDown = (e: React.MouseEvent) => {
if (!containerRef.current) return;
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current.scrollLeft;
};
const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging.current || !containerRef.current) return;
e.preventDefault();
const x = e.pageX;
const walk = x - startX.current;
containerRef.current.scrollLeft = scrollLeft.current - walk;
};
const onTouchStart = (e: React.TouchEvent) => {
if (!containerRef.current) return;
isDragging.current = true;
startX.current = e.touches[0].pageX;
scrollLeft.current = containerRef.current.scrollLeft;
};
const onTouchMove = (e: React.TouchEvent) => {
if (!isDragging.current || !containerRef.current) return;
const x = e.touches[0].pageX;
const walk = x - startX.current;
containerRef.current.scrollLeft = scrollLeft.current - walk;
};
🤖 Prompt for AI Agents
In `@apps/web/src/components/bottomSheet/albumRow/AlbumRow.tsx` around lines 17 -
30, Replace unsafe non-null assertions on containerRef.current in
onMouseDown/onMouseMove with proper null checks and early returns: check
containerRef.current exists before reading/writing scrollLeft. Add corresponding
mouse up/leave handlers to set isDragging.current = false. Implement touch
equivalents (onTouchStart, onTouchMove, onTouchEnd) that mirror
onMouseDown/onMouseMove logic using e.touches[0].pageX, preventDefault in touch
move, and update startX, scrollLeft, and isDragging refs (symbols: onMouseDown,
onMouseMove, containerRef, isDragging, startX, scrollLeft); wire these handlers
into the JSX so dragging works on mobile/tablet.

@@ -0,0 +1,37 @@
import AlbumDetail from '../\balbumDetail/AlbumDetail';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

임포트 경로에 잘못된 문자 포함

임포트 경로 '../\balbumDetail/AlbumDetail'에 이스케이프 시퀀스 \b(백스페이스)가 포함되어 있습니다. 이는 빌드 오류를 일으킬 수 있습니다.

🐛 제안된 수정
-import AlbumDetail from '../\balbumDetail/AlbumDetail';
+import AlbumDetail from '../albumDetail/AlbumDetail';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import AlbumDetail from '../\balbumDetail/AlbumDetail';
import AlbumDetail from '../albumDetail/AlbumDetail';
🤖 Prompt for AI Agents
In
`@apps/web/src/components/bottomSheet/bottomSheetContent/BottomSheetContent.tsx`
at line 1, The import for AlbumDetail contains an accidental backslash escape
sequence; open BottomSheetContent.tsx, locate the import statement that
references AlbumDetail and remove the `\b` so the path reads correctly (e.g.,
import AlbumDetail from '../albumDetail/AlbumDetail'), then save and verify the
module resolves and the build error is gone.

Comment on lines 27 to 30
case 'albumDetail':
return (
<AlbumDetail album={albumDetailById[context.albumId]} />
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

albumDetailById 조회 시 존재하지 않는 ID 처리 필요

context.albumIdalbumDetailById에 존재하지 않을 경우 undefinedAlbumDetail에 전달됩니다. 방어적 처리를 추가하는 것이 좋습니다.

🐛 제안된 수정
     case 'albumDetail':
+      const albumDetail = albumDetailById[context.albumId];
+      if (!albumDetail) {
+        return null; // 또는 로딩/에러 상태 표시
+      }
       return (
-        <AlbumDetail album={albumDetailById[context.albumId]} />
+        <AlbumDetail album={albumDetail} />
       );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'albumDetail':
return (
<AlbumDetail album={albumDetailById[context.albumId]} />
);
case 'albumDetail':
const albumDetail = albumDetailById[context.albumId];
if (!albumDetail) {
return null; // 또는 로딩/에러 상태 표시
}
return (
<AlbumDetail album={albumDetail} />
);
🤖 Prompt for AI Agents
In
`@apps/web/src/components/bottomSheet/bottomSheetContent/BottomSheetContent.tsx`
around lines 27 - 30, The AlbumDetail branch passes
albumDetailById[context.albumId] directly, which can be undefined; update the
BottomSheetContent component to guard against missing IDs by checking
albumDetailById[context.albumId] before rendering (e.g., if falsy return null or
a loading/placeholder) or pass a validated/fallback object to AlbumDetail;
reference the albumDetailById lookup and the AlbumDetail component in your
change and ensure context.albumId is validated first.

@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react';
import TextButton from '@/components/buttons/textButton/TextButton';
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

사용하지 않는 import 제거

TextButton이 import되었지만 사용되지 않습니다.

🐛 제안된 수정
 import type { Meta, StoryObj } from '@storybook/react';
-import TextButton from '@/components/buttons/textButton/TextButton';
 import ImagePin from './ImagePin';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import TextButton from '@/components/buttons/textButton/TextButton';
import type { Meta, StoryObj } from '@storybook/react';
import ImagePin from './ImagePin';
🤖 Prompt for AI Agents
In `@apps/web/src/components/image/ImagePin.stories.ts` at line 2, 파일
ImagePin.stories.ts에서 사용되지 않는 import TextButton을 제거하세요: 찾아서 import TextButton
from '@/components/buttons/textButton/TextButton'; 줄을 삭제하면 됩니다 (관련된 컴포넌트/스토리
코드에는 TextButton 참조가 없으므로 다른 변경은 필요 없습니다).

Comment on lines +1 to +6
import styled from '@emotion/styled';

export const Wrapper = styled.div`
width: 100%;
height: 100vh;
`;
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

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and look for similar style files
find apps/web/src/components -name "*styles.ts" -o -name "*styels.ts" 2>/dev/null | head -20

Repository: YAPP-Github/27th-Web-Team-1-FE

Length of output: 1269


🏁 Script executed:

# Search for imports of MapView.styels
rg -n "MapView.styels|from.*MapView.styels" --type=ts --type=tsx

Repository: YAPP-Github/27th-Web-Team-1-FE

Length of output: 101


🏁 Script executed:

# Check the actual file in map directory
ls -la apps/web/src/components/map/ 2>/dev/null || echo "Directory check"

Repository: YAPP-Github/27th-Web-Team-1-FE

Length of output: 316


🏁 Script executed:

# Search for imports of MapView.styels with simpler syntax
rg "MapView\.styels|from.*map/MapView" apps/web/src

Repository: YAPP-Github/27th-Web-Team-1-FE

Length of output: 230


🏁 Script executed:

# Also check if MapView.tsx imports MapView.styels
cat -n apps/web/src/components/map/MapView.tsx | head -30

Repository: YAPP-Github/27th-Web-Team-1-FE

Length of output: 1120


파일명 오타: MapView.styels.tsMapView.styles.ts

파일명에 오타가 있습니다 (styelsstyles). 프로젝트의 모든 스타일 파일(AlbumContainer.styles.ts, Button.styles.ts 등)은 .styles.ts로 명명되어 있으며, MapView.tsx의 import 문(8번 줄)도 함께 수정되어야 합니다.

🤖 Prompt for AI Agents
In `@apps/web/src/components/map/MapView.styels.ts` around lines 1 - 6, 파일명 오타로
MapView.styels.ts를 MapView.styles.ts로 리네임하고, MapView 컴포넌트의 import 구문에서 잘못된 파일명을
사용한 부분을 수정하세요; 구체적으로 현재 파일 MapView.styels.ts → MapView.styles.ts로 변경하고
MapView.tsx에서 import { Wrapper } from './MapView.styels' (또는 유사한 경로) 를
'./MapView.styles' 로 업데이트하며 프로젝트 전반에서 동일한 잘못된 파일명이 사용된 다른 import가 있는지 확인해 조정하세요.

Comment on lines +62 to +66
<ImagePin
imageUrl={pin.imageUrl}
imageCount={pin.imageCount}
onClick={() => console.log(`${pin.id} 클릭됨`)}
/>
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

디버그용 console.log 제거 필요

프로덕션 코드에 console.log가 남아 있습니다. 실제 클릭 핸들러 로직으로 교체하거나, 핀 클릭 시 바텀시트에서 앨범 상세를 표시하는 콜백을 props로 전달받는 것을 고려해 주세요.

🐛 제안된 수정
 <ImagePin
   imageUrl={pin.imageUrl}
   imageCount={pin.imageCount}
-  onClick={() => console.log(`${pin.id} 클릭됨`)}
+  onClick={() => onPinClick?.(pin.id)}
 />

Props에 핸들러 추가:

interface MapViewProps {
  locationState: LocationState;
  pins: MapPin[];
  selectedAlbumId: number | null;
  onPinClick?: (pinId: number) => void;
}
🤖 Prompt for AI Agents
In `@apps/web/src/components/map/MapView.tsx` around lines 62 - 66, Remove the
debug console.log in the ImagePin onClick and wire the click to a real handler:
replace the inline onClick={() => console.log(...)} with a call to a prop
handler (e.g., onPinClick) or a local method that opens the album bottom sheet;
update the MapView props/interface (MapViewProps) to include onPinClick?:
(pinId: number) => void and ensure the ImagePin usage passes onClick={() =>
props.onPinClick?.(pin.id)} (or calls the local openAlbumDetail(pin.id)
function) so pin clicks trigger the album-detail flow instead of logging.

Copy link
Contributor

@salmonco salmonco left a comment

Choose a reason for hiding this comment

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

완성도가 올라갔네요 감사합니다 ㅎㅎ

코멘트 확인 부탁드려요!

export default function MapPage() {
const [viewState, setViewState] = useState<LocationState | null>(null);
const [sheetContext, setSheetContext] = useState<SheetContext>({
type: 'home',
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 타입을 추후 관리하기 편하게 상수로 관리하면 어떨까요? 혹시나 이름 변경되면 코드 변경이 많아져서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

3f7b8b2

추가하였습니다!

} catch (err) {
console.log(err);
setViewState({
latitude: 37.5665,
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 추후 변경하기 편하게 기본 위치도 상수화하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

75c4446

수정하였습니당 ~


const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
Copy link
Contributor

Choose a reason for hiding this comment

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

(질문) currentTarget 타입 단언 안하는 방법은 없을까요?

import { SheetContext } from '../_context/SheetContext';

export function useBottomSheetController(context: SheetContext) {
const MIN_HEIGHT = 160;
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 상수를 훅 밖으로 빼면 어떨까요? 렌더링에 상관 없는 값이라서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

03479c8

수정하였어요!

const MIN_HEIGHT = 160;
const MID_HEIGHT = 394;

const getMaxHeight = () => (typeof window !== 'undefined' ? window.innerHeight : 800);
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 얘도 useEffect 안에서 값 저장하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이 함수는 다른 함수도 접근해야해서 따로 분리하였습니다!

const onMouseDown = (e: React.MouseEvent) => {
isDragging.current = true;
startX.current = e.pageX;
scrollLeft.current = containerRef.current!.scrollLeft;
Copy link
Contributor

Choose a reason for hiding this comment

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

(질문) 단언으로 된 이유가 있을까요? null check 해도 될 것 같아서요


const x = e.pageX;
const walk = x - startX.current;
containerRef.current!.scrollLeft = scrollLeft.current - walk;
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 단언대신 null check하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

f21d824

타입 단언과 관련된 부분들은 이 커밋에서 적용하였습니다!

return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 타임아웃 상수화하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@H0ngJu H0ngJu requested a review from salmonco January 29, 2026 11:35
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: 5

🤖 Fix all issues with AI agents
In `@apps/web/src/components/bottomSheet/_hooks/useBottomSheetController.ts`:
- Around line 32-38: deriveContextFromHeight currently returns only HOME or
ALBUM_LIST based on height, which can drop ALBUM_DETAIL and its albumId when
snapping; update deriveContextFromHeight to accept the current context (e.g., a
parameter currentContext: SheetContext) and, when height falls into ranges that
would otherwise map to HOME or ALBUM_LIST, preserve and return the existing
ALBUM_DETAIL context (with its albumId) instead of discarding it—modify the
function signature and logic in deriveContextFromHeight and update callers to
pass through the current context so ALBUM_DETAIL is retained.

In `@apps/web/src/hooks/queries/useMapPhotos.ts`:
- Around line 19-23: In useMapPhotos.ts the params spread uses a truthy check so
albumId === 0 will be omitted; change the conditional that adds albumId (the
expression currently using (params.albumId ? { albumId: params.albumId } : {}))
to a null/undefined check (e.g., params.albumId != null or params.albumId !==
undefined && params.albumId !== null) so albumId values like 0 are preserved in
the query params.
- Around line 39-55: photoPins and clusterPins currently assign non-unique
fallback ids (photo.id ?? 0 and index for clusters) which can cause key
collisions; update the mapping in useMapPhotos to ensure each MapPin.id is
unique by either filtering out items without ids or generating a deterministic
unique fallback (e.g., combine a prefix with the array index or a stable hash of
thumbnailUrl/coordinates) when creating photoPins and clusterPins; locate the
map callbacks that produce MapPin objects and replace the fixed 0 fallback with
a unique fallback strategy (or filter undefined ids) so rendering keys and
selection logic won’t collide.

In `@apps/web/src/hooks/queries/useSelectableAlbums.ts`:
- Around line 21-28: The mapping in useSelectableAlbums sets id to 0 when
album.id is missing, causing duplicate IDs; update the map in
useSelectableAlbums to either filter out albums with missing id before mapping
or generate a unique fallback per item (e.g., use the map index or a UUID with a
prefix) instead of a constant 0, and ensure the resulting id type stays
consistent with downstream selection logic (adjust consumers if switching to
string fallbacks).

In `@apps/web/src/mocks/handlers/map/getMapPhotos/mockGetMapPhotos.ts`:
- Around line 8-23: The code uses Number(url.searchParams.get('zoom')) and
Number(albumId) without guarding against NaN which can break the zoom/albumId
branching and filters; update the parsing in this block so zoom is parsed with a
fallback (e.g., const zoom = Number(parsed) || 14 or using isNaN check) and
albumId is validated/converted to a numeric id (e.g., const albumIdNum = albumId
!== null ? Number(albumId) : null and check isNaN to set albumIdNum = null)
before using it in filteredPhotos/filteredClusters and the zoom >= 15 branch;
make sure to reference and replace uses of zoom, albumId (or create albumIdNum),
filteredPhotos, filteredClusters, and the data assignment for MapPhotosResponse
so filters only run with a valid numeric albumId and zoom has a safe default.
🧹 Nitpick comments (6)
apps/web/src/app/map/constants.ts (2)

1-7: 기본 줌 값(12 vs 14) 불일치로 혼란/드리프트 위험

DEFAULT_LOCATION.zoomDEFAULT_ZOOM이 서로 달라 사용처에 따라 기본 줌이 달라질 수 있습니다. 의도적으로 다른 값이라면 역할을 명확히 드러내는 주석/이름을 추가하고, 동일 의도라면 단일 소스로 합치는 편이 안전합니다.

🔧 한 가지 안전한 정리 예시(역할 명확화)
 export const DEFAULT_LOCATION = {
   latitude: 37.5665,
   longitude: 126.978,
-  zoom: 12,
+  zoom: 12, // 기본 위치(fallback) 초기 줌
 } as const;
 
-export const DEFAULT_ZOOM = 14;
+export const DEFAULT_ZOOM = 14; // 사용자 위치 기반 기본 줌

1-7: 상수 네이밍을 camelCase로 맞춰주세요

가이드라인에서 변수/함수는 camelCase를 사용하도록 되어 있어 DEFAULT_LOCATION, DEFAULT_ZOOM은 규칙 위반입니다. 사용처도 함께 업데이트해주세요.

♻️ 제안 변경
-export const DEFAULT_LOCATION = {
+export const defaultLocation = {
   latitude: 37.5665,
   longitude: 126.978,
   zoom: 12,
 } as const;
 
-export const DEFAULT_ZOOM = 14;
+export const defaultZoom = 14;
apps/web/src/components/bottomSheet/constants.ts (1)

1-13: 상수 네이밍을 camelCase로 통일하는 편이 가이드와 맞습니다.

라인 1-13의 상수명이 SCREAMING_SNAKE_CASE입니다. 프로젝트 가이드가 변수/함수 camelCase를 요구하므로 네이밍 정리를 권장합니다(사용처도 함께 수정 필요).

♻️ 제안 변경(사용처 동시 수정 필요)
-export const SHEET_CONTEXT_TYPE = {
+export const sheetContextType = {
   HOME: 'home',
   ALBUM_LIST: 'albumList',
   ALBUM_DETAIL: 'albumDetail',
 } as const;
 
-export type SheetContext =
-  | { type: typeof SHEET_CONTEXT_TYPE.HOME }
-  | { type: typeof SHEET_CONTEXT_TYPE.ALBUM_LIST }
-  | { type: typeof SHEET_CONTEXT_TYPE.ALBUM_DETAIL; albumId: number };
+export type SheetContext =
+  | { type: typeof sheetContextType.HOME }
+  | { type: typeof sheetContextType.ALBUM_LIST }
+  | { type: typeof sheetContextType.ALBUM_DETAIL; albumId: number };
 
-export const MIN_HEIGHT = 160;
-export const MID_HEIGHT = 394;
+export const minHeight = 160;
+export const midHeight = 394;
apps/web/src/hooks/queries/useSelectableAlbums.ts (1)

24-27: TODO는 이슈로 추적하는 편이 좋습니다.

Line 24-27의 TODO가 실제로 필요한 변경이라면 이슈로 분리해 추적하는 것을 권장합니다.

원하시면 이 변경을 반영한 타입/매핑 수정안을 바로 제안해 드릴까요?

apps/web/src/mocks/handlers.ts (1)

5-10: excludedKeywords 배열에 중복 매칭 가능성이 있습니다.

'/photos'가 이미 '/photos/presigned-url''/map/photos' 경로를 포함하기 때문에 .includes() 로직상 중복 매칭이 발생합니다. 기능적으로는 문제없지만, 명시적 의도를 위해 현재 구조를 유지하거나, 더 구체적인 경로만 남기는 방안을 고려해볼 수 있습니다.

apps/web/src/mocks/handlers/photos/getPhotos/mockGetPhotos.ts (1)

17-19: Number() 변환 시 유효하지 않은 값 처리를 고려해보세요.

albumId가 숫자가 아닌 문자열(예: "abc")일 경우 Number(albumId)NaN을 반환합니다. 현재 로직에서는 find()가 일치하는 앨범을 찾지 못해 빈 배열을 반환하므로 기능상 문제는 없지만, 명시적인 유효성 검사를 추가하면 디버깅 시 도움이 될 수 있습니다.

♻️ 선택적 개선안
  const albumId = url.searchParams.get('albumId');

  if (!albumId) {
    return HttpResponse.json(
      { message: 'albumId is required' },
      { status: 400 },
    );
  }

+ const parsedAlbumId = Number(albumId);
+ if (Number.isNaN(parsedAlbumId)) {
+   return HttpResponse.json(
+     { message: 'albumId must be a valid number' },
+     { status: 400 },
+   );
+ }

  const album = photoListMockData.albums?.find(
-   (a) => a.id === Number(albumId),
+   (a) => a.id === parsedAlbumId,
  );

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.

feat: 메인 지도 화면

3 participants