✨Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결#109
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAI 기반 추천 코스 뮤테이션과 세션 조회가 도입되었고, 맵 결과 UI가 공유 컴포넌트로 재구성되었으며 Kakao 지도 훅·타입·SDK 로드 유틸, 관련 API 클라이언트·타입·상수·아이콘이 추가·수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant MapPage as /map
participant SelectGroup as CourseSelectGroup
participant Input as CourseInputSection
participant Recommend as useRecommendCourse
participant API as /api/courses/recommend
participant ResultPage as /map/result
participant SessionHook as useCourseSession
participant Backend as /api/courses/session/{id}
User->>SelectGroup: 목적/체류/이동 선택
User->>Input: mustVisit 입력 및 Next
MapPage->>Recommend: mutate({ travelPurpose, stayDuration, transportation, mustVisitPlace, userLatitude?, userLongitude? })
Recommend->>API: POST /api/courses/recommend
API-->>Recommend: { sessionId, places, ... }
Recommend-->>MapPage: 성공(sessionId)
MapPage->>ResultPage: navigate(/map/result?sessionId=...)
ResultPage->>SessionHook: useCourseSession(sessionId)
SessionHook->>Backend: GET /api/courses/session/{id}
Backend-->>SessionHook: session payload
SessionHook-->>ResultPage: places, travelPurpose, ...
ResultPage->>ResultPage: render TagGroup / ResultMap / ResultList / FullMap
sequenceDiagram
participant FullMap as FullMap 컴포넌트
participant Session as useCourseSession
participant KakaoHook as useKakaoCourseMap
participant KakaoSDK as Kakao SDK
participant Bubble as LocationBubble
FullMap->>Session: 요청(sessionId)
Session-->>FullMap: places
FullMap->>KakaoHook: init(mapRef, places, enableClick:true, onPinClick)
KakaoHook->>KakaoSDK: 지도 초기화 및 오버레이 생성
User->>KakaoSDK: 핀 클릭
KakaoHook->>FullMap: onPinClick(place, point)
FullMap->>Bubble: selectedPlace 표시
User->>Bubble: 클릭 -> 네비게이션 (/map/location/{placeId})
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message. |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/hooks/kakaoMap/useKakaoMap.ts (1)
11-33: 사용되지 않는locations매개변수를 제거하세요.
locations매개변수가 의존성 배열에 포함되어 있지만(Line 33) 실제로는 사용되지 않고 있어, 불필요한 재렌더링을 유발할 수 있습니다. 리팩토링으로 마커 로직이 제거된 것으로 보이는데, 이 훅 자체가useKakaoCourseMap으로 대체되었다면 deprecated 표시를 하거나 제거를 고려하세요.export function useKakaoMap( mapRef: React.RefObject<HTMLDivElement | null>, - locations: Location[], ) { useEffect(() => { if (!mapRef.current) return; const initMap = () => { if (!window.kakao || !window.kakao.maps) { console.warn('Kakao Maps SDK not loaded yet'); return; } window.kakao.maps.load(() => { new window.kakao.maps.Map(mapRef.current!, { center: new window.kakao.maps.LatLng(37.498, 126.783), level: 5, }); }); }; loadKakaoSdk(initMap); - }, [mapRef, locations]); + }, [mapRef]); }
🧹 Nitpick comments (2)
src/shared/api/course/types/recommendTypes.ts (1)
10-18: CoursePlace 타입 중복 정의로 인한 혼란 가능성
src/shared/api/course/types/courseSettion.ts에서도CoursePlace를 내보내고 있는데, 이번 파일의 정의는 null 허용 여부 등이 달라 두 타입이 혼용되면 타입 불일치나 런타임 누락을 유발할 수 있습니다. 공통 타입을 재사용하거나, 추천 API 전용이라면 이름을 분리·확장해 혼선을 줄여 주세요.가능한 한 기존 타입을 가져와 확장하는 방향을 고려해 주세요:
-import type { CoursePlace } from '@/shared/api/course/types/courseSettion'; - -export interface CoursePlace { - placeId: number | null; - name: string; - address: string; - latitude: number; - longitude: number; - description: string; - placeImg: string | null; -} +import type { CoursePlace as SessionCoursePlace } from '@/shared/api/course/types/courseSettion'; + +export type CoursePlace = SessionCoursePlace & { + placeId: SessionCoursePlace['placeId'] | null; + placeImg: SessionCoursePlace['placeImg'] | null; +};src/shared/components/map/result/components/FullMap.tsx (1)
15-24: 사용되지 않는 selectedLoc 상태 정리 제안
selectedLoc을 업데이트하지만 실제로는 어디에서도 활용되지 않아 불필요한 렌더링만 발생합니다. 지도 중심 이동 등에 쓸 계획이 없다면 상태를 제거해 주세요.- const [selectedLoc, setSelectedLoc] = useState<{ lat: number; lng: number } | null>(null); + const [, setSelectedLoc] = useState<{ lat: number; lng: number } | null>(null);사용하지 않는다면 위 상태 자체를 삭제해도 됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/shared/icons/source/RedMapPin.svgis excluded by!**/*.svg
📒 Files selected for processing (25)
src/pages/map/index.tsx(2 hunks)src/pages/map/result/Map.tsx(2 hunks)src/pages/map/result/components/FullMap.tsx(0 hunks)src/pages/map/result/components/ResultList.tsx(0 hunks)src/pages/map/result/components/ResultMap.tsx(0 hunks)src/pages/map/result/index.tsx(3 hunks)src/shared/api/course/api/getCourseSettion.ts(1 hunks)src/shared/api/course/api/postRecommendCourse.ts(1 hunks)src/shared/api/course/types/courseSettion.ts(1 hunks)src/shared/api/course/types/recommendTypes.ts(1 hunks)src/shared/components/map/components/CourseInputSection.tsx(3 hunks)src/shared/components/map/components/CourseSelectGroup.tsx(1 hunks)src/shared/components/map/components/CourseSelectSection.tsx(0 hunks)src/shared/components/map/result/components/FullMap.tsx(1 hunks)src/shared/components/map/result/components/ResultList.tsx(1 hunks)src/shared/components/map/result/components/ResultMap.tsx(1 hunks)src/shared/components/map/result/components/TagGroup.tsx(1 hunks)src/shared/constants/course/courseOptions.ts(1 hunks)src/shared/hooks/course/useCourseSession.ts(1 hunks)src/shared/hooks/course/useRecommendCourse.ts(1 hunks)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts(1 hunks)src/shared/hooks/kakaoMap/useKakaoMap.ts(1 hunks)src/shared/icons/iconNames.ts(1 hunks)src/shared/icons/index.ts(1 hunks)src/shared/utils/loadKakaoSdk.ts(1 hunks)
💤 Files with no reviewable changes (4)
- src/pages/map/result/components/ResultList.tsx
- src/shared/components/map/components/CourseSelectSection.tsx
- src/pages/map/result/components/FullMap.tsx
- src/pages/map/result/components/ResultMap.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.
Applied to files:
src/pages/map/result/Map.tsxsrc/pages/map/index.tsxsrc/pages/map/result/index.tsx
🧬 Code graph analysis (17)
src/pages/map/result/Map.tsx (1)
src/shared/components/map/result/components/FullMap.tsx (1)
FullMap(11-47)
src/shared/api/course/api/getCourseSettion.ts (2)
src/shared/api/instance.ts (1)
apiAuth(20-23)src/shared/api/course/types/courseSettion.ts (1)
CourseSessionResponse(11-24)
src/shared/utils/loadKakaoSdk.ts (1)
src/shared/hooks/kakao/useKakaoMap.ts (3)
map(24-44)mapRef(15-48)useKakaoMap(11-49)
src/shared/components/map/components/CourseSelectGroup.tsx (4)
src/shared/components/map/components/CourseSelectSection.tsx (1)
CourseSelectSection(11-62)src/shared/constants/course/courseOptions.ts (3)
purposes(1-5)stays(7-11)moves(13-17)src/pages/map/components/CourseSelectSection.tsx (4)
CourseSelectSectionProps(5-10)CourseSelectSection(12-63)div(43-52)onSelect(49-49)src/shared/hooks/useCourseSelection.ts (1)
useCourseSelection(3-16)
src/shared/hooks/course/useRecommendCourse.ts (2)
src/shared/api/course/types/recommendTypes.ts (2)
RecommendCourseResponse(20-30)RecommendCourseRequest(1-8)src/shared/api/course/api/postRecommendCourse.ts (1)
postRecommendCourse(7-13)
src/shared/components/map/result/components/ResultMap.tsx (1)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
useKakaoCourseMap(13-62)
src/shared/api/course/types/recommendTypes.ts (1)
src/shared/api/course/types/courseSettion.ts (1)
CoursePlace(1-9)
src/shared/components/map/result/components/ResultList.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
src/shared/hooks/course/useCourseSession.ts (2)
src/shared/api/course/types/courseSettion.ts (1)
CourseSessionResponse(11-24)src/shared/api/course/api/getCourseSettion.ts (1)
getCourseSession(4-9)
src/shared/api/course/types/courseSettion.ts (1)
src/shared/api/course/types/recommendTypes.ts (1)
CoursePlace(10-18)
src/shared/api/course/api/postRecommendCourse.ts (2)
src/shared/api/course/types/recommendTypes.ts (2)
RecommendCourseRequest(1-8)RecommendCourseResponse(20-30)src/shared/api/instance.ts (1)
apiWithToken(14-17)
src/shared/components/map/result/components/FullMap.tsx (2)
src/shared/hooks/course/useCourseSession.ts (1)
useCourseSession(5-15)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
useKakaoCourseMap(13-62)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
src/shared/utils/loadKakaoSdk.ts (1)
loadKakaoSdk(1-16)
src/pages/map/index.tsx (4)
src/shared/hooks/course/useRecommendCourse.ts (1)
useRecommendCourse(10-14)src/shared/hooks/useCourseSelection.ts (1)
useCourseSelection(3-16)src/shared/components/map/components/CourseSelectGroup.tsx (1)
CourseSelectGroup(14-44)src/shared/components/map/components/CourseInputSection.tsx (1)
CourseInputSection(12-72)
src/shared/components/map/components/CourseInputSection.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
src/shared/components/map/result/components/TagGroup.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
src/pages/map/result/index.tsx (3)
src/shared/hooks/course/useCourseSession.ts (1)
useCourseSession(5-15)src/shared/components/map/result/components/ResultList.tsx (1)
ResultList(10-40)src/shared/components/map/result/components/ResultMap.tsx (1)
ResultMap(11-41)
🪛 ast-grep (0.39.9)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts
[warning] 38-42: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 38-42: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (6)
src/shared/icons/iconNames.ts (1)
1-1: 자동 생성 파일 수정 및 일관성 확인
index.ts와 마찬가지로 자동 생성 파일로 표시되어 있으나 수동 수정된 것으로 보입니다.변경 사항 자체는 올바르게 구현되었습니다:
- "RedMapPin"이 알파벳 순서에 맞게 배치됨
IconName타입에 자동으로 포함됨- 기존 패턴과 일관성 유지
index.ts와 함께 아이콘 추가 프로세스 및 SVG 파일 존재 여부를 확인해주세요.Also applies to: 22-22
src/shared/icons/index.ts (1)
1-1: 원본 리뷰 의견이 부정확합니다.이것은 자동 생성 파일의 수동 수정이 아닙니다. 올바른 프로세스를 따릅니다:
RedMapPin.svg파일이src/shared/icons/source/디렉토리에 추가됨 (commit 6296b1a)generate-icon-list.ts스크립트가 자동으로 SVG 파일들을 검색- 스크립트가
index.ts파일을 완전히 재생성 (동일 commit 6296b1a)- 결과 파일에 자동으로 import 라인이 추가됨
생성된 파일의 구조와 git 히스토리 모두 의도된 자동화 워크플로우가 올바르게 실행되었음을 확인합니다. 추가 조치가 필요하지 않습니다.
Likely an incorrect or invalid review comment.
src/shared/hooks/course/useRecommendCourse.ts (1)
10-14: LGTM!
useMutation을 간결하게 래핑한 깔끔한 구현입니다. 에러 처리와 성공 콜백을 소비자에게 위임하는 것은 재사용 가능한 훅으로서 적절한 설계입니다.src/shared/hooks/course/useCourseSession.ts (1)
10-13: React Query 캐싱 전략이 적절합니다.5분 staleTime과 포커스/재연결 시 refetch 설정이 코스 세션 데이터의 특성에 잘 맞습니다. 세션 데이터는 자주 변경되지 않으므로 불필요한 재요청을 줄이면서도 적절한 갱신 주기를 유지하고 있습니다.
src/shared/components/map/result/components/ResultList.tsx (1)
20-32: 리뷰 코멘트는 부정확합니다. placeId는 항상 유효한 숫자(number) 타입입니다.코드에서 사용 중인
CoursePlace타입은courseSettion.ts에서 정의된 것으로,placeId: number로 명시되어 있습니다. 따라서placeId가 null일 수 없으며,place.placeId ?? place.name표현식의 fallback은 절대 실행되지 않습니다. 현재 구현은 안전하고 정상 동작합니다.Likely an incorrect or invalid review comment.
src/shared/components/map/components/CourseInputSection.tsx (1)
5-72: 제어 컴포넌트 패턴과 로딩 상태 구현이 잘 되어 있습니다.
- 제어 컴포넌트 패턴이 올바르게 적용되어
value와onChange로 상태를 부모에서 관리합니다.isLoading상태에 따라 입력란과 버튼이 적절히 비활성화됩니다.- 로딩 시 시각적 피드백(투명도, 커서, 아이콘 색상)이 명확하게 제공됩니다.
- 접근성을 위한 aria 레이블과 설명이 잘 구현되어 있습니다.
| import { apiAuth } from "@/shared/api/instance"; | ||
| import type { CourseSessionResponse } from "@/shared/api/course/types/courseSettion"; | ||
|
|
||
| export const getCourseSession = async (sessionId: string) => { | ||
| const { data } = await apiAuth.get<CourseSessionResponse>( | ||
| `/api/courses/session/${sessionId}`, | ||
| ); | ||
| return data.result; | ||
| }; |
There was a problem hiding this comment.
파일명 오타를 수정하세요.
파일명이 getCourseSettion.ts로 되어 있는데 "Session"이 올바른 철자입니다. 타입 파일(courseSettion.ts)도 동일한 오타가 있다면 함께 수정이 필요합니다.
파일명 변경:
getCourseSettion.ts→getCourseSession.tscourseSettion.ts→courseSession.ts(타입 파일도 확인 필요)
모든 import 경로도 업데이트해야 합니다.
🤖 Prompt for AI Agents
In src/shared/api/course/api/getCourseSettion.ts lines 1-9: the filename and
referenced type file contain a spelling error ("Settion" → "Session"); rename
the file to getCourseSession.ts and rename the type file courseSettion.ts to
courseSession.ts (if present), then update all import paths and any references
throughout the repo to use the corrected filenames and exported type names
(ensure the import path "@/shared/api/course/types/courseSession" and any named
exports are adjusted accordingly).
| export interface CoursePlace { | ||
| placeId: number; | ||
| name: string; | ||
| address: string; | ||
| latitude: number; | ||
| longitude: number; | ||
| description: string; | ||
| placeImg: string; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
CoursePlace 인터페이스 중복 및 타입 불일치를 해결하세요.
CoursePlace 인터페이스가 src/shared/api/course/types/recommendTypes.ts에도 정의되어 있지만, nullability가 다릅니다:
- 현재 파일:
placeId: number,placeImg: string - recommendTypes.ts:
placeId: number | null,placeImg: string | null
서버가 null을 반환할 경우 런타임 에러가 발생할 수 있습니다. 두 인터페이스를 하나로 통합하고 올바른 nullability를 적용하세요.
다음 스크립트로 실제 API 응답을 확인하여 null 가능 여부를 검증하세요:
🏁 Script executed:
#!/bin/bash
# Description: Check API response structure in related files
# Find API call implementations
rg -n -A 10 "getCourseSettion|getCourseSession" --type=ts --type=tsx
# Check for null handling in related hooks
rg -n "CoursePlace\[" --type=ts --type=tsxLength of output: 55
🏁 Script executed:
#!/bin/bash
# Search for CoursePlace interface definitions across codebase
rg -n "interface CoursePlace" --type=ts
# Search for CoursePlace type definitions
rg -n "type CoursePlace" --type=ts
# Check recommendTypes.ts for CoursePlace definition
rg -n -A 10 "CoursePlace" src/shared/api/course/types/recommendTypes.ts
# Find files that import CoursePlace
rg -n "import.*CoursePlace" --type=ts
# Check the actual file mentioned in review
cat src/shared/api/course/types/courseSettion.tsLength of output: 1394
CoursePlace 인터페이스의 중복 정의 및 타입 불일치 해결 필요
CoursePlace 인터페이스가 두 개 파일에 서로 다른 정의로 존재합니다:
- src/shared/api/course/types/courseSettion.ts (현재 파일):
placeId: number,placeImg: string - src/shared/api/course/types/recommendTypes.ts:
placeId: number | null,placeImg: string | null
현재 courseSettion.ts의 비-nullable 버전을 세 파일(useKakaoCourseMap.ts, ResultList.tsx, ResultMap.tsx)이 import하고 있으며, recommendTypes.ts의 nullable 버전은 API 응답 타입에서 사용 중입니다.
API가 null 값을 반환할 경우 타입 계약 위반으로 인한 런타임 에러가 발생할 수 있습니다. 올바른 nullability를 결정한 후 단일 정의로 통합하고, 모든 import를 해당 파일로 통일하세요.
🤖 Prompt for AI Agents
In src/shared/api/course/types/courseSettion.ts lines 1-9: the CoursePlace
interface conflicts with the nullable version in
src/shared/api/course/types/recommendTypes.ts and can cause runtime errors when
API returns nulls; choose the API-backed nullable shape (placeId: number | null,
placeImg: string | null), consolidate the single CoursePlace definition into one
canonical file (prefer recommendTypes.ts), remove the duplicate file, and update
all imports in useKakaoCourseMap.ts, ResultList.tsx, and ResultMap.tsx to import
CoursePlace from the unified file so all code uses the same nullable types.
| export interface CoursePlace { | ||
| placeId: number; | ||
| name: string; | ||
| address: string; | ||
| latitude: number; | ||
| longitude: number; | ||
| description: string; | ||
| placeImg: string; | ||
| } | ||
|
|
||
| export interface CourseSessionResponse { | ||
| code: string; | ||
| message: string; | ||
| isSuccess: boolean; | ||
| result: { | ||
| memberId: number; | ||
| createdAt: string; | ||
| travelPurpose: string; | ||
| stayDuration: string; | ||
| transportation: string; | ||
| mustVisitPlace?: string; | ||
| places: CoursePlace[]; | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
파일명 오타를 수정하세요.
파일명이 courseSettion.ts로 되어 있는데, 올바른 철자는 courseSession.ts입니다. 이 오타는 모든 import 문에 영향을 미칩니다.
다음 스크립트를 실행하여 이 파일을 import하는 모든 위치를 확인하세요:
🏁 Script executed:
#!/bin/bash
# Description: Find all imports of the misspelled filename
# Search for imports of courseSettion
rg -n "courseSettion" --type=ts --type=tsxLength of output: 27
🏁 Script executed:
#!/bin/bash
# Description: Search for misspelled filename and verify file existence
echo "=== Searching for all references to 'courseSettion' ==="
rg -n "courseSettion"
echo ""
echo "=== Searching for references to 'courseSession' ==="
rg -n "courseSession"
echo ""
echo "=== Finding TypeScript files with 'course' in the name ==="
fd -e ts -e tsx "course" --type fLength of output: 1463
🏁 Script executed:
#!/bin/bash
# Description: Check the actual file at the mentioned path
echo "=== Checking if file exists at src/shared/api/course/types/courseSettion.ts ==="
if [ -f "src/shared/api/course/types/courseSettion.ts" ]; then
echo "File EXISTS at: src/shared/api/course/types/courseSettion.ts"
echo "=== First 10 lines of the file ==="
head -10 "src/shared/api/course/types/courseSettion.ts"
else
echo "File NOT FOUND at: src/shared/api/course/types/courseSettion.ts"
fi
echo ""
echo "=== Checking corrected filename ==="
if [ -f "src/shared/api/course/types/courseSession.ts" ]; then
echo "File EXISTS at: src/shared/api/course/types/courseSession.ts"
else
echo "File NOT FOUND at: src/shared/api/course/types/courseSession.ts"
fi
echo ""
echo "=== Listing all files in course/types directory ==="
ls -la "src/shared/api/course/types/" 2>/dev/null || echo "Directory not found"Length of output: 741
파일명 오타를 수정하세요: courseSettion.ts → courseSession.ts
현재 파일명이 courseSettion.ts로 되어 있는데, 올바른 철자는 courseSession.ts입니다. 이 오타로 인해 5개 파일의 import 문과 관련 API 함수명도 함께 수정해야 합니다.
수정이 필요한 파일들:
src/shared/api/course/types/courseSettion.ts→courseSession.ts로 파일명 변경src/shared/api/course/api/getCourseSettion.ts→getCourseSession.ts로 함수명 변경src/shared/hooks/course/useCourseSession.ts:3- import 경로 수정src/shared/hooks/kakaoMap/useKakaoCourseMap.ts:5- import 경로 수정src/shared/components/map/result/components/ResultMap.tsx:4- import 경로 수정src/shared/components/map/result/components/ResultList.tsx:4- import 경로 수정
🤖 Prompt for AI Agents
In src/shared/api/course/types/courseSettion.ts lines 1-24: the file name is
misspelled as courseSettion.ts; rename the file to courseSession.ts, update the
exported type import paths and any references; also rename the related API file
src/shared/api/course/api/getCourseSettion.ts to getCourseSession.ts and update
its exported function name accordingly; then update imports in
src/shared/hooks/course/useCourseSession.ts (line 3),
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (line 5),
src/shared/components/map/result/components/ResultMap.tsx (line 4), and
src/shared/components/map/result/components/ResultList.tsx (line 4) to point to
the new courseSession.ts path and new getCourseSession export; ensure TypeScript
references/paths and any barrel/index exports are updated and run type-check to
fix any broken imports.
| { id: '데이트', label: '데이트' }, | ||
| { id: '가족여행', label: '가족여행' }, | ||
| { id: '우정여행', label: '우정여행' }, | ||
| ]; | ||
|
|
||
| export const stays = [ | ||
| { id: 'one', label: '당일치기' }, | ||
| { id: 'two', label: '1박2일' }, | ||
| { id: 'three', label: '2박3일' }, | ||
| { id: '당일치기', label: '당일치기' }, | ||
| { id: '1박2일', label: '1박2일' }, | ||
| { id: '2박3일', label: '2박3일' }, | ||
| ]; | ||
|
|
||
| export const moves = [ | ||
| { id: 'walk', label: '도보' }, | ||
| { id: 'transit', label: '대중교통' }, | ||
| { id: 'car', label: '자가차' }, | ||
| { id: '도보', label: '도보' }, | ||
| { id: '대중교통', label: '대중교통' }, | ||
| { id: '자가차', label: '자가차' }, |
There was a problem hiding this comment.
API 전송용 id 값 변경으로 인한 호환성 위험
이전에는 id 값이 date, family, friends 등 영문 코드라 백엔드와 맞춰 통신했는데, 지금은 한글이 그대로 전송됩니다. 서버가 아직 영문 코드를 기대하면 추천 API 호출이 즉시 실패할 수 있으니 계약 변경 여부를 확인하거나, 화면 표시용 라벨과 전송용 코드를 분리하는 편이 안전합니다.
-export const purposes = [
- { id: '데이트', label: '데이트' },
- { id: '가족여행', label: '가족여행' },
- { id: '우정여행', label: '우정여행' },
-];
+export const purposes = [
+ { id: 'date', label: '데이트' },
+ { id: 'family', label: '가족여행' },
+ { id: 'friends', label: '우정여행' },
+];
export const stays = [
- { id: '당일치기', label: '당일치기' },
- { id: '1박2일', label: '1박2일' },
- { id: '2박3일', label: '2박3일' },
+ { id: 'one', label: '당일치기' },
+ { id: 'two', label: '1박2일' },
+ { id: 'three', label: '2박3일' },
];
export const moves = [
- { id: '도보', label: '도보' },
- { id: '대중교통', label: '대중교통' },
- { id: '자가차', label: '자가차' },
+ { id: 'walk', label: '도보' },
+ { id: 'transit', label: '대중교통' },
+ { id: 'car', label: '자가차' },
];Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/shared/constants/course/courseOptions.ts around lines 2 to 16, the
current objects use Korean strings as the id fields which breaks backward
compatibility with the API that expects English codes; change the shape so the
API-facing key is an English code (e.g., add/replace with a stable field like
value or code such as "date", "family", "friends", "daytrip", "1n2d", "walk",
"public_transport", "car") and keep the Korean text in label for UI display;
update all entries in the categories (including stays and moves) to include the
English API code and leave the label as-is, and ensure any consumers use the
API-facing field when sending requests (or map to it before calling the API).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
38-42: innerHTML 사용은 안전하지만 모범 사례 관점에서 개선 가능합니다.하드코딩된 SVG 문자열이므로 XSS 위험은 없지만, 정적 분석 도구가 경고를 발생시킵니다. 더 나은 방법으로는 SVG 요소를 직접 생성하거나, 템플릿 요소를 사용하는 것이 있습니다.
다음과 같이 리팩토링을 고려해보세요:
- el.innerHTML = ` - <svg width="28" height="28" viewBox="0 0 24 24"> - <use href="#icon-RedMapPin"></use> - </svg> - `; + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '28'); + svg.setAttribute('height', '28'); + svg.setAttribute('viewBox', '0 0 24 24'); + const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#icon-RedMapPin'); + svg.appendChild(use); + el.appendChild(svg);src/shared/types/kakao.d.ts (2)
10-14: Map 클래스의 타입 정의를 개선할 수 있습니다.두 가지 관찰 사항:
전역 Map 섀도잉: Biome이 경고를 발생시키지만, 이는 Kakao Maps API가 실제로
Map이라는 이름을 사용하기 때문에 불가피합니다. 외부 라이브러리의 API를 타이핑하는 것이므로 이름을 변경할 수 없습니다.느슨한 타입:
options?: object는 타입 안정성이 부족합니다. 더 구체적인 인터페이스로 개선할 수 있습니다.options 타입을 구체화하는 것을 권장합니다:
+ interface MapOptions { + center?: LatLng; + level?: number; + draggable?: boolean; + scrollwheel?: boolean; + } + class Map { - constructor(container: HTMLElement, options?: object); + constructor(container: HTMLElement, options?: MapOptions); setCenter(latlng: LatLng): void; setLevel(level: number): void; }
6-29: 향후 확장을 위해 API 커버리지를 고려하세요.현재 타입 정의는 기본적인 지도 생성 및 오버레이 기능만 포함하고 있습니다. 향후 다음 기능들이 필요할 수 있습니다:
Marker클래스 (마커 표시)InfoWindow클래스 (정보 창)Polyline/Polygon(경로선 시각화 - PR 설명에서 언급된 후속 작업)Map.getBounds(),Map.panTo()등 추가 메서드- 이벤트 리스너 관련 타입
현재 PR 범위에서는 충분하지만, 향후 기능 추가 시 타입 정의도 함께 확장하는 것을 권장합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts(1 hunks)src/shared/types/kakao.d.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
src/shared/utils/loadKakaoSdk.ts (1)
loadKakaoSdk(1-16)
🪛 ast-grep (0.39.9)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts
[warning] 37-41: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 37-41: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
🪛 Biome (2.1.2)
src/shared/types/kakao.d.ts
[error] 10-10: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (1)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
59-59: onPinClick을 의존성 배열에 포함하여 불필요한 재렌더링이 발생할 수 있습니다.부모 컴포넌트에서
onPinClick을 메모이제이션하지 않으면, 부모가 리렌더링될 때마다 이 effect가 재실행되어 지도 전체가 재생성됩니다.부모 컴포넌트에서
useCallback으로onPinClick을 감싸는 것을 권장합니다.FullMap.tsx의 경우 다음과 같이 수정하세요:const handlePinClick = useCallback((place: CoursePlace) => { setSelected(place.name); setSelectedLoc({ lat: place.latitude, lng: place.longitude }); }, []); useKakaoCourseMap(mapRef, { places: data?.places || [], enableClick: true, onPinClick: handlePinClick, });
| useEffect(() => { | ||
| if (!places?.length) return; | ||
|
|
||
| loadKakaoSdk(() => { | ||
| if (!window.kakao?.maps || !mapRef.current) return; | ||
|
|
||
| window.kakao.maps.load(() => { | ||
| const { maps } = window.kakao; | ||
| const center = new maps.LatLng(places[0].latitude, places[0].longitude); | ||
|
|
||
| const map = new maps.Map(mapRef.current!, { center, level: 6 }); | ||
| mapInstanceRef.current = map; | ||
|
|
||
| places.forEach((place) => { | ||
| const el = document.createElement('div'); | ||
| el.style.cursor = enableClick ? 'pointer' : 'default'; | ||
| el.style.display = 'flex'; | ||
| el.style.alignItems = 'center'; | ||
| el.style.justifyContent = 'center'; | ||
| el.innerHTML = ` | ||
| <svg width="28" height="28" viewBox="0 0 24 24"> | ||
| <use href="#icon-RedMapPin"></use> | ||
| </svg> | ||
| `; | ||
|
|
||
| const overlay = new maps.CustomOverlay({ | ||
| position: new maps.LatLng(place.latitude, place.longitude), | ||
| content: el, | ||
| yAnchor: 1, | ||
| }); | ||
|
|
||
| overlay.setMap(map); | ||
|
|
||
| if (enableClick && onPinClick) { | ||
| el.addEventListener('click', () => onPinClick(place)); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| }, [places, enableClick, onPinClick, mapRef]); |
There was a problem hiding this comment.
cleanup 함수 누락으로 인한 메모리 누수 위험이 있습니다.
현재 effect는 다음 리소스들을 정리하지 않습니다:
- 기존 지도 인스턴스가 파괴되지 않아 places 변경 시 새 지도가 계속 생성됨
- 생성된 오버레이들이 제거되지 않음
- 핀에 추가된 이벤트 리스너가 제거되지 않음
다음과 같이 cleanup 로직을 추가하세요:
export function useKakaoCourseMap(
mapRef: React.RefObject<HTMLDivElement | null>,
{ places, onPinClick, enableClick = false }: UseKakaoCourseMapOptions,
) {
const mapInstanceRef = useRef<kakao.maps.Map | null>(null);
+ const overlaysRef = useRef<kakao.maps.CustomOverlay[]>([]);
useEffect(() => {
if (!places?.length) return;
+ // Cleanup function
+ return () => {
+ overlaysRef.current.forEach(overlay => overlay.setMap(null));
+ overlaysRef.current = [];
+ mapInstanceRef.current = null;
+ };
loadKakaoSdk(() => {
// ... existing code ...
places.forEach((place) => {
// ... existing overlay creation ...
overlay.setMap(map);
+ overlaysRef.current.push(overlay);
if (enableClick && onPinClick) {
el.addEventListener('click', () => onPinClick(place));
}
});
});
});
}, [places, enableClick, onPinClick, mapRef]);
return mapInstanceRef.current;
}📝 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.
| useEffect(() => { | |
| if (!places?.length) return; | |
| loadKakaoSdk(() => { | |
| if (!window.kakao?.maps || !mapRef.current) return; | |
| window.kakao.maps.load(() => { | |
| const { maps } = window.kakao; | |
| const center = new maps.LatLng(places[0].latitude, places[0].longitude); | |
| const map = new maps.Map(mapRef.current!, { center, level: 6 }); | |
| mapInstanceRef.current = map; | |
| places.forEach((place) => { | |
| const el = document.createElement('div'); | |
| el.style.cursor = enableClick ? 'pointer' : 'default'; | |
| el.style.display = 'flex'; | |
| el.style.alignItems = 'center'; | |
| el.style.justifyContent = 'center'; | |
| el.innerHTML = ` | |
| <svg width="28" height="28" viewBox="0 0 24 24"> | |
| <use href="#icon-RedMapPin"></use> | |
| </svg> | |
| `; | |
| const overlay = new maps.CustomOverlay({ | |
| position: new maps.LatLng(place.latitude, place.longitude), | |
| content: el, | |
| yAnchor: 1, | |
| }); | |
| overlay.setMap(map); | |
| if (enableClick && onPinClick) { | |
| el.addEventListener('click', () => onPinClick(place)); | |
| } | |
| }); | |
| }); | |
| }); | |
| }, [places, enableClick, onPinClick, mapRef]); | |
| export function useKakaoCourseMap( | |
| mapRef: React.RefObject<HTMLDivElement | null>, | |
| { places, onPinClick, enableClick = false }: UseKakaoCourseMapOptions, | |
| ) { | |
| const mapInstanceRef = useRef<kakao.maps.Map | null>(null); | |
| const overlaysRef = useRef<kakao.maps.CustomOverlay[]>([]); | |
| useEffect(() => { | |
| if (!places?.length) return; | |
| loadKakaoSdk(() => { | |
| if (!window.kakao?.maps || !mapRef.current) return; | |
| window.kakao.maps.load(() => { | |
| const { maps } = window.kakao; | |
| const center = new maps.LatLng(places[0].latitude, places[0].longitude); | |
| const map = new maps.Map(mapRef.current!, { center, level: 6 }); | |
| mapInstanceRef.current = map; | |
| places.forEach((place) => { | |
| const el = document.createElement('div'); | |
| el.style.cursor = enableClick ? 'pointer' : 'default'; | |
| el.style.display = 'flex'; | |
| el.style.alignItems = 'center'; | |
| el.style.justifyContent = 'center'; | |
| el.innerHTML = ` | |
| <svg width="28" height="28" viewBox="0 0 24 24"> | |
| <use href="#icon-RedMapPin"></use> | |
| </svg> | |
| `; | |
| const overlay = new maps.CustomOverlay({ | |
| position: new maps.LatLng(place.latitude, place.longitude), | |
| content: el, | |
| yAnchor: 1, | |
| }); | |
| overlay.setMap(map); | |
| overlaysRef.current.push(overlay); | |
| if (enableClick && onPinClick) { | |
| el.addEventListener('click', () => onPinClick(place)); | |
| } | |
| }); | |
| }); | |
| }); | |
| return () => { | |
| overlaysRef.current.forEach(overlay => overlay.setMap(null)); | |
| overlaysRef.current = []; | |
| mapInstanceRef.current = null; | |
| }; | |
| }, [places, enableClick, onPinClick, mapRef]); | |
| } |
🧰 Tools
🪛 ast-grep (0.39.9)
[warning] 37-41: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 37-41: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
🤖 Prompt for AI Agents
In src/shared/hooks/kakaoMap/useKakaoCourseMap.ts around lines 19 to 59, the
effect creates maps, overlays and DOM event listeners but provides no cleanup,
causing memory leaks; fix by tracking created resources (map instance, overlay
objects, and any event handlers) in refs or local arrays, and return a cleanup
function from the useEffect that: removes each overlay via overlay.setMap(null),
removes all click event listeners from the created pin elements via
removeEventListener, and clears or destroys the map instance (set
mapInstanceRef.current to null and call any available destroy/cleanup method on
the map if provided by the Kakao API), ensuring you also clear stored
arrays/refs so subsequent renders create a fresh map without leaking previous
resources.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/shared/api/course/types/courseSession.ts (1)
1-9: [이전 리뷰] CoursePlace 타입 중복 문제가 아직 해결되지 않았습니다.이전 리뷰에서 지적된 critical 이슈가 여전히 남아있습니다.
CoursePlace인터페이스가src/shared/api/course/types/recommendTypes.ts에도 존재하며 nullability가 다릅니다:
- 현재 파일:
placeId: number,placeImg: string(non-nullable)- recommendTypes.ts:
placeId: number | null,placeImg: string | null(nullable)API가 null을 반환할 경우 타입 불일치로 인한 런타임 에러가 발생할 수 있습니다. 다음 중 하나를 선택하여 해결하세요:
해결 방안:
- API 응답을 확인하여 실제 nullability를 파악
- 올바른 타입 정의를 가진 하나의 파일로 통합 (recommendTypes.ts 권장)
- 중복 파일 제거 및 모든 import 경로 업데이트
#!/bin/bash # CoursePlace 타입 정의 위치와 사용처 확인 rg -n "interface CoursePlace" --type=ts # 두 타입의 실제 차이점 확인 rg -n -A 8 "interface CoursePlace" src/shared/api/course/types/courseSession.ts rg -n -A 8 "interface CoursePlace" src/shared/api/course/types/recommendTypes.tssrc/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
54-56: 이벤트 리스너 정리가 누락되어 메모리 누수가 발생할 수 있습니다.오버레이 정리는 추가되었으나(lines 62-66), 핀 요소에 추가된 클릭 이벤트 리스너는 정리되지 않습니다. 컴포넌트가 언마운트되거나
places가 변경될 때 이벤트 리스너가 메모리에 남아있게 됩니다.다음과 같이 이벤트 리스너를 추적하고 정리하세요:
export function useKakaoCourseMap( mapRef: React.RefObject<HTMLDivElement | null>, { places, onPinClick, enableClick = false }: UseKakaoCourseMapOptions, ) { const mapInstanceRef = useRef<kakao.maps.Map | null>(null); const overlaysRef = useRef<kakao.maps.CustomOverlay[]>([]); + const listenersRef = useRef<Array<{ element: HTMLElement; handler: () => void }>>([]); useEffect(() => { // ... 기존 코드 ... places.forEach((place) => { // ... 기존 오버레이 생성 코드 ... if (enableClick && onPinClick) { - el.addEventListener('click', () => onPinClick(place)); + const handler = () => onPinClick(place); + el.addEventListener('click', handler); + listenersRef.current.push({ element: el, handler }); } }); // cleanup return () => { + listenersRef.current.forEach(({ element, handler }) => { + element.removeEventListener('click', handler); + }); + listenersRef.current = []; overlaysRef.current.forEach((overlay) => overlay.setMap(null)); overlaysRef.current = []; mapInstanceRef.current = null; }; }, [places, enableClick, onPinClick, mapRef]);
🧹 Nitpick comments (1)
src/shared/utils/loadKakaoSdk.ts (1)
4-9: 환경 변수 검증이 추가되었으나 개선이 필요합니다.검증 로직이 추가되어 이전 리뷰의 critical 이슈가 해결되었습니다. 다만 환경 변수가 없을 때
console.error만 출력하고 조용히 반환하면, 호출한 컴포넌트는 SDK 로딩 실패를 인지하지 못해 무한 로딩 상태에 빠질 수 있습니다.다음 개선을 고려하세요:
if (!appKey) { - console.error('NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY is not defined'); - return; + throw new Error('NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY is not defined'); }또는 에러 상태를 호출자에게 전달하는 방식으로 변경:
-export function loadKakaoSdk(callback: () => void) { +export function loadKakaoSdk(callback: () => void, onError?: (error: Error) => void) { // ... if (!appKey) { - console.error('NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY is not defined'); - return; + const error = new Error('NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY is not defined'); + console.error(error.message); + onError?.(error); + return; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/pages/map/result/Map.tsx(2 hunks)src/shared/api/course/api/getCourseSession.ts(1 hunks)src/shared/api/course/types/courseSession.ts(1 hunks)src/shared/components/map/components/CourseSelectGroup.tsx(1 hunks)src/shared/components/map/result/components/ResultList.tsx(1 hunks)src/shared/components/map/result/components/ResultMap.tsx(1 hunks)src/shared/hooks/course/useCourseSession.ts(1 hunks)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts(1 hunks)src/shared/utils/loadKakaoSdk.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/shared/hooks/course/useCourseSession.ts
- src/pages/map/result/Map.tsx
- src/shared/components/map/result/components/ResultList.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/shared/api/course/api/getCourseSession.ts (2)
src/shared/api/instance.ts (1)
apiAuth(20-23)src/shared/api/course/types/courseSession.ts (1)
CourseSessionResponse(11-24)
src/shared/components/map/result/components/ResultMap.tsx (3)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
useKakaoCourseMap(13-70)src/shared/lib/utils.ts (1)
cn(71-73)
src/shared/components/map/components/CourseSelectGroup.tsx (2)
src/shared/components/map/components/CourseSelectSection.tsx (1)
CourseSelectSection(11-62)src/shared/constants/course/courseOptions.ts (3)
purposes(1-5)stays(7-11)moves(13-17)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (2)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/utils/loadKakaoSdk.ts (1)
loadKakaoSdk(1-22)
🪛 ast-grep (0.39.9)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts
[warning] 38-42: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 38-42: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (6)
src/shared/components/map/components/CourseSelectGroup.tsx (2)
5-12: 이전 리뷰의 타입 불일치 문제가 해결되었습니다.Props 인터페이스의 setter 함수들이 이제
(value: string | null) => void로 올바르게 정의되어 있어,CourseSelectSection의 토글 동작(선택 해제 시 null 반환)과 타입이 일치합니다.
14-44: 이전 리뷰의 null 변환 이슈가 해결되었습니다.setter 함수들을 직접 전달하여(lines 28, 34, 40) 이전에 지적된
id ?? ''변환 로직이 제거되었습니다. 이제 "선택 안 함"(null)과 빈 문자열의 의미가 명확히 구분되며,CourseSelectSection의 토글 동작이 올바르게 보존됩니다.컴포넌트 구조도 깔끔하게 3개의 선택 섹션을 그룹화하여 코드 재사용성과 유지보수성을 개선했습니다.
src/shared/api/course/api/getCourseSession.ts (1)
4-14: 이전 리뷰 피드백이 반영되었습니다!API 응답 검증 로직이 추가되어
isSuccess필드를 확인하고 실패 시 적절한 에러를 발생시킵니다. 구현이 정확합니다.src/shared/components/map/result/components/ResultMap.tsx (1)
12-44: 이전 리뷰 피드백이 완벽하게 반영되었습니다!
sessionId가 없을 때 버튼을 비활성화하고 적절한 스타일을 적용하는 Option 2 방식으로 구현되었습니다. UX 측면에서도 안전하고 명확한 구현입니다.src/shared/api/course/types/courseSession.ts (1)
11-24: API 응답 타입 구조가 적절합니다.표준 API 응답 패턴을 따르고 있으며,
result객체 내 필드들이 명확하게 정의되어 있습니다.mustVisitPlace가 optional로 처리된 것도 적절합니다.src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
62-66: 오버레이 정리 로직이 추가되었습니다.이전 리뷰에서 지적된 오버레이 메모리 누수 문제가 부분적으로 해결되었습니다. 오버레이는 정리되고 있으나, 이벤트 리스너 정리는 여전히 누락되어 있습니다(별도 코멘트 참조).
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/pages/map/index.tsx(2 hunks)src/pages/map/result/index.tsx(3 hunks)src/shared/api/course/queries/useCourseSession.ts(1 hunks)src/shared/api/course/queries/useRecommendCourse.ts(1 hunks)src/shared/components/map/result/components/FullMap.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/shared/components/map/result/components/FullMap.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.
Applied to files:
src/pages/map/index.tsxsrc/pages/map/result/index.tsx
🧬 Code graph analysis (4)
src/shared/api/course/queries/useCourseSession.ts (2)
src/shared/api/course/types/courseSession.ts (1)
CourseSessionResponse(11-24)src/shared/api/course/api/getCourseSession.ts (1)
getCourseSession(4-14)
src/shared/api/course/queries/useRecommendCourse.ts (2)
src/shared/api/course/types/recommendTypes.ts (2)
RecommendCourseResponse(20-30)RecommendCourseRequest(1-8)src/shared/api/course/api/postRecommendCourse.ts (1)
postRecommendCourse(7-13)
src/pages/map/index.tsx (4)
src/shared/api/course/queries/useRecommendCourse.ts (1)
useRecommendCourse(10-14)src/shared/hooks/useCourseSelection.ts (1)
useCourseSelection(3-16)src/shared/components/map/components/CourseSelectGroup.tsx (1)
CourseSelectGroup(14-44)src/shared/components/map/components/CourseInputSection.tsx (1)
CourseInputSection(12-72)
src/pages/map/result/index.tsx (3)
src/shared/api/course/queries/useCourseSession.ts (1)
useCourseSession(5-15)src/shared/components/map/result/components/ResultList.tsx (1)
ResultList(10-40)src/shared/components/map/result/components/ResultMap.tsx (1)
ResultMap(12-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/pages/map/result/index.tsx (1)
16-18: sessionId 배열 입력 시 런타임 오류 발생
router.query.sessionId는 URL에서 동일 파라미터가 중복될 경우string[]으로 들어올 수 있습니다. 현재처럼 단순 타입 캐스팅만 하면 배열이 그대로useCourseSession에 전달되어 내부의sessionId.trim()호출 시TypeError: sessionId.trim is not a function런타임 에러가 발생합니다.기존 리뷰에서 제안된 정규화 로직을 적용하세요:
- const sessionId = router.query.sessionId as string | undefined; - const { data } = useCourseSession(sessionId ?? ''); + const rawSessionId = router.query.sessionId; + const sessionId = + typeof rawSessionId === 'string' + ? rawSessionId + : Array.isArray(rawSessionId) + ? rawSessionId[0] + : undefined; + const normalizedSessionId = sessionId?.trim() ?? ''; + const { data } = useCourseSession(normalizedSessionId);그리고 Line 76에서도 동일하게
normalizedSessionId를 전달하도록 수정하세요.
🧹 Nitpick comments (2)
src/pages/map/result/index.tsx (1)
35-39: 태그 타입 안전성 개선 권장
filter(Boolean) as string[]로 타입을 단언하고 있지만, data 필드가 실제로 string인지 컴파일 타임에 보장되지 않습니다.더 명시적인 타입 가드를 사용하세요:
const tags = [ data?.travelPurpose || null, data?.stayDuration || null, data?.transportation || null, - ].filter(Boolean) as string[]; + ].filter((tag): tag is string => typeof tag === 'string' && tag.length > 0);src/pages/map/index.tsx (1)
21-44: 중복 제출 방지 개선 권장
isPending이CourseInputSection에 전달되어 버튼을 비활성화하지만,handleNext함수 자체에는 중복 호출 방지 로직이 없습니다. 극히 드물지만 빠른 더블클릭 시 두 개의 mutation이 동시에 트리거될 수 있습니다.함수 최상단에 early return을 추가하세요:
const handleNext = () => { - if (!canProceed) return; + if (!canProceed || isPending) return; mutate(Based on learnings.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/pages/map/index.tsx(2 hunks)src/pages/map/result/index.tsx(4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.
Applied to files:
src/pages/map/index.tsxsrc/pages/map/result/index.tsx
🧬 Code graph analysis (2)
src/pages/map/index.tsx (4)
src/shared/api/course/queries/useRecommendCourse.ts (1)
useRecommendCourse(10-14)src/shared/hooks/useCourseSelection.ts (1)
useCourseSelection(3-16)src/shared/components/map/components/CourseSelectGroup.tsx (1)
CourseSelectGroup(14-44)src/shared/components/map/components/CourseInputSection.tsx (1)
CourseInputSection(12-72)
src/pages/map/result/index.tsx (3)
src/shared/api/course/queries/useCourseSession.ts (1)
useCourseSession(5-15)src/shared/components/map/result/components/ResultList.tsx (1)
ResultList(10-40)src/shared/components/map/result/components/ResultMap.tsx (1)
ResultMap(12-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (1)
src/pages/map/index.tsx (1)
87-92: 코드 구조 확인 완료
CourseInputSection에 올바른 props를 전달하고 있으며, controlled component 패턴이 적절히 적용되었습니다.isLoading상태가 UI에 잘 반영되어 사용자 경험이 개선되었습니다.
| userLatitude: 37.4985, | ||
| userLongitude: 126.7822, |
There was a problem hiding this comment.
하드코딩된 좌표 사용
사용자 위치 대신 고정된 좌표 (37.4985, 126.7822)를 전송하고 있어, 모든 사용자에게 동일한 기준점에서 코스가 추천됩니다. 이는 사용자의 실제 위치 기반 추천이라는 기능 의도와 맞지 않습니다.
다음 중 하나를 선택하세요:
방안 1 (권장): Geolocation API로 실제 사용자 위치 사용
const [userLocation, setUserLocation] = useState<{lat: number; lng: number} | null>(null);
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserLocation({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
},
(error) => {
console.error('위치 정보 조회 실패:', error);
// 폴백 또는 에러 처리
}
);
}
}, []);
// mutate 호출 시
userLatitude: userLocation?.lat ?? 37.4985,
userLongitude: userLocation?.lng ?? 126.7822,방안 2: 하드코딩된 값이 의도된 것이라면 상수로 분리하고 주석 추가
// 임시: 개발/테스트용 기본 위치 (서울 인근)
const DEFAULT_LAT = 37.4985;
const DEFAULT_LNG = 126.7822;🤖 Prompt for AI Agents
In src/pages/map/index.tsx around lines 29-30 the userLatitude/userLongitude are
hardcoded to 37.4985/126.7822; replace this by obtaining the real user position
via the Geolocation API (add a state for userLocation, call
navigator.geolocation.getCurrentPosition in a useEffect, set the state, and pass
userLocation?.lat / userLocation?.lng with a fallback to defaults when calling
mutate), and if you instead intend to keep hardcoded values for testing, extract
them to named constants (e.g., DEFAULT_LAT/DEFAULT_LNG) with a comment stating
they are temporary dev/test defaults and ensure permission/fallback errors are
handled.
| onSuccess: (res) => { | ||
| if (res.isSuccess) { | ||
| router.push(`/map/result?sessionId=${res.result.sessionId}`); | ||
| } | ||
| }, | ||
| onError: (err) => { | ||
| console.error('AI 코스 추천 실패:', err); | ||
| }, | ||
| }, |
There was a problem hiding this comment.
에러 및 실패 응답 처리 부족
현재 에러 처리에 두 가지 문제가 있습니다:
- 네트워크 에러 시 사용자 피드백 없음:
onError에서console.error만 하고 사용자에게는 아무 알림이 없어 버튼이 동작하지 않는 것처럼 보입니다. res.isSuccess === false케이스 미처리: 서버가 200 응답을 주지만isSuccess: false인 경우 아무 동작도 하지 않아 사용자가 무한 대기하게 됩니다.
다음과 같이 수정하세요:
onSuccess: (res) => {
if (res.isSuccess) {
router.push(`/map/result?sessionId=${res.result.sessionId}`);
+ } else {
+ alert('코스 추천에 실패했습니다. 다시 시도해 주세요.');
}
},
onError: (err) => {
console.error('AI 코스 추천 실패:', err);
+ alert('네트워크 오류가 발생했습니다. 다시 시도해 주세요.');
},더 나은 UX를 위해 toast 라이브러리나 모달을 사용하는 것을 권장합니다.
📝 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.
| onSuccess: (res) => { | |
| if (res.isSuccess) { | |
| router.push(`/map/result?sessionId=${res.result.sessionId}`); | |
| } | |
| }, | |
| onError: (err) => { | |
| console.error('AI 코스 추천 실패:', err); | |
| }, | |
| }, | |
| onSuccess: (res) => { | |
| if (res.isSuccess) { | |
| router.push(`/map/result?sessionId=${res.result.sessionId}`); | |
| } else { | |
| alert('코스 추천에 실패했습니다. 다시 시도해 주세요.'); | |
| } | |
| }, | |
| onError: (err) => { | |
| console.error('AI 코스 추천 실패:', err); | |
| alert('네트워크 오류가 발생했습니다. 다시 시도해 주세요.'); | |
| }, | |
| }, |
🤖 Prompt for AI Agents
In src/pages/map/index.tsx around lines 34 to 42, the response/error handling is
incomplete: add user-facing feedback and state cleanup for both network errors
and successful HTTP responses where res.isSuccess === false. Update onError to
stop any loading state, log the error, and show a toast/modal with a friendly
message and optional retry; update onSuccess to handle the false case by
clearing loading, logging the server error details, and showing a toast/modal
(and only navigate when res.isSuccess is true). Ensure any disabled buttons are
re-enabled and include enough context in logs for debugging.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
src/pages/map/index.tsx (2)
29-30: 하드코딩된 좌표 사용사용자 위치 대신 고정된 좌표를 전송하고 있어, 실제 위치 기반 추천 기능과 맞지 않습니다. 이전 리뷰에서 이미 지적된 사항입니다.
34-42: 에러 및 실패 응답 처리 부족네트워크 에러 시 사용자 피드백이 없고,
res.success === false케이스가 미처리되어 있습니다. 이전 리뷰에서 이미 지적된 사항입니다.src/shared/api/course/types/recommendTypes.ts (1)
10-18: CoursePlace 인터페이스 중복 정의 및 타입 불일치 문제이 파일의
CoursePlace는placeId: number | null,placeImg: string | null로 nullable 타입을 사용하지만,courseSession.ts의 동일 인터페이스는 non-nullable(placeId: number,placeImg: string)로 정의되어 있습니다.서로 다른 nullability 계약으로 인해 API가 null을 반환할 경우 런타임 에러가 발생할 수 있습니다. 두 파일의 타입 정의를 하나로 통합하고, API 실제 응답에 맞는 올바른 nullability를 적용해야 합니다.
다음 스크립트로 CoursePlace를 사용하는 모든 위치를 확인하세요:
#!/bin/bash # Description: CoursePlace 인터페이스 사용처 확인 및 타입 불일치 검증 # CoursePlace 인터페이스 정의 위치 찾기 echo "=== CoursePlace 인터페이스 정의 ===" rg -n "interface CoursePlace" --type=ts # CoursePlace import 사용처 찾기 echo "" echo "=== CoursePlace import 사용처 ===" rg -n "import.*CoursePlace" --type=ts # null 체크 패턴 확인 echo "" echo "=== placeId/placeImg null 체크 존재 여부 ===" rg -n "placeId.*null|placeImg.*null|\?\.placeId|\?\.placeImg" --type=ts --type=tsxsrc/shared/api/course/types/courseSession.ts (2)
1-9: CoursePlace 중복 정의 - recommendTypes.ts와 nullability 불일치이 파일의
CoursePlace는placeId: number,placeImg: string로 non-nullable이지만,recommendTypes.ts의 동일 인터페이스는placeId: number | null,placeImg: string | null로 nullable입니다.현재 이 파일의 non-nullable 버전을 3개 파일(
useKakaoCourseMap.ts,ResultList.tsx,ResultMap.tsx)이 import하고 있으며, API가 실제로 null을 반환할 경우 런타임 타입 안전성이 깨집니다.권장 조치:
- API 실제 응답 스펙 확인 후 올바른 nullability 결정
- 두 정의를 하나의 파일로 통합 (recommendTypes.ts 권장)
- 모든 import를 통합된 정의로 변경
1-25: 파일명 오타 확인 필요과거 리뷰 코멘트에서 지적된 대로, 파일명이
courseSettion.ts로 오타가 있을 수 있습니다. 올바른 철자는courseSession.ts입니다.만약 실제 파일명이 오타를 포함하고 있다면, 파일명 변경과 함께 관련된 모든 import 경로(5개 파일)와 API 함수명(
getCourseSettion→getCourseSession)도 일괄 수정이 필요합니다.
🧹 Nitpick comments (1)
src/shared/components/map/result/components/FullMap.tsx (1)
14-15: 선택적 리팩토링: 중복 상태 제거 고려
selectedPlace에 이미latitude와longitude가 포함되어 있으므로,selectedLoc상태는 중복입니다. 현재 구조로도 정상 동작하지만, 상태 관리를 단순화할 수 있습니다.다음과 같이 리팩토링을 고려하세요:
- const [selectedPlace, setSelectedPlace] = useState<CoursePlace | null>(null); - const [selectedLoc, setSelectedLoc] = useState<{ lat: number; lng: number } | null>(null); + const [selectedPlace, setSelectedPlace] = useState<CoursePlace | null>(null); useKakaoCourseMap(mapRef, { places: data?.places || [], enableClick: true, onPinClick: (place) => { setSelectedPlace(place); - setSelectedLoc({ lat: place.latitude, lng: place.longitude }); }, }); - {selectedPlace && selectedLoc && ( + {selectedPlace && (
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/pages/map/index.tsx(1 hunks)src/pages/map/location/[placeId].tsx(1 hunks)src/pages/map/result/index.tsx(3 hunks)src/shared/api/course/api/getCourseSession.ts(1 hunks)src/shared/api/course/api/postRecommendCourse.ts(1 hunks)src/shared/api/course/queries/useCourseSession.ts(1 hunks)src/shared/api/course/queries/useRecommendCourse.ts(1 hunks)src/shared/api/course/types/courseSession.ts(1 hunks)src/shared/api/course/types/recommendTypes.ts(1 hunks)src/shared/components/container/LocationBubble.tsx(1 hunks)src/shared/components/map/result/components/FullMap.tsx(1 hunks)src/shared/constants/course/courseOptions.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/shared/api/course/queries/useRecommendCourse.ts
- src/shared/api/course/api/postRecommendCourse.ts
- src/shared/api/course/api/getCourseSession.ts
- src/shared/constants/course/courseOptions.ts
- src/shared/api/course/queries/useCourseSession.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.
Applied to files:
src/pages/map/result/index.tsxsrc/pages/map/location/[placeId].tsxsrc/pages/map/index.tsx
🧬 Code graph analysis (7)
src/pages/map/result/index.tsx (5)
src/shared/api/course/queries/useCourseSession.ts (1)
useCourseSession(5-12)src/shared/components/map/result/components/TagGroup.tsx (1)
TagGroup(11-40)src/shared/lib/utils.ts (1)
cn(71-73)src/shared/components/map/result/components/ResultList.tsx (1)
ResultList(10-40)src/shared/components/map/result/components/ResultMap.tsx (1)
ResultMap(12-44)
src/shared/api/course/types/courseSession.ts (1)
src/shared/api/course/types/recommendTypes.ts (1)
CoursePlace(10-18)
src/pages/map/location/[placeId].tsx (1)
src/shared/main/queries/useGetPlaceDetail.ts (1)
useGetPlaceDetail(4-10)
src/shared/components/container/LocationBubble.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
src/pages/map/index.tsx (4)
src/shared/api/course/queries/useRecommendCourse.ts (1)
useRecommendCourse(8-12)src/shared/hooks/useCourseSelection.ts (1)
useCourseSelection(3-16)src/shared/components/map/components/CourseSelectGroup.tsx (1)
CourseSelectGroup(14-44)src/shared/components/map/components/CourseInputSection.tsx (1)
CourseInputSection(12-72)
src/shared/components/map/result/components/FullMap.tsx (3)
src/shared/api/course/queries/useCourseSession.ts (1)
useCourseSession(5-12)src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
useKakaoCourseMap(13-70)
src/shared/api/course/types/recommendTypes.ts (1)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (12)
src/pages/map/index.tsx (4)
1-10: 필요한 모든 의존성이 올바르게 임포트되었습니다.Next.js Pages Router 사용, 커스텀 훅 및 컴포넌트 임포트가 적절합니다.
14-17: 상태 관리 구조가 잘 설계되었습니다.뮤테이션 훅과 폼 상태 관리가 명확하게 분리되어 있고, 로딩 상태(
isPending)도 적절히 추출되었습니다.
79-93: 컴포넌트 통합이 잘 구현되었습니다.
CourseSelectGroup과CourseInputSection에 props가 명확하게 전달되고, 로딩 상태(isPending→isLoading)가 올바르게 연결되어 사용자 경험을 개선하고 있습니다.
35-36: 코드가 API 응답 타입과 정확히 일치함
src/shared/api/course/types/recommendTypes.ts의RecommendCourseResponse타입 정의를 확인한 결과, 현재 코드의 필드명이 정확합니다:
res.success✓ (실제 타입:success: boolean)res.data.sessionId✓ (실제 타입:data.sessionId: string)이전 리뷰에서 언급된
res.isSuccess와res.result.sessionId는 현재 API 타입과 일치하지 않습니다. 코드는 올바르게 구현되어 있습니다.src/shared/api/course/types/recommendTypes.ts (2)
1-8: 요청 타입 정의가 적절합니다.필수 필드(여행 목적, 체류 기간, 교통수단)와 선택적 필드(위치 좌표, 필수 방문지)의 구분이 명확하며, API 요청 구조로 적합합니다.
20-31: 응답 타입 구조가 적절합니다.표준 envelope 패턴(success, code, message, data, timestamp)을 따르며,
sessionId를 통한 세션 기반 조회 플로우를 지원합니다.places,routeSummary,totalDistance필드가 코스 추천 결과를 적절히 표현하고 있습니다.src/shared/api/course/types/courseSession.ts (1)
11-25: 세션 응답 타입이 적절하게 설계되었습니다.원본 요청 파라미터(travelPurpose, stayDuration, transportation)와 결과 데이터(places), 세션 메타데이터(memberId, createdAt)를 포함하여 세션 조회 API 응답을 잘 표현하고 있습니다.
단,
CoursePlace타입 정의 통합 후 이 인터페이스의data.places필드도 올바른 nullability를 반영하도록 확인이 필요합니다.src/pages/map/location/[placeId].tsx (1)
11-13: LGTM! router.isReady 패턴이 올바르게 구현되었습니다.동적 라우트 파라미터를 안전하게 처리하기 위해
router.isReady체크를 사용한 것이 적절합니다. 이를 통해 SSR 단계에서undefined가 전달되는 것을 방지하고,enabled옵션과 함께 불필요한 API 호출을 막을 수 있습니다.src/shared/components/map/result/components/FullMap.tsx (1)
30-47: 오버레이 구현 확인 완료오버레이와 LocationBubble 간의 이벤트 처리가 올바르게 구현되었습니다:
- Line 33의
onClick으로 배경 클릭 시 오버레이 닫기- Line 38의
stopPropagation으로 버블 클릭 시 오버레이 유지참고:
z-[2000]은 매우 높은 값입니다. 다른 UI 요소(모달, 드롭다운 등)와의 충돌 가능성을 검토해 주세요.src/shared/components/container/LocationBubble.tsx (1)
10-10: LGTM! placeId 기반 동적 라우팅이 올바르게 구현되었습니다.
placeIdprop을 추가하고 이를 통해/map/location/${placeId}경로로 이동하는 구조가 명확합니다. 필수 prop으로 선언한 것도 적절합니다.Also applies to: 18-18, 23-23
src/pages/map/result/index.tsx (2)
17-20: LGTM! 안전한 sessionId 추출 패턴
router.isReady체크를 통해 SSR 및 초기 렌더링 중undefined를 안전하게 처리하고 있습니다. 이는 Pages Router에서 권장되는 패턴입니다.
22-22: 이전 리뷰 코멘트 해결 완료
isLoading과isError상태를 올바르게 추출하고, 조건부 렌더링으로 사용자 피드백을 제공하고 있습니다. 이전 리뷰에서 지적된 문제가 완전히 해결되었습니다.Also applies to: 62-68
| {viewMode === 'list' ? ( | ||
| <ResultList places={data?.places ?? []} /> | ||
| ) : ( | ||
| <ResultMap sessionId={sessionId!} places={data?.places ?? []} /> |
There was a problem hiding this comment.
sessionId 타입 안정성 개선 필요
Line 89에서 sessionId!를 사용하고 있지만, 사용자가 쿼리 파라미터 없이 /map/result로 직접 접근하는 경우 sessionId가 undefined일 수 있습니다.
다음과 같이 명시적 가드를 추가하세요:
- ) : isError ? (
+ ) : isError || !sessionId ? (
<p className='text-center text-red-500 mt-[4rem]'>
데이터를 불러오지 못했습니다. 다시 시도해주세요.
</p>
) : (
<>
<TagGroup
viewMode={viewMode}
tags={tags}
onToggleView={() =>
setViewMode((prev) => (prev === 'list' ? 'map' : 'list'))
}
/>
<section
className={cn(
'mt-[1.4rem] w-full text-gray-600',
viewMode === 'list'
? 'h-[43.6rem] overflow-y-auto no-scrollbar'
: 'h-[43.6rem] overflow-hidden',
)}
>
{viewMode === 'list' ? (
<ResultList places={data?.places ?? []} />
) : (
- <ResultMap sessionId={sessionId!} places={data?.places ?? []} />
+ <ResultMap sessionId={sessionId} places={data?.places ?? []} />
)}
</section>
</>
)}📝 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.
| <ResultMap sessionId={sessionId!} places={data?.places ?? []} /> | |
| ) : isError || !sessionId ? ( | |
| <p className='text-center text-red-500 mt-[4rem]'> | |
| 데이터를 불러오지 못했습니다. 다시 시도해주세요. | |
| </p> | |
| ) : ( | |
| <> | |
| <TagGroup | |
| viewMode={viewMode} | |
| tags={tags} | |
| onToggleView={() => | |
| setViewMode((prev) => (prev === 'list' ? 'map' : 'list')) | |
| } | |
| /> | |
| <section | |
| className={cn( | |
| 'mt-[1.4rem] w-full text-gray-600', | |
| viewMode === 'list' | |
| ? 'h-[43.6rem] overflow-y-auto no-scrollbar' | |
| : 'h-[43.6rem] overflow-hidden', | |
| )} | |
| > | |
| {viewMode === 'list' ? ( | |
| <ResultList places={data?.places ?? []} /> | |
| ) : ( | |
| <ResultMap sessionId={sessionId} places={data?.places ?? []} /> | |
| )} | |
| </section> | |
| </> | |
| )} |
🤖 Prompt for AI Agents
In src/pages/map/result/index.tsx around line 89, the code unconditionally
passes sessionId using a non-null assertion (sessionId!) which can be undefined
if the user navigates to /map/result without query params; add an explicit guard
that checks if sessionId is present before rendering ResultMap (e.g., return a
loading/error/redirect fallback or navigate back to the search entry), and only
render <ResultMap sessionId={sessionId} places={...} /> when sessionId is
defined to ensure type safety and avoid runtime errors.
|
|
||
| export default function FullMap({ sessionId }: FullMapProps) { | ||
| const mapRef = useRef<HTMLDivElement | null>(null); | ||
| const { data } = useCourseSession(sessionId); |
There was a problem hiding this comment.
로딩 및 에러 상태 처리 누락
useCourseSession에서 isLoading과 isError 상태를 받아오지 않아 다음 문제가 발생합니다:
- 데이터 로딩 중 사용자에게 피드백이 없음 (빈 지도만 표시)
- API 호출 실패 시 에러 메시지 없이 빈 지도가 표시되어 사용자 혼란 유발
다음과 같이 수정하세요:
- const { data } = useCourseSession(sessionId);
+ const { data, isLoading, isError } = useCourseSession(sessionId);그리고 조건부 렌더링을 추가하세요:
if (isLoading) {
return (
<div className="relative w-full h-full flex items-center justify-center bg-gray-100">
<p className="text-gray-400">지도를 불러오는 중...</p>
</div>
);
}
if (isError || !data) {
return (
<div className="relative w-full h-full flex items-center justify-center bg-gray-100">
<p className="text-red-500">지도를 불러올 수 없습니다.</p>
</div>
);
}🤖 Prompt for AI Agents
In src/shared/components/map/result/components/FullMap.tsx around line 13, the
hook call only destructures data from useCourseSession causing missing
loading/error handling; update the hook call to also destructure isLoading and
isError, then add conditional rendering: if isLoading return a centered "지도를
불러오는 중..." placeholder container, and if isError or !data return a centered
error container with "지도를 불러올 수 없습니다."; ensure these returns occur before
rendering the map so the UI shows proper feedback during load and on failure.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/components/set/LocationBubbleSet.tsx (1)
1-12: Pages Router 환경에서 불필요한'use client'지시자 제거 필요이 파일에는 React 훅이 전혀 사용되지 않으며, 프로젝트가 Next.js Pages Router를 사용 중인 만큼
'use client'지시자는 팀 컨벤션에 맞지 않습니다. 다음과 같이 제거해야 합니다.-'use client'; - import Overlay from '@/shared/overlay/Overlay'; import LocationBubble from '@/shared/components/container/LocationBubble'; import { cn } from '@/shared/lib';
🧹 Nitpick comments (3)
src/pages/map/result/Map.tsx (1)
23-29: 빈 상태 UI에 사용자 안내를 추가하는 것을 고려하세요.현재 "코스 정보가 없습니다" 메시지만 표시되어 사용자가 다음 액션을 취하기 어려울 수 있습니다. 메인 페이지로 이동하는 버튼이나 추가 안내 문구를 제공하면 사용자 경험이 개선될 수 있습니다.
예시:
) : ( - <div className="flex h-full items-center justify-center text-gray-600 text-title-sm bg-gray-200"> - 코스 정보가 없습니다 + <div className="flex flex-col gap-4 h-full items-center justify-center text-gray-600 bg-gray-200"> + <p className="text-title-sm">코스 정보가 없습니다</p> + <button + onClick={() => router.push('/')} + className="px-4 py-2 bg-mint-500 text-white rounded-lg" + > + 홈으로 돌아가기 + </button> </div> )}src/pages/map/location/[placeId].tsx (1)
8-36: router.query 직접 String 변환으로 인한 표시 값/이미지 fallback 이슈 가능성현재
router.query에서 꺼낸 값들을 그대로String(...)으로 감싸서 렌더링하고 있고,imageSrc는 단순히typeof imageSrc === 'string'여부만 보고 있습니다.
address나description이 없는 경우String(address)→"undefined"가 그대로 UI에 노출될 수 있습니다.imageSrc가 빈 문자열("")인 경우에도typeof imageSrc === 'string'조건을 통과해 빈 경로가<Image>에 넘어가고, 이는 런타임 에러나 깨진 이미지로 이어질 수 있습니다.
router.query값을 안전하게 정규화한 뒤 사용하는 쪽이 좋겠습니다:- const { name, imageSrc, description, address } = router.query; + const { name, imageSrc, description, address } = router.query; + const nameStr = + typeof name === 'string' ? name : Array.isArray(name) ? name[0] : ''; + const addressStr = + typeof address === 'string' + ? address + : Array.isArray(address) + ? address[0] + : ''; + const descriptionStr = + typeof description === 'string' + ? description + : Array.isArray(description) + ? description[0] + : ''; + const imageSrcStr = + typeof imageSrc === 'string' && imageSrc.length > 0 + ? imageSrc + : '/assets/board.svg';그리고 아래 JSX에서는
String(...)대신 위에서 만든 안전한 문자열들을 사용하는 방향이 좋겠습니다.- <Header title={String(name)} onClick={() => router.back()} /> + <Header title={nameStr} onClick={() => router.back()} /> ... - <Image - src={typeof imageSrc === 'string' ? imageSrc : '/assets/board.svg'} - alt={String(name)} + <Image + src={imageSrcStr} + alt={nameStr} ... - <LocationCard - name={String(name)} - address={String(address)} - description={String(description)} + <LocationCard + name={nameStr} + address={addressStr} + description={descriptionStr} ... - <AddressCopy variant="mint" value={String(address)} /> + <AddressCopy variant="mint" value={addressStr} />이렇게 하면 잘못된
"undefined"표시와 비어 있는 이미지 경로로 인한 문제를 함께 피할 수 있습니다.src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
34-96: 핀 DOM 이벤트 정리 및innerHTML사용은 안전하지만 정교화 여지 있음 (선택 사항)이 부분은 기능적으로는 잘 동작하지만, 몇 가지 개선 여지가 있습니다.
DOM
click리스너 정리
overlay는 cleanup에서setMap(null)로 제거하지만,el.addEventListener('click', ...)로 등록한 DOM 리스너는 따로 해제하지 않고 있습니다. 현재 구조에서는 overlay/DOM 노드가 함께 GC되어 큰 문제는 없겠지만, 메모리 관리 관점에서 리스너 참조도 함께 제거해 두면 더 안전합니다.간단하게는 핸들러를 변수에 담아두고 cleanup에서
el.removeEventListener('click', handler)를 호출하도록 overlaysRef와 함께 관리하는 방식을 고려해볼 수 있습니다.
innerHTML로 고정 SVG 삽입 (XSS 측면)
el.innerHTML = \<svg ...>...`부분은 완전히 고정된 문자열이라 현재로서는 XSS 위험은 사실상 없습니다. 다만 정적 분석 도구에서 계속 경고를 띄운다면,document.createElementNS로 SVG를 생성해서appendChild` 하는 방식으로 바꾸는 것도 한 가지 방법입니다.둘 다 지금 당장 필수는 아니지만, 향후 맵 관련 로직이 더 복잡해질 것을 고려하면 한 번쯤 리팩터링 후보로 가져가도 좋을 것 같습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/pages/map/location/[placeId].tsx(1 hunks)src/pages/map/result/Map.tsx(2 hunks)src/shared/api/course/api/getCourseSession.ts(1 hunks)src/shared/components/container/LocationBubble.tsx(1 hunks)src/shared/components/map/result/components/FullMap.tsx(1 hunks)src/shared/components/map/result/components/ResultList.tsx(1 hunks)src/shared/components/map/result/components/TagGroup.tsx(1 hunks)src/shared/components/set/LocationBubbleSet.tsx(1 hunks)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts(1 hunks)src/shared/types/kakao.d.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/shared/components/map/result/components/FullMap.tsx
- src/shared/components/map/result/components/TagGroup.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.
Applied to files:
src/pages/map/location/[placeId].tsxsrc/shared/components/map/result/components/ResultList.tsxsrc/pages/map/result/Map.tsx
🧬 Code graph analysis (7)
src/shared/api/course/api/getCourseSession.ts (2)
src/shared/api/instance.ts (1)
apiAuth(20-23)src/shared/api/course/types/courseSession.ts (1)
CourseSessionResponse(11-25)
src/shared/components/set/LocationBubbleSet.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
src/pages/map/location/[placeId].tsx (1)
src/shared/components/container/LocationCard.tsx (2)
LocationCardProps(16-119)LocationCardProps(7-14)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (3)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/utils/loadKakaoSdk.ts (1)
loadKakaoSdk(1-22)src/shared/hooks/kakao/useKakaoMap.ts (6)
useKakaoMap(11-49)map(24-44)mapRef(15-48)window(18-45)p(30-43)Location(5-9)
src/shared/components/map/result/components/ResultList.tsx (2)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/lib/utils.ts (1)
cn(71-73)
src/shared/components/container/LocationBubble.tsx (1)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)
src/pages/map/result/Map.tsx (1)
src/shared/components/map/result/components/FullMap.tsx (1)
FullMap(11-43)
🪛 ast-grep (0.39.9)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts
[warning] 36-40: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 36-40: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="28" height="28" viewBox="0 0 24 24"> <use href="#icon-RedMapPin"></use> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
🪛 Biome (2.1.2)
src/shared/types/kakao.d.ts
[error] 11-11: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-deploy
🔇 Additional comments (5)
src/shared/api/course/api/getCourseSession.ts (1)
1-2: LGTM!필요한 의존성만 정확히 import하고 있으며,
type키워드를 사용한 타입 전용 import도 적절합니다.src/pages/map/result/Map.tsx (2)
8-10: 이전 리뷰 코멘트가 정확히 반영되었습니다.
router.query.sessionId가 배열일 경우를 적절히 처리하여, 타입 안정성이 개선되었습니다.
3-3: 컴포넌트 재구성이 적절하게 반영되었습니다.
FullMap컴포넌트를 공유 컴포넌트로 이동한 것은 재사용성을 높이는 좋은 리팩토링입니다.src/shared/components/container/LocationBubble.tsx (1)
8-71: CoursePlace 단일 props로 정리된 인터페이스 및 라우팅 흐름 좋습니다
place: CoursePlace하나로 버블 내부 UI와 라우팅 모두를 처리하도록 정리한 구조가 깔끔합니다.
/map/location/[placeId]이동 시 넘기는 query 키(name,imageSrc,address,description)도ResultList와 정확히 일관되어 있어서 추후 변경·추적이 용이해 보입니다.현재 구현은 그대로 두셔도 될 것 같습니다.
src/shared/types/kakao.d.ts (1)
1-66: Kakao Maps 타입 선언 전반적으로 일관되고 문제 없어 보입니다
declare global+namespace kakao.maps패턴으로 Kakao Maps SDK 타입을 정리한 방식이 실제 사용 코드(useKakaoCourseMap등)와 잘 맞습니다.Biome에서
Map이름이 전역Map을 shadow한다고 경고하지만, 이 선언은kakao.maps.Map네임스페이스 안에 한정되어 있고 실제 Kakao SDK의 클래스 명칭과도 일치하므로 유지해도 무방해 보입니다. 필요하다면 해당 파일에 한해 lint rule을 완화하는 정도만 고려하시면 될 것 같습니다.
| export const getCourseSession = async (sessionId: string) => { | ||
| const { data } = await apiAuth.get<CourseSessionResponse>( | ||
| `/api/courses/session/${sessionId}`, | ||
| ); | ||
|
|
||
| if (!data.success) { | ||
| throw new Error(data.message || "Failed to fetch course session"); | ||
| } | ||
|
|
||
| return data.data; | ||
| }; |
There was a problem hiding this comment.
이전 리뷰 피드백이 잘 반영되었습니다!
API 응답 검증 로직(data.success 체크 및 에러 throw)이 올바르게 추가되었습니다. 다만 추가로 고려할 수 있는 개선 사항이 있습니다.
Minor: data.data null 체크 추가 고려
data.success가 true이더라도 data.data가 예상치 못하게 null 또는 undefined일 수 있는 엣지 케이스에 대한 방어 코드가 없습니다. 백엔드 계약상 이런 상황은 발생하지 않아야 하지만, 방어적 프로그래밍 관점에서 체크를 추가할 수 있습니다.
if (!data.success) {
throw new Error(data.message || "Failed to fetch course session");
}
+
+ if (!data.data) {
+ throw new Error("Course session data is missing");
+ }
return data.data;Optional: 타입 안전성 개선
현재 CourseSessionResponse 타입은 success 값과 관계없이 data 필드가 항상 존재한다고 정의되어 있습니다. Discriminated union을 사용하면 타입 수준에서 더 안전하게 표현할 수 있습니다.
src/shared/api/course/types/courseSession.ts에서 타입을 다음과 같이 개선할 수 있습니다:
type CourseSessionResponseSuccess = {
success: true;
code: string;
message: string;
data: {
memberId: number;
places: CoursePlace[];
createdAt: string;
travelPurpose: string;
stayDuration: string;
transportation: string;
mustVisitPlace?: string;
};
timestamp: string;
};
type CourseSessionResponseFailure = {
success: false;
code: string;
message: string;
data?: never;
timestamp: string;
};
export type CourseSessionResponse =
| CourseSessionResponseSuccess
| CourseSessionResponseFailure;이렇게 하면 TypeScript가 data.success === true 체크 후 data.data의 존재를 보장합니다.
🤖 Prompt for AI Agents
In src/shared/api/course/api/getCourseSession.ts around lines 4 to 14, add a
defensive null/undefined check for data.data after the existing data.success
check and throw a clear error if data.success is true but data.data is missing
so callers never receive undefined; additionally, update
src/shared/api/course/types/courseSession.ts to use a discriminated union
(success: true/false) so TypeScript can guarantee data exists when success is
true, and adjust the function signature/types accordingly to satisfy the
compiler.
| 'use client'; | ||
| import { cn } from '@/shared/lib'; | ||
| import { LocationCard } from '@/shared/components'; | ||
| import type { CoursePlace } from '@/shared/api/course/types/courseSession'; | ||
| import { useRouter } from 'next/router'; | ||
|
|
||
| interface ResultListProps { | ||
| places: CoursePlace[]; | ||
| } | ||
|
|
||
| export default function ResultList({ places }: ResultListProps) { | ||
| const router = useRouter(); | ||
|
|
||
| const handleClick = (place: CoursePlace) => { | ||
| router.push({ | ||
| pathname: `/map/location/${place.placeId}`, | ||
| query: { | ||
| name: place.name, | ||
| address: place.address, | ||
| description: place.description, | ||
| imageSrc: place.placeImg, | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| role="list" | ||
| aria-label="추천 결과 리스트" | ||
| className={cn( | ||
| 'grid grid-cols-2 gap-[1.2rem] w-full justify-items-center pb-[15rem]', | ||
| 'overflow-y-scroll no-scrollbar' | ||
| )} | ||
| > | ||
| {places.length > 0 ? ( | ||
| places.map((place) => ( | ||
| <div | ||
| key={place.placeId} | ||
| role="listitem" | ||
| className="w-full cursor-pointer" | ||
| onClick={() => handleClick(place)} | ||
| > | ||
| <LocationCard | ||
| name={place.name} | ||
| address={place.address} | ||
| description={place.description} | ||
| imageSrc={place.placeImg} | ||
| variant="gray" | ||
| size="medium" | ||
| /> | ||
| </div> | ||
| )) | ||
| ) : ( | ||
| <p className="text-gray-400 text-body-sm text-center w-full"> | ||
| 표시할 장소가 없습니다. | ||
| </p> | ||
| )} | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
ResultList에서 불필요한 'use client' 지시자 및 클릭 영역 접근성 개선 제안
-
'use client'제거 (Pages Router 컨벤션 위반)
이 프로젝트는 Pages Router를 사용하고 있어, 이전 PR에서와 마찬가지로'use client'지시자를 사용하지 않는 방향으로 리뷰가 진행되었습니다. 이 컴포넌트는 Pages Router 환경에서 기본적으로 클라이언트에서 실행되므로'use client'는 의미가 없고, 컨벤션상 제거하는 편이 좋겠습니다.-'use client'; - import { cn } from '@/shared/lib';
-
클릭 가능한 카드의 접근성 보완 (선택)
현재 각 장소는<div role="listitem">+onClick으로만 클릭 가능해서, 키보드 사용자 입장에서는 포커싱/엔터 입력이 되지 않습니다. 아래 둘 중 하나로 개선을 고려해볼 수 있습니다.<button>또는<a>태그로 변경 (onClick유지,type="button"지정)- 혹은 현재 구조를 유지하되
tabIndex={0}와onKeyDown(Enter/Space 처리)을 추가
필수는 아니지만, 지도 결과 리스트가 메인 인터랙션 영역인 만큼 접근성 차원에서 한 번 검토해 보시면 좋겠습니다.
Based on learnings
---
🏁 Script executed:
```shell
#!/bin/bash
# Verify file exists and check its content structure
fd -t f "ResultList.tsx" --exec head -n 10 {}
Length of output: 274
🏁 Script executed:
#!/bin/bash
# Confirm the exact import statements
rg -n "use client|useRouter|next/router" "src/shared/components/map/result/components/ResultList.tsx" -A 2 -B 2Length of output: 424
🏁 Script executed:
#!/bin/bash
# Check for any other Files using 'use client' in the project to understand convention
rg "'use client'" --type=tsx --type=ts | head -20Length of output: 27
🏁 Script executed:
#!/bin/bash
# Verify the component export and overall structure
wc -l "src/shared/components/map/result/components/ResultList.tsx"Length of output: 61
🏁 Script executed:
#!/bin/bash
# Check if similar components (ResultMap.tsx mentioned in learnings) exist and their structure
fd -t f "ResultMap.tsx" --exec head -n 5 {}Length of output: 262
🏁 Script executed:
#!/bin/bash
# Look for other files in the same directory to understand patterns
fd -t f . "src/shared/components/map/result/components/" --type fLength of output: 229
🏁 Script executed:
#!/bin/bash
# Check if there are any accessibility patterns used elsewhere in the project
rg "onKeyDown|tabIndex" "src/shared/components" -l | head -10Length of output: 153
🏁 Script executed:
#!/bin/bash
# Verify Pages Router usage in the project
fd -t f "next.config" --exec cat {}Length of output: 1013
🏁 Script executed:
#!/bin/bash
# Check for useRouter patterns in other Pages Router components
rg "useRouter.*next/router" src/pages --type=tsx -c | head -5Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for 'use client' in pages directory to see if it should be there
rg "'use client'" "src/pages" --type=tsx -cLength of output: 27
🏁 Script executed:
#!/bin/bash
# Check if the file actually has 'use client' on line 1
head -1 "src/shared/components/map/result/components/ResultList.tsx"Length of output: 13
🏁 Script executed:
#!/bin/bash
# Verify lines 1-59 match the provided snippet
sed -n '1,59p' "src/shared/components/map/result/components/ResultList.tsx" | wc -lLength of output: 2
🏁 Script executed:
#!/bin/bash
# Look at the role attribute usage patterns
rg "role=[\"']list[\"']|role=[\"']listitem[\"']" "src/shared/components" -C 2Length of output: 2196
🏁 Script executed:
#!/bin/bash
# Check if onClick on divs is a common pattern in the project
rg "onClick" "src/shared/components" --type=tsx -B 2 | grep -E "(div|span).*onClick|onClick.*div|onClick.*span" | head -10Length of output: 27
KongMezu
left a comment
There was a problem hiding this comment.
리뷰 한번 확인해주시고! 바로 approve하겠습니다..!
There was a problem hiding this comment.
이거 기존 아이콘 MapPin 에 fill color 채워서 해도 되는데 확인 한번 해주세요!
| }; | ||
| }, [places, enableClick, mapRef]); | ||
|
|
||
| return mapInstanceRef.current; |
There was a problem hiding this comment.
여기 return 시점에서 map instance 잘 받나요..?
return mapInstanceRef; 이렇게 반환하는게..안전하지 않을까요?
| el.addEventListener('click', () => { | ||
| if (!mapRef.current) return; | ||
|
|
||
| const pos = new maps.LatLng(place.latitude, place.longitude); | ||
| const proj = map.getProjection(); | ||
|
|
||
| const offsetY = 40; | ||
| const p = proj.pointFromCoords(pos); | ||
| const newPoint = new maps.Point(p.x, p.y - offsetY); | ||
| const newCenter = proj.coordsFromPoint(newPoint); | ||
|
|
||
| map.panTo(newCenter); | ||
|
|
||
| const mapRect = mapRef.current.getBoundingClientRect(); | ||
| const localPoint = proj.containerPointFromCoords(pos); | ||
|
|
||
| const pageX = mapRect.left + localPoint.x; | ||
| const pageY = mapRect.top + localPoint.y; | ||
|
|
||
| onPinClick?.(place, { | ||
| x: pageX, | ||
| y: pageY, | ||
| }); |
There was a problem hiding this comment.
이거 메모리에 리스너가 계속 누적되진..않나요..?
중간에 한번 clean up 필요할 거 같습니다!
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
117-117: 의존성 배열에onPinClick이 누락되어 stale closure 버그 위험이 있습니다.Line 97에서
onPinClick을 호출하는 클릭 리스너가 Line 78-98에서 생성되지만, Line 117의 의존성 배열에는onPinClick이 포함되지 않았습니다.onPinClick이 변경되어도 effect가 재실행되지 않아 이전 콜백을 계속 참조하게 됩니다.과거 리뷰에서 "commit 9916376에서 해결됨"으로 표시되었으나 현재 코드에는 여전히 누락되어 있습니다.
다음과 같이 수정하세요:
- }, [places, enableClick, mapRef]); + }, [places, enableClick, mapRef, onPinClick]);상위 컴포넌트에서
onPinClick을useCallback으로 감싸면 불필요한 재실행을 방지할 수 있습니다.
🧹 Nitpick comments (2)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (1)
119-119: ref 객체 대신ref.current를 반환하는 것을 고려해보세요.현재
mapInstanceRef(ref 객체)를 반환하고 있어 사용하는 쪽에서.current에 접근해야 합니다. 일반적인 커스텀 훅 패턴에서는 값 자체(mapInstanceRef.current)를 반환합니다.ref 객체를 반환하는 것도 틀린 것은 아니지만(안정적이고 재렌더링을 유발하지 않음), 개발자 경험(DX) 측면에서는 값을 직접 반환하는 것이 더 직관적입니다.
다음과 같이 수정하는 것을 고려해보세요:
- return mapInstanceRef; + return mapInstanceRef.current;다만, 여러 렌더링에서 맵 인스턴스에 접근해야 하는 특별한 요구사항이 있다면 현재 방식을 유지할 수도 있습니다.
next.config.ts (1)
24-29:pathname제한 추가로 보안 강화 권장웹 검색 결과, Google Places API v1은 공식적으로
places.googleapis.com을 사용하며, 사진 엔드포인트는https://places.googleapis.com/v1/places/PLACE_ID/photos/PHOTO_REFERENCE/media구조입니다. 따라서 현재 설정된 호스트명은 올바릅니다.다만 보안을 위해
pathname패턴을 추가하여 허용된 경로를 제한하는 것을 권장합니다:remotePatterns: [ { protocol: 'https', hostname: 'places.googleapis.com', + pathname: '/v1/places/**', }, ],
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
next.config.ts(1 hunks)src/shared/hooks/kakaoMap/useKakaoCourseMap.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (2)
src/shared/api/course/types/courseSession.ts (1)
CoursePlace(1-9)src/shared/utils/loadKakaoSdk.ts (1)
loadKakaoSdk(1-22)
🪛 ast-grep (0.39.9)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts
[warning] 47-63: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <svg width="36" height="36" viewBox="0 0 24 24" fill="#EB3939" stroke="#EB3939" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#a)"> <path d="M12 1.5a8.26 8.26 0 0 0-8.25 8.25c0 7.06 7.5 12.39 7.82 12.614a.75.75 0 0 0 .86 0c.32-.223 7.82-5.555 7.82-12.614A8.26 8.26 0 0 0 12 1.5m0 5.25a3 3 0 1 1 0 6 3 3 0 0 1 0-6" fill="#EB3939" stroke="#EB3939" /> </g> <defs> <clipPath id="a"> <path d="M0 0h24v24H0z" /> </clipPath> </defs> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 47-63: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <svg width="36" height="36" viewBox="0 0 24 24" fill="#EB3939" stroke="#EB3939" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#a)"> <path d="M12 1.5a8.26 8.26 0 0 0-8.25 8.25c0 7.06 7.5 12.39 7.82 12.614a.75.75 0 0 0 .86 0c.32-.223 7.82-5.555 7.82-12.614A8.26 8.26 0 0 0 12 1.5m0 5.25a3 3 0 1 1 0 6 3 3 0 0 1 0-6" fill="#EB3939" stroke="#EB3939" /> </g> <defs> <clipPath id="a"> <path d="M0 0h24v24H0z" /> </clipPath> </defs> </svg>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build-and-deploy
- GitHub Check: build-and-deploy
🔇 Additional comments (5)
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts (5)
1-4: LGTM! 임포트가 적절합니다.클라이언트 컴포넌트 지시자와 필요한 React 훅, 유틸리티, 타입을 올바르게 가져왔습니다.
6-13: LGTM! 인터페이스 설계가 명확합니다.옵션 인터페이스가 잘 구조화되어 있으며,
onPinClick콜백에 장소 데이터와 페이지 좌표를 함께 전달하는 설계가 적절합니다.
19-21: LGTM! Ref 구조가 개선되었습니다.맵 인스턴스, 오버레이, 이벤트 리스너 정리를 위한 ref가 모두 준비되어 과거 리뷰에서 지적된 메모리 누수 문제를 해결했습니다.
48-64: 정적 분석 경고는 false positive입니다.
innerHTML에 할당되는 SVG는 완전히 하드코딩된 문자열 리터럴이며 사용자 입력이 포함되지 않으므로 XSS 위험이 없습니다. 정적 분석 도구의 경고는 무시해도 안전합니다.
110-116: LGTM! cleanup 로직이 올바르게 구현되었습니다.오버레이 제거(
setMap(null))와 이벤트 리스너 정리(removeEventListener)가 모두 수행되며, 배열도 초기화되어 메모리 누수를 방지합니다. 과거 리뷰에서 지적된 문제가 잘 해결되었습니다.
|
피드백 반영 완료했습니다! |
🔥 작업 내용
/api/courses/recommend기반 AI 코스 추천 요청(POST) + 결과 페이지 자동 이동 구현useRecommendCourse훅 구현 및mutate기반 비동기 처리sessionId를 URL 파라미터로 포함하여/map/result로 이동/api/courses/session/{sessionId}기반 AI 코스 추천 결과 조회 API 연동useCourseSession훅 생성 및 React Query로 서버 데이터 캐싱 구조 구축places,travelPurpose,stayDuration,transportation) 실시간 반영CourseResultPage에서ResultList,ResultMap,TagGroup을 통합 관리sessionId,places,tags를 공통 props로 전달해 일관된 렌더링 구조 확립useKakaoCourseMap훅 생성 ->FullMap,ResultMap에서 재사용 가능하도록 구조화listData) 제거 -> 서버 데이터 기반 리스트 렌더링travelPurpose,stayDuration,transportation)을 기반으로 동적 태그 표시전체화면 보기버튼 클릭 시sessionId를 포함해 지도 페이지로 이동PopupSet) 유지🧠 개선 포인트
useKakaoCourseMap으로 통합해 중복 최소화CourseResultPage)에서 데이터 흐름을 단일화해 유지보수성 향상🤔 추후 작업 사항
/api/courses/recommend응답 구조 변경 시 타입 및 예외 처리 리팩토링🔗 이슈
close #108
💬 PR Point (To Reviewer)
useRecommendCourse->useCourseSession) 구조가 자연스러운지 검토 부탁드립니다.CourseResultPage -> ResultMap/ResultListprops 전달 구조가 효율적인지 확인 부탁드립니다.useKakaoCourseMap훅의 추상화 수준(지도 초기화, 오버레이 렌더링, 클릭 이벤트 처리)이 적절한지 검토 부탁드립니다.📸 기능 스크린샷 / 시연 GIF
-.Clipchamp.mp4
Summary by CodeRabbit
새로운 기능
개선 사항