[Feat] : 카카오 지도 기반 노래방 위치 검색 및 즐겨찾기 기능 추가 (#224)#225
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
/describe |
|
/review |
|
/improve |
Code Review by Qodo
Context used✅ Tickets:
🎫 지도 API를 사용한 노래방 위치 검색 및 즐겨찾기 기능 추가 1. No place search input
|
Review Summary by QodoAdd Kakao Map-based karaoke location search and favorites feature
WalkthroughsDescription• Add Kakao Map integration for karaoke location search within 1km radius • Implement favorites management system with Supabase backend • Create /map public page with geolocation and marker display • Add Map tab to footer navigation with authentication-aware favorites Diagramflowchart LR
A["Kakao Map SDK"] -->|"Initialize & Search"| B["useKakaoMap Hook"]
B -->|"Display Markers"| C["KakaoMap Component"]
C -->|"Toggle Favorite"| D["Favorites API Route"]
D -->|"CRUD Operations"| E["Supabase karaoke_favorites"]
C -->|"Query Favorites"| F["karaokeQuery Hooks"]
F -->|"Cache Management"| G["React Query"]
H["Map Page"] -->|"Render"| C
I["Footer Navigation"] -->|"Link to"| H
File Changes1. apps/web/src/app/api/karaoke/favorites/route.ts
|
Code Review by Qodo
Context used✅ Tickets:
🎫 지도 API를 사용한 노래방 위치 검색 및 즐겨찾기 기능 추가 1. Kakao Places called in browser
|
| const searchNearbyKaraoke = useCallback((map: any, lat: number, lng: number) => { | ||
| const ps = new window.kakao.maps.services.Places(); | ||
| const center = new window.kakao.maps.LatLng(lat, lng); | ||
| const infowindow = new window.kakao.maps.InfoWindow({ zIndex: 1 }); | ||
|
|
||
| ps.keywordSearch( | ||
| '노래방', | ||
| (data: KakaoPlace[], status: string) => { | ||
| if (status !== window.kakao.maps.services.Status.OK) return; | ||
|
|
||
| data.forEach(place => { | ||
| const position = new window.kakao.maps.LatLng(Number(place.y), Number(place.x)); | ||
| const marker = new window.kakao.maps.Marker({ map, position }); | ||
|
|
||
| window.kakao.maps.event.addListener(marker, 'click', () => { | ||
| infowindow.close(); | ||
| setSelectedPlace(place); | ||
| infowindow.setContent( | ||
| `<div style="padding:6px 10px;font-size:13px;font-weight:bold;">${place.place_name}</div>`, | ||
| ); | ||
| infowindow.open(map, marker); | ||
| }); | ||
| }); | ||
| }, | ||
| { | ||
| location: center, | ||
| radius: 1000, | ||
| sort: window.kakao.maps.services.SortBy.DISTANCE, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
1. Kakao places called in browser 📘 Rule violation ⛨ Security
Client code directly loads the Kakao Maps SDK and calls Places().keywordSearch() in the browser, which is an external API interaction outside /api/*. This violates the BFF requirement and bypasses centralized auth/validation and request controls.
Agent Prompt
## Issue description
Browser/client code is directly calling an external API (Kakao Places via JS SDK) instead of routing through internal `/api/*` endpoints (BFF pattern).
## Issue Context
`useKakaoMap` uses `new window.kakao.maps.services.Places()` and `keywordSearch()`, and `KakaoMap` loads Kakao's SDK script in the client.
## Fix Focus Areas
- apps/web/src/hooks/useKakaoMap.ts[20-49]
- apps/web/src/components/KakaoMap.tsx[53-56]
- apps/web/src/app/api/[...]/route.ts[1-200]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <Script | ||
| src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_KEY}&libraries=services&autoload=false`} | ||
| onLoad={() => setIsScriptLoaded(true)} | ||
| /> |
There was a problem hiding this comment.
2. Claude.md missing map docs 📘 Rule violation ⚙ Maintainability
The PR introduces a new public route (/map) and a required env var (NEXT_PUBLIC_KAKAO_MAP_KEY) but does not update apps/web/CLAUDE.md to reflect these changes. This creates onboarding/setup drift and can cause local runs to fail or behave unexpectedly.
Agent Prompt
## Issue description
`apps/web/CLAUDE.md` is outdated relative to the PR changes (new public route and new required env var).
## Issue Context
- `/map` was added to public allow paths.
- `NEXT_PUBLIC_KAKAO_MAP_KEY` is now required for Kakao map loading.
## Fix Focus Areas
- apps/web/CLAUDE.md[59-62]
- apps/web/CLAUDE.md[89-98]
- apps/web/src/auth.tsx[13-16]
- apps/web/src/components/KakaoMap.tsx[53-56]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| window.kakao.maps.event.addListener(marker, 'click', () => { | ||
| infowindow.close(); | ||
| setSelectedPlace(place); | ||
| infowindow.setContent( | ||
| `<div style="padding:6px 10px;font-size:13px;font-weight:bold;">${place.place_name}</div>`, | ||
| ); | ||
| infowindow.open(map, marker); | ||
| }); |
There was a problem hiding this comment.
3. Infowindow xss risk 🐞 Bug ⛨ Security
useKakaoMap()이 Kakao InfoWindow content를 HTML 문자열로 만들면서 외부 데이터인 place.place_name을 그대로 보간해 DOM에 주입합니다. 악의적인 place_name 값이 들어오면 InfoWindow 영역에서 스크립트/HTML 실행(XSS)로 이어질 수 있습니다.
Agent Prompt
### Issue description
`infowindow.setContent()`에 외부 데이터(`place.place_name`)를 HTML 문자열로 직접 삽입해 XSS가 가능해집니다.
### Issue Context
Kakao Maps InfoWindow는 문자열 content를 HTML로 렌더링합니다. 따라서 place_name을 HTML-escape 하거나, 문자열 대신 DOM 노드를 만들어 `textContent`로 넣어야 합니다.
### Fix Focus Areas
- apps/web/src/hooks/useKakaoMap.ts[34-41]
### Suggested approach
- `const div = document.createElement('div'); div.style...; div.textContent = place.place_name; infowindow.setContent(div);` 처럼 DOM node 기반으로 설정
- 또는 최소한 HTML escape 유틸을 만들어 `${escapeHtml(place.place_name)}` 적용
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const searchNearbyKaraoke = useCallback((map: any, lat: number, lng: number) => { | ||
| const ps = new window.kakao.maps.services.Places(); | ||
| const center = new window.kakao.maps.LatLng(lat, lng); | ||
| const infowindow = new window.kakao.maps.InfoWindow({ zIndex: 1 }); | ||
|
|
||
| ps.keywordSearch( | ||
| '노래방', | ||
| (data: KakaoPlace[], status: string) => { | ||
| if (status !== window.kakao.maps.services.Status.OK) return; | ||
|
|
||
| data.forEach(place => { | ||
| const position = new window.kakao.maps.LatLng(Number(place.y), Number(place.x)); | ||
| const marker = new window.kakao.maps.Marker({ map, position }); | ||
|
|
||
| window.kakao.maps.event.addListener(marker, 'click', () => { | ||
| infowindow.close(); | ||
| setSelectedPlace(place); | ||
| infowindow.setContent( | ||
| `<div style="padding:6px 10px;font-size:13px;font-weight:bold;">${place.place_name}</div>`, | ||
| ); | ||
| infowindow.open(map, marker); | ||
| }); | ||
| }); | ||
| }, | ||
| { | ||
| location: center, | ||
| radius: 1000, | ||
| sort: window.kakao.maps.services.SortBy.DISTANCE, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
1. No place search input 📎 Requirement gap ≡ Correctness
The new /map UI only performs a fixed keyword search ('노래방') around the current/fallback
location and does not provide a user-driven place search input/results UI. This fails the
requirement for a location search page that includes place search functionality.
Agent Prompt
## Issue description
`/map` currently has no user-driven place search UI (input/results); it only runs a fixed keyword search (`'노래방'`).
## Issue Context
Compliance requires a location search page with map display + place search UI/logic (search input and results display).
## Fix Focus Areas
- apps/web/src/components/KakaoMap.tsx[51-60]
- apps/web/src/hooks/useKakaoMap.ts[20-49]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| {favorites.length > 0 && ( | ||
| <div className="bg-card rounded-lg border p-4"> | ||
| <p className="mb-2 font-semibold">즐겨찾기 ({favorites.length})</p> | ||
| <ul className="flex flex-col gap-2"> | ||
| {favorites.map(fav => ( | ||
| <li key={fav.place_id} className="flex items-center justify-between text-sm"> | ||
| <span>{fav.place_name}</span> | ||
| <Button | ||
| size="sm" | ||
| variant="ghost" | ||
| className="text-muted-foreground h-auto px-2 py-1 text-xs" | ||
| onClick={() => | ||
| deleteFavorite.mutate(fav.place_id, { | ||
| onSuccess: () => toast.success(`${fav.place_name} 삭제`), | ||
| }) | ||
| } | ||
| > | ||
| 삭제 | ||
| </Button> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
2. Karaoke favorites page missing 📎 Requirement gap ≡ Correctness
There is no dedicated page/route that displays the user's saved karaoke favorites; favorites are only shown inline within the map component. This does not meet the requirement for a standalone favorites list page.
Agent Prompt
## Issue description
A dedicated karaoke favorites list page/route is missing; favorites are only shown within the map UI.
## Issue Context
Compliance requires a standalone page that fetches and displays favorites stored in Supabase (via the internal `/api/karaoke/favorites` route).
## Fix Focus Areas
- apps/web/src/app/map/page.tsx[1-12]
- apps/web/src/components/KakaoMap.tsx[81-104]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| window.kakao.maps.event.addListener(marker, 'click', () => { | ||
| infowindow.close(); | ||
| setSelectedPlace(place); | ||
| infowindow.setContent( | ||
| `<div style="padding:6px 10px;font-size:13px;font-weight:bold;">${place.place_name}</div>`, | ||
| ); | ||
| infowindow.open(map, marker); | ||
| }); |
There was a problem hiding this comment.
3. Kakao infowindow xss 🐞 Bug ⛨ Security
useKakaoMap()에서 외부(Kakao Places API)로부터 온 place_name을 HTML 문자열에 그대로 삽입해 InfoWindow.setContent()에 넘겨 DOM 기반 XSS가 가능합니다. 악성 place_name이 포함되면 스크립트 실행/세션 탈취 등으로 이어질 수 있습니다.
Agent Prompt
### Issue description
`InfoWindow.setContent()`에 전달하는 문자열에 `place.place_name`을 그대로 삽입하고 있어, 외부 데이터 기반 XSS가 가능합니다.
### Issue Context
Kakao Places API 응답은 신뢰할 수 없는 입력으로 취급해야 합니다. `setContent()`에는 HTML 문자열 대신 DOM 노드를 사용하거나, 반드시 HTML escaping/sanitization을 적용해야 합니다.
### Fix Focus Areas
- apps/web/src/hooks/useKakaoMap.ts[25-41]
### Suggested fix
- 가능하면 DOM 엘리먼트를 만들어 `textContent`로 값을 세팅한 뒤 `setContent(element)`로 전달하세요.
- 예: `const el = document.createElement('div'); el.style...; el.textContent = place.place_name; infowindow.setContent(el);`
- HTML 문자열을 유지해야 한다면 최소한 `place_name`을 HTML escape 처리(예: `& < > " '` 변환)하거나, 프로젝트 정책에 맞는 sanitizer(예: DOMPurify)를 적용하세요.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
PR Description updated to latest commit (cdebcc6)
|
User description
📌 PR 제목
[Feat] : 카카오 지도 기반 노래방 위치 검색 및 즐겨찾기 기능 추가
📌 변경 사항
useKakaoMap커스텀 훅으로 초기화 로직 캡슐화)/map페이지 추가 — 현재 위치 기반 반경 1km 내 노래방 마커 표시GET/POST/DELETE /api/karaoke/favorites)karaoke_favorites테이블 연동 (CRUD)지도탭 추가,FooterKey에MAP추가/map공개 경로 추가 (비로그인 시 지도 조회 가능, 즐겨찾기는 401 처리)💬 추가 참고 사항
karaoke_favorites테이블 수동 생성 필요 (route.ts 상단 SQL 주석 참고).env.local에NEXT_PUBLIC_KAKAO_MAP_KEY추가 필요PR Type
Enhancement
Description
Add Kakao Map integration for karaoke location search within 1km radius
Implement favorites management system with Supabase backend
Create
/mappublic page with geolocation and marker displayAdd map navigation tab to footer with authentication-based access control
Diagram Walkthrough
File Walkthrough
9 files
Karaoke favorites API endpoints with Supabase integrationCustom hook for Kakao Map initialization and karaoke searchAPI client functions for favorites CRUD operationsReact Query hooks for karaoke favorites data managementTypeScript interfaces for karaoke and Kakao Place dataMap component with place selection and favorites UINew map page route for karaoke location discoveryAdd MAP footer key type to navigation storeAdd map navigation tab to footer menu1 files
Add /map to public allowed paths list