Skip to content

✨Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결#109

Merged
skyblue1232 merged 18 commits intodevelopfrom
api/#108/kakao-map-api
Nov 15, 2025
Merged

✨Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결#109
skyblue1232 merged 18 commits intodevelopfrom
api/#108/kakao-map-api

Conversation

@skyblue1232
Copy link
Copy Markdown
Contributor

@skyblue1232 skyblue1232 commented Nov 11, 2025

🔥 작업 내용

  • /api/courses/recommend 기반 AI 코스 추천 요청(POST) + 결과 페이지 자동 이동 구현
    • 사용자가 선택한 여행 목적, 체류 기간, 교통수단, 필수 방문지를 입력 -> AI 추천 결과 세션 생성
    • useRecommendCourse 훅 구현 및 mutate 기반 비동기 처리
    • 요청 성공 시 sessionId를 URL 파라미터로 포함하여 /map/result로 이동
  • /api/courses/session/{sessionId} 기반 AI 코스 추천 결과 조회 API 연동
    • useCourseSession 훅 생성 및 React Query로 서버 데이터 캐싱 구조 구축
    • 서버 응답(places, travelPurpose, stayDuration, transportation) 실시간 반영
  • 코스 결과 UI 통합
    • CourseResultPage에서 ResultList, ResultMap, TagGroup을 통합 관리
    • 상위에서 sessionId, places, tags를 공통 props로 전달해 일관된 렌더링 구조 확립
  • Kakao 지도 기능 리팩토링
    • useKakaoCourseMap 훅 생성 -> FullMap, ResultMap에서 재사용 가능하도록 구조화
    • 지도 중심 이동, 커스텀 오버레이(핀), 클릭 이벤트 핸들러 통합 관리
  • 리스트 뷰 개선
    • 더미 데이터(listData) 제거 -> 서버 데이터 기반 리스트 렌더링
    • 장소명, 주소, 설명, 이미지 등 실시간 반영
  • 태그 그룹 자동 생성
    • 서버 응답 값(travelPurpose, stayDuration, transportation)을 기반으로 동적 태그 표시
    • 기존 하드코딩 제거, API 기반 개인화된 태그 UI 제공
  • UX/UI 개선
    • “지도로 보기 <-> 리스트로 보기” 전환 유지
    • 전체화면 보기 버튼 클릭 시 sessionId를 포함해 지도 페이지로 이동
    • 새로고침 시 결과 초기화 안내 팝업(PopupSet) 유지

🧠 개선 포인트

  • 기존 더미 데이터 기반 구조를 서버 연동형 구조로 리팩토링
  • Kakao Map 초기화·핀 표시·이벤트 로직을 useKakaoCourseMap으로 통합해 중복 최소화
  • 상위 컴포넌트(CourseResultPage)에서 데이터 흐름을 단일화해 유지보수성 향상
  • React Query의 staleTime 설정으로 API 호출 최소화 및 렌더링 안정화
  • POST -> GET(세션 기반)으로 이어지는 데이터 플로우 구축으로 UX 일관성 확보

🤔 추후 작업 사항

  • /api/courses/recommend 응답 구조 변경 시 타입 및 예외 처리 리팩토링
  • 코스 공유 링크(sessionId 기반) 생성 및 접근 기능 추가
  • 코스 순서에 따른 지도 경로선(Polyline) 시각화
  • 리스트 -> 지도 간 포커싱 연동 (카드 클릭 시 해당 핀 강조)
  • 지도 반응형 및 클러스터링 적용

🔗 이슈

close #108

💬 PR Point (To Reviewer)

  • POST -> GET 연계 흐름(useRecommendCourse -> useCourseSession) 구조가 자연스러운지 검토 부탁드립니다.
  • CourseResultPage -> ResultMap/ResultList props 전달 구조가 효율적인지 확인 부탁드립니다.
  • useKakaoCourseMap 훅의 추상화 수준(지도 초기화, 오버레이 렌더링, 클릭 이벤트 처리)이 적절한지 검토 부탁드립니다.
  • React Query의 캐싱 정책(staleTime: 5분)과 에러 핸들링 로직에 대한 의견 부탁드립니다.

📸 기능 스크린샷 / 시연 GIF

-.Clipchamp.mp4

Summary by CodeRabbit

  • 새로운 기능

    • AI 기반 코스 추천 흐름 추가(입력 → 추천 → 세션 결과 이동)
    • 목적·숙박·이동수단을 한곳에서 선택하는 통합 선택 UI 추가
    • 세션 기반 전체화면 지도와 미리보기 지도, 장소 목록 및 장소 상세 페이지 추가
  • 개선 사항

    • 입력 섹션을 제어형으로 전환하고 추천 로딩 상태 반영
    • 결과 페이지가 세션 데이터를 기반으로 태그·장소를 표시하고 빈 상태/로딩/오류 처리 강화
    • 지도 핀 상호작용 및 위치 정보 표시 개선

@skyblue1232 skyblue1232 self-assigned this Nov 11, 2025
@skyblue1232 skyblue1232 added the feat 새로운 기능 추가 / 퍼블리싱 label Nov 11, 2025
@skyblue1232 skyblue1232 added the api api 연결 label Nov 11, 2025
@skyblue1232 skyblue1232 linked an issue Nov 11, 2025 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 11, 2025

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

Project Deployment Preview Comments Updated (UTC)
af-fe Ready Ready Preview Comment Nov 14, 2025 6:27pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

AI 기반 추천 코스 뮤테이션과 세션 조회가 도입되었고, 맵 결과 UI가 공유 컴포넌트로 재구성되었으며 Kakao 지도 훅·타입·SDK 로드 유틸, 관련 API 클라이언트·타입·상수·아이콘이 추가·수정되었습니다.

Changes

Cohort / File(s) 변경 요약
지도 페이지 메인 흐름
src/pages/map/index.tsx
CourseSelectGroup 도입으로 선택 UI 통합; CourseInputSection을 제어형으로 변경(value/onChange/onNext/isLoading); useRecommendCourse 뮤테이션 호출 추가 및 성공 시 /map/result?sessionId=...로 네비게이트
결과 페이지 엔트리 및 Map 페이지
src/pages/map/result/index.tsx, src/pages/map/result/Map.tsx
sessionId 쿼리 추출·검증; Header 뒤로가기 로직 개선; useCourseSession으로 데이터 로드하여 TagGroup, ResultList, ResultMap 또는 <FullMap sessionId={...}> 조건부 렌더링
결과 컴포넌트(공유) 추가/이관
src/shared/components/map/result/components/FullMap.tsx, .../ResultList.tsx, .../ResultMap.tsx, .../TagGroup.tsx
FullMap/ResultList/ResultMap/TagGroup 추가 또는 시그니처 변경(예: sessionId, places, tags) — 장소 데이터로 지도·리스트·태그 렌더링 및 핀 클릭 상호작용
삭제된 페이지 레벨 컴포넌트
src/pages/map/result/components/FullMap.tsx, .../ResultList.tsx, .../ResultMap.tsx
기존 페이지 레벨의 하위 컴포넌트 제거(공유 컴포넌트로 대체)
입력·선택 UI 및 옵션 상수 변경
src/shared/components/map/components/CourseInputSection.tsx, .../CourseSelectGroup.tsx, .../CourseSelectSection.tsx, src/shared/constants/course/courseOptions.ts
CourseInputSection 제어형 전환 및 로딩 플래그 추가; CourseSelectGroup 추가로 목적/체류/이동 선택 중앙화; course option ids를 한글로 변경
추천 API·쿼리·타입 추가
src/shared/api/course/api/postRecommendCourse.ts, src/shared/api/course/queries/useRecommendCourse.ts, src/shared/api/course/types/recommendTypes.ts
추천 코스 POST 클라이언트와 요청/응답 타입 추가; React Query 뮤테이션 훅 useRecommendCourse 추가
세션 API·쿼리·타입 추가
src/shared/api/course/api/getCourseSession.ts, src/shared/api/course/queries/useCourseSession.ts, src/shared/api/course/types/courseSession.ts
세션 GET 클라이언트 및 타입 추가; React Query 쿼리 훅 useCourseSession 추가
Kakao 지도 훅·SDK·타입 추가
src/shared/hooks/kakaoMap/useKakaoCourseMap.ts, src/shared/utils/loadKakaoSdk.ts, src/shared/types/kakao.d.ts
useKakaoCourseMap 훅 추가(places 기반 오버레이·클릭 처리); SDK 로드에서 env 키(NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY) 검증; kakao.maps 타입 선언 확장
버블·버블셋 컴포넌트 변경
src/shared/components/container/LocationBubble.tsx, src/shared/components/set/LocationBubbleSet.tsx
LocationBubble/LocationBubbleSet이 place: CoursePlace 객체 기반으로 네비게이션·렌더링 변경
아이콘 및 리소스 추가
src/shared/icons/iconNames.ts, src/shared/icons/index.ts
RedMapPin 아이콘 추가 및 SVG 임포트
Next 이미지 설정 변경
next.config.ts
images.domains에서 maps.googleapis.com 제거, places.googleapis.com에 대한 remotePatterns 추가

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
Loading
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})
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 깊게 볼 항목:
    • src/pages/map/index.tsx — 뮤테이션 파라미터 구성·에러 처리·네비게이션
    • src/shared/hooks/kakaoMap/useKakaoCourseMap.ts — 오버레이 생성/클린업 및 클릭 좌표 계산
    • API 클라이언트/타입 일관성: postRecommendCourse, getCourseSession, recommend/courseSession 타입 매칭
    • FullMap/ResultMap/ResultList의 props 및 라우팅 쿼리 일관성
    • loadKakaoSdk의 env 키 검증 로직

Possibly related PRs

Suggested reviewers

  • jjangminii
  • KongMezu

Poem

🐰 깡충깡충 지도 위로 폴짝,
AI가 길을 속삭여 세션을 만들고,
핀 하나 찍히면 당근 들고 출발해요 —
맵 위에 당근길, 코스가 반짝! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 AI 코스 추천 API 연동 및 Kakao 지도 리팩토링을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 변경사항이 이슈 #108의 지도 페이지 API 연동 요구사항을 충족합니다: POST /api/courses/recommend를 통한 AI 추천 요청, GET /api/courses/session/{sessionId}를 통한 결과 조회, React Query 기반 캐싱, Kakao 지도 훅 통합, 서버 데이터 기반 UI 렌더링이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #108의 지도 페이지 API 연동 범위 내에 있습니다. 이미지 원본 소스 설정(next.config.ts), 타입 선언(kakao.d.ts), 환경변수 처리(loadKakaoSdk.ts) 등의 지원 변경사항도 모두 핵심 기능 구현에 필요한 범위 내 변경입니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션을 포함하고 있으며, 작업 내용, 개선 포인트, 추후 작업, 이슈 연결, 리뷰 포인트가 명확하게 작성되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch api/#108/kakao-map-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@skyblue1232 skyblue1232 changed the title Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결 ✨Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결 Nov 11, 2025
@github-actions github-actions bot added the comment 필요한 주석 추가 및 변경 label Nov 11, 2025
@github-actions
Copy link
Copy Markdown

🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message.
Please verify that they are correct before merging.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 883d393 and c8f80fe.

⛔ Files ignored due to path filters (1)
  • src/shared/icons/source/RedMapPin.svg is 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.tsx
  • src/pages/map/index.tsx
  • src/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: 원본 리뷰 의견이 부정확합니다.

이것은 자동 생성 파일의 수동 수정이 아닙니다. 올바른 프로세스를 따릅니다:

  1. RedMapPin.svg 파일이 src/shared/icons/source/ 디렉토리에 추가됨 (commit 6296b1a)
  2. generate-icon-list.ts 스크립트가 자동으로 SVG 파일들을 검색
  3. 스크립트가 index.ts 파일을 완전히 재생성 (동일 commit 6296b1a)
  4. 결과 파일에 자동으로 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: 제어 컴포넌트 패턴과 로딩 상태 구현이 잘 되어 있습니다.

  • 제어 컴포넌트 패턴이 올바르게 적용되어 valueonChange로 상태를 부모에서 관리합니다.
  • isLoading 상태에 따라 입력란과 버튼이 적절히 비활성화됩니다.
  • 로딩 시 시각적 피드백(투명도, 커서, 아이콘 색상)이 명확하게 제공됩니다.
  • 접근성을 위한 aria 레이블과 설명이 잘 구현되어 있습니다.

Comment on lines +1 to +9
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;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파일명 오타를 수정하세요.

파일명이 getCourseSettion.ts로 되어 있는데 "Session"이 올바른 철자입니다. 타입 파일(courseSettion.ts)도 동일한 오타가 있다면 함께 수정이 필요합니다.

파일명 변경:

  • getCourseSettion.tsgetCourseSession.ts
  • courseSettion.tscourseSession.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).

Comment on lines +1 to +9
export interface CoursePlace {
placeId: number;
name: string;
address: string;
latitude: number;
longitude: number;
description: string;
placeImg: string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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=tsx

Length 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.ts

Length 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.

Comment on lines +1 to +24
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[];
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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=tsx

Length 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 f

Length 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.tscourseSession.ts

현재 파일명이 courseSettion.ts로 되어 있는데, 올바른 철자는 courseSession.ts입니다. 이 오타로 인해 5개 파일의 import 문과 관련 API 함수명도 함께 수정해야 합니다.

수정이 필요한 파일들:

  • src/shared/api/course/types/courseSettion.tscourseSession.ts로 파일명 변경
  • src/shared/api/course/api/getCourseSettion.tsgetCourseSession.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.

Comment on lines +2 to +16
{ 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: '자가차' },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 클래스의 타입 정의를 개선할 수 있습니다.

두 가지 관찰 사항:

  1. 전역 Map 섀도잉: Biome이 경고를 발생시키지만, 이는 Kakao Maps API가 실제로 Map이라는 이름을 사용하기 때문에 불가피합니다. 외부 라이브러리의 API를 타이핑하는 것이므로 이름을 변경할 수 없습니다.

  2. 느슨한 타입: 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

📥 Commits

Reviewing files that changed from the base of the PR and between c8f80fe and 120cc93.

📒 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,
});

Comment on lines +19 to +59
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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup 함수 누락으로 인한 메모리 누수 위험이 있습니다.

현재 effect는 다음 리소스들을 정리하지 않습니다:

  1. 기존 지도 인스턴스가 파괴되지 않아 places 변경 시 새 지도가 계속 생성됨
  2. 생성된 오버레이들이 제거되지 않음
  3. 핀에 추가된 이벤트 리스너가 제거되지 않음

다음과 같이 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.

Suggested change
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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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을 반환할 경우 타입 불일치로 인한 런타임 에러가 발생할 수 있습니다. 다음 중 하나를 선택하여 해결하세요:

해결 방안:

  1. API 응답을 확인하여 실제 nullability를 파악
  2. 올바른 타입 정의를 가진 하나의 파일로 통합 (recommendTypes.ts 권장)
  3. 중복 파일 제거 및 모든 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.ts
src/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

📥 Commits

Reviewing files that changed from the base of the PR and between 120cc93 and 902a002.

📒 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: 오버레이 정리 로직이 추가되었습니다.

이전 리뷰에서 지적된 오버레이 메모리 누수 문제가 부분적으로 해결되었습니다. 오버레이는 정리되고 있으나, 이벤트 리스너 정리는 여전히 누락되어 있습니다(별도 코멘트 참조).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 902a002 and 5de0d5b.

📒 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.tsx
  • src/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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 중복 제출 방지 개선 권장

isPendingCourseInputSection에 전달되어 버튼을 비활성화하지만, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5de0d5b and ac7f16b.

📒 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.tsx
  • src/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에 잘 반영되어 사용자 경험이 개선되었습니다.

Comment on lines +29 to +30
userLatitude: 37.4985,
userLongitude: 126.7822,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

하드코딩된 좌표 사용

사용자 위치 대신 고정된 좌표 (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.

Comment on lines +34 to +42
onSuccess: (res) => {
if (res.isSuccess) {
router.push(`/map/result?sessionId=${res.result.sessionId}`);
}
},
onError: (err) => {
console.error('AI 코스 추천 실패:', err);
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

에러 및 실패 응답 처리 부족

현재 에러 처리에 두 가지 문제가 있습니다:

  1. 네트워크 에러 시 사용자 피드백 없음: onError에서 console.error만 하고 사용자에게는 아무 알림이 없어 버튼이 동작하지 않는 것처럼 보입니다.
  2. 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.

Suggested change
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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 인터페이스 중복 정의 및 타입 불일치 문제

이 파일의 CoursePlaceplaceId: 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=tsx
src/shared/api/course/types/courseSession.ts (2)

1-9: CoursePlace 중복 정의 - recommendTypes.ts와 nullability 불일치

이 파일의 CoursePlaceplaceId: 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을 반환할 경우 런타임 타입 안전성이 깨집니다.

권장 조치:

  1. API 실제 응답 스펙 확인 후 올바른 nullability 결정
  2. 두 정의를 하나의 파일로 통합 (recommendTypes.ts 권장)
  3. 모든 import를 통합된 정의로 변경

1-25: 파일명 오타 확인 필요

과거 리뷰 코멘트에서 지적된 대로, 파일명이 courseSettion.ts로 오타가 있을 수 있습니다. 올바른 철자는 courseSession.ts입니다.

만약 실제 파일명이 오타를 포함하고 있다면, 파일명 변경과 함께 관련된 모든 import 경로(5개 파일)와 API 함수명(getCourseSettiongetCourseSession)도 일괄 수정이 필요합니다.

🧹 Nitpick comments (1)
src/shared/components/map/result/components/FullMap.tsx (1)

14-15: 선택적 리팩토링: 중복 상태 제거 고려

selectedPlace에 이미 latitudelongitude가 포함되어 있으므로, 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

📥 Commits

Reviewing files that changed from the base of the PR and between ac7f16b and 8acf7e9.

📒 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.tsx
  • src/pages/map/location/[placeId].tsx
  • src/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: 컴포넌트 통합이 잘 구현되었습니다.

CourseSelectGroupCourseInputSection에 props가 명확하게 전달되고, 로딩 상태(isPendingisLoading)가 올바르게 연결되어 사용자 경험을 개선하고 있습니다.


35-36: 코드가 API 응답 타입과 정확히 일치함

src/shared/api/course/types/recommendTypes.tsRecommendCourseResponse 타입 정의를 확인한 결과, 현재 코드의 필드명이 정확합니다:

  • res.success ✓ (실제 타입: success: boolean)
  • res.data.sessionId ✓ (실제 타입: data.sessionId: string)

이전 리뷰에서 언급된 res.isSuccessres.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 기반 동적 라우팅이 올바르게 구현되었습니다.

placeId prop을 추가하고 이를 통해 /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: 이전 리뷰 코멘트 해결 완료

isLoadingisError 상태를 올바르게 추출하고, 조건부 렌더링으로 사용자 피드백을 제공하고 있습니다. 이전 리뷰에서 지적된 문제가 완전히 해결되었습니다.

Also applies to: 62-68

{viewMode === 'list' ? (
<ResultList places={data?.places ?? []} />
) : (
<ResultMap sessionId={sessionId!} places={data?.places ?? []} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

sessionId 타입 안정성 개선 필요

Line 89에서 sessionId!를 사용하고 있지만, 사용자가 쿼리 파라미터 없이 /map/result로 직접 접근하는 경우 sessionIdundefined일 수 있습니다.

다음과 같이 명시적 가드를 추가하세요:

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

Suggested change
<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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로딩 및 에러 상태 처리 누락

useCourseSession에서 isLoadingisError 상태를 받아오지 않아 다음 문제가 발생합니다:

  • 데이터 로딩 중 사용자에게 피드백이 없음 (빈 지도만 표시)
  • 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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' 여부만 보고 있습니다.

  • addressdescription이 없는 경우 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 사용은 안전하지만 정교화 여지 있음 (선택 사항)

이 부분은 기능적으로는 잘 동작하지만, 몇 가지 개선 여지가 있습니다.

  1. DOM click 리스너 정리
    overlay는 cleanup에서 setMap(null)로 제거하지만, el.addEventListener('click', ...)로 등록한 DOM 리스너는 따로 해제하지 않고 있습니다. 현재 구조에서는 overlay/DOM 노드가 함께 GC되어 큰 문제는 없겠지만, 메모리 관리 관점에서 리스너 참조도 함께 제거해 두면 더 안전합니다.

    간단하게는 핸들러를 변수에 담아두고 cleanup에서 el.removeEventListener('click', handler)를 호출하도록 overlaysRef와 함께 관리하는 방식을 고려해볼 수 있습니다.

  2. innerHTML로 고정 SVG 삽입 (XSS 측면)
    el.innerHTML = \<svg ...>...`부분은 완전히 고정된 문자열이라 현재로서는 XSS 위험은 사실상 없습니다. 다만 정적 분석 도구에서 계속 경고를 띄운다면,document.createElementNS로 SVG를 생성해서 appendChild` 하는 방식으로 바꾸는 것도 한 가지 방법입니다.

둘 다 지금 당장 필수는 아니지만, 향후 맵 관련 로직이 더 복잡해질 것을 고려하면 한 번쯤 리팩터링 후보로 가져가도 좋을 것 같습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8acf7e9 and 1817266.

📒 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].tsx
  • src/shared/components/map/result/components/ResultList.tsx
  • src/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을 완화하는 정도만 고려하시면 될 것 같습니다.

Comment on lines +4 to +14
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;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

이전 리뷰 피드백이 잘 반영되었습니다!

API 응답 검증 로직(data.success 체크 및 에러 throw)이 올바르게 추가되었습니다. 다만 추가로 고려할 수 있는 개선 사항이 있습니다.

Minor: data.data null 체크 추가 고려

data.successtrue이더라도 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.

Comment on lines +1 to +59
'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>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

ResultList에서 불필요한 'use client' 지시자 및 클릭 영역 접근성 개선 제안

  1. 'use client' 제거 (Pages Router 컨벤션 위반)
    이 프로젝트는 Pages Router를 사용하고 있어, 이전 PR에서와 마찬가지로 'use client' 지시자를 사용하지 않는 방향으로 리뷰가 진행되었습니다. 이 컴포넌트는 Pages Router 환경에서 기본적으로 클라이언트에서 실행되므로 'use client'는 의미가 없고, 컨벤션상 제거하는 편이 좋겠습니다.

    -'use client';
    -
    import { cn } from '@/shared/lib';
  2. 클릭 가능한 카드의 접근성 보완 (선택)
    현재 각 장소는 <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 2

Length 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 -20

Length 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 f

Length 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 -10

Length 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 -5

Length 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 -c

Length 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 -l

Length of output: 2


🏁 Script executed:

#!/bin/bash
# Look at the role attribute usage patterns
rg "role=[\"']list[\"']|role=[\"']listitem[\"']" "src/shared/components" -C 2

Length 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 -10

Length of output: 27


KongMezu
KongMezu previously approved these changes Nov 14, 2025
Copy link
Copy Markdown
Contributor

@KongMezu KongMezu left a comment

Choose a reason for hiding this comment

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

리뷰 한번 확인해주시고! 바로 approve하겠습니다..!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이거 기존 아이콘 MapPin 에 fill color 채워서 해도 되는데 확인 한번 해주세요!

};
}, [places, enableClick, mapRef]);

return mapInstanceRef.current;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

여기 return 시점에서 map instance 잘 받나요..?
return mapInstanceRef; 이렇게 반환하는게..안전하지 않을까요?

Comment on lines +64 to +86
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,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이거 메모리에 리스너가 계속 누적되진..않나요..?
중간에 한번 clean up 필요할 거 같습니다!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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]);

상위 컴포넌트에서 onPinClickuseCallback으로 감싸면 불필요한 재실행을 방지할 수 있습니다.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 24d75f3 and 4acf746.

📒 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)가 모두 수행되며, 배열도 초기화되어 메모리 누수를 방지합니다. 과거 리뷰에서 지적된 문제가 잘 해결되었습니다.

@skyblue1232
Copy link
Copy Markdown
Contributor Author

피드백 반영 완료했습니다!

@skyblue1232 skyblue1232 merged commit 830288d into develop Nov 15, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api api 연결 comment 필요한 주석 추가 및 변경 feat 새로운 기능 추가 / 퍼블리싱

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[api] 지도 페이지 api 연동

2 participants