Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/assets/sample1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/pages/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
'use client';
import Image from 'next/image';
import { cn } from '@/shared/lib';
import { ControlBar } from '@/shared/components';
Expand Down
17 changes: 17 additions & 0 deletions src/pages/map/result/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Header } from '@/shared/components';
import router from 'next/router';
import FullMap from '@/pages/map/result/components/FullMap';

const Map = () => {
return (
<div className="w-full h-[100vh] bg-[#46d1cd] overflow-hidden">
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.

이 부분 bg-mint-300 으로 유틸 설정해놔서 이거 쓰심 될 거 같습니다!

<Header
title="코스 추천"
onClick={() => router.push('/map/result?from=map')}
/>
<FullMap />
</div>
);
};

export default Map;
19 changes: 19 additions & 0 deletions src/pages/map/result/components/FullMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { useRef } from 'react';
import { useKakaoMap } from '@/shared/hooks/kakao/useKakaoMap';
import { MAP_LOCATIONS } from '@/shared/constants/map/result/mapLocations';

export default function FullMap() {
const mapRef = useRef<HTMLDivElement | null>(null);
useKakaoMap(mapRef, MAP_LOCATIONS);

return (
<div
ref={mapRef}
role="region"
aria-label="카카오 지도 전체 보기"
className="w-full h-full bg-gray-200 overflow-hidden"
/>
);
}
31 changes: 31 additions & 0 deletions src/pages/map/result/components/ResultList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';
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

Pages Router에서는 'use client' 지시어가 불필요합니다.

이 프로젝트는 Next.js App Router가 아닌 Pages Router를 사용하고 있습니다. Pages Router에서는 모든 컴포넌트가 기본적으로 클라이언트 컴포넌트로 동작하므로 'use client' 지시어를 제거해야 합니다.

다음과 같이 수정하세요:

-'use client';
-
 import { cn } from '@/shared/lib';

Based on learnings

📝 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
'use client';
import { cn } from '@/shared/lib';
🤖 Prompt for AI Agents
In src/pages/map/result/components/ResultList.tsx around line 1, remove the
top-level 'use client' directive because this project uses the Next.js Pages
Router where components are already client-side; simply delete that line and
ensure no other code depends on the directive so the file compiles correctly.


import { cn } from '@/shared/lib';
import LocationCard from '@/shared/components/container/LocationCard';
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.

이거 임포트 경로 @/shared/components 만 써도 됩니다

import { listData } from '@/shared/constants/map/result/listData';

export default function ResultList() {
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'
)}
>
{listData.map((place) => (
<div role="listitem" key={place.id} className="w-full">
<LocationCard
name={place.name}
address={place.address}
description={place.description}
variant="gray"
size="medium"
imageSrc={place.imageSrc}
/>
</div>
))}
</div>
);
}
33 changes: 33 additions & 0 deletions src/pages/map/result/components/ResultMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useRef } from 'react';
import { useRouter } from 'next/router';
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Oct 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Next.js App Router에서는 next/navigation을 사용하세요.

'use client' 컴포넌트에서 next/router를 import하고 있습니다. Next.js 13+ App Router를 사용하는 경우 next/navigationuseRouter를 사용해야 합니다.

다음과 같이 수정하세요:

-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
📝 Committable suggestion

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

Suggested change
import { useRouter } from 'next/router';
import { useRouter } from 'next/navigation';
🤖 Prompt for AI Agents
In src/pages/map/result/components/ResultMap.tsx around line 3, the component
currently imports useRouter from 'next/router' which is incorrect for App Router
client components; replace that import to use useRouter from 'next/navigation'
(ensure the file has 'use client' at the top if it’s a client component), update
any router method calls that differ between the two hooks to their
next/navigation equivalents, and remove the old 'next/router' import.

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.

저희는 page Router이라 next/router가 맞을거같아요 하지만 'use client';는 수정해야할 것 같네요

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.

@jjangminii 네, 맞습니다! Pages Router를 사용하시는 거였군요. 정정해주셔서 감사합니다.

Pages Router에서는:

  • next/router 사용이 맞습니다
  • 'use client' 지시문은 App Router 전용이므로 제거해야 합니다

Pages Router에서는 pages/ 디렉토리 하위의 모든 컴포넌트가 자동으로 클라이언트 측 라우팅을 지원하므로 'use client' 지시문이 필요하지 않습니다. 이 지시문은 Next.js 13+ App Router의 Server Components와 Client Components를 구분하기 위한 것입니다.

해당 파일(src/pages/map/result/components/ResultMap.tsx)에서 첫 줄의 'use client';를 제거하시면 됩니다.


✏️ Learnings added
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.305Z
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

import { useKakaoMap } from '@/shared/hooks/kakao/useKakaoMap';
import { MAP_LOCATIONS } from '@/shared/constants/map/result/mapLocations';

export default function ResultMap() {
const mapRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();

useKakaoMap(mapRef, MAP_LOCATIONS);

return (
<section
className="relative w-full h-[43.6rem] rounded-[2rem] overflow-hidden bg-gray-200"
role="region"
aria-labelledby="map-title"
>
<div
ref={mapRef}
role="application"
className="w-full h-full"
/>

<button
onClick={() => router.push('/map/result/Map?from=map')}
aria-label="전체화면 지도로 보기"
className="absolute right-[1.2rem] bottom-[1.4rem] bg-pink-200 border border-pink-300 text-white text-title-sm px-[2.2rem] py-[1.2rem] rounded-[2rem]"
>
전체화면 보기
</button>
</section>
);
}
34 changes: 34 additions & 0 deletions src/pages/map/result/components/TagGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import Tag from '@/shared/components/tag/Tag';
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.

이것도 경로 수정해 주세요

import { cn } from '@/shared/lib';

interface TagGroupProps {
viewMode: 'list' | 'map';
onToggleView: () => void;
}

export default function TagGroup({ viewMode, onToggleView }: TagGroupProps) {
const tags = ['#데이트', '#당일치기', '#도보'];

return (
<div
className={cn(
'flex items-center justify-between w-full gap-[0.4rem] flex-wrap'
)}
>
<div className="flex gap-[1.4rem]">
{tags.map((tag) => (
<Tag key={tag} label={tag} variant="hash" />
))}
</div>

<Tag
label={viewMode === 'list' ? '지도로 보기' : '리스트로 보기'}
icon={viewMode === 'list' ? 'MapPin_' : 'ListButton'}
variant="toggle"
onClick={onToggleView}
/>
</div>
);
}
84 changes: 84 additions & 0 deletions src/pages/map/result/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { cn } from '@/shared/lib';
import { ControlBar } from '@/shared/components';
import { BottomNav } from '@/shared/components/tab/BottomNav';
import { PopupSet } from '@/shared/components';
import TagGroup from '@/pages/map/result/components/TagGroup';
import ResultList from '@/pages/map/result/components/ResultList';
import ResultMap from '@/pages/map/result/components/ResultMap';

export default function CourseResultPage() {
const router = useRouter();
const [showPopup, setShowPopup] = useState(false);
const [viewMode, setViewMode] = useState<'list' | 'map'>('list');

useEffect(() => {
const hasSeenPopup = localStorage.getItem('seenCoursePopup');
if (!hasSeenPopup) {
setShowPopup(true);
localStorage.setItem('seenCoursePopup', 'true');
}
}, []);

useEffect(() => {
if (router.query.from === 'map') {
setViewMode('map');
}
}, [router.query.from]);
Comment on lines +17 to +31
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.

query 체크랑 localstorage 체크 이렇게 2개 useeffect로 분리하는게 의존성 문제에서도 더 좋을 거 같습니다!


const handlePopupClose = () => setShowPopup(false);

return (
<div className="relative bg-white flex flex-col min-h-screen pb-[12rem] no-scrollbar">
<ControlBar
isLoggedIn={false}
onLogin={() => {}}
userName="글다"
className="fixed top-[1rem] left-0 right-0 z-50 px-[2rem]"
/>

<main className="relative w-full h-full pt-[6.3rem] flex flex-col overflow-hidden">
<div className="px-[2.4rem]">
<section className="mb-[2rem] text-center">
<Image
src="/assets/bannerMap.svg"
alt="여행 결과 배너 이미지"
width={354}
height={79}
className="w-full h-auto object-cover block"
/>
</section>

<TagGroup
viewMode={viewMode}
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 /> : <ResultMap />}
</section>
</div>
</main>

<BottomNav />

{showPopup && (
<PopupSet
text="새로고침 시 결과가 초기화됩니다."
onClose={handlePopupClose}
/>
)}
</div>
);
}
65 changes: 65 additions & 0 deletions src/shared/constants/map/result/listData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export const listData = [
{
id: 1,
name: '장소 이름 1',
address: '서울시 강남구',
description: '서울의 중심에서 즐기는 고급 디저트 카페입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 2,
name: '장소 이름 2',
address: '부산시 해운대구',
description: '해운대 바다가 보이는 감성 카페입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 3,
name: '장소 이름 3',
address: '제주시 애월읍',
description: '애월 해변을 바라보며 커피 한 잔.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 4,
name: '장소 이름 4',
address: '광주시 서구',
description: '아늑한 분위기의 브런치 레스토랑입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 5,
name: '장소 이름 5',
address: '대전시 유성구',
description: '정원 뷰가 매력적인 루프탑 카페입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 6,
name: '장소 이름 6',
address: '인천시 연수구',
description: '감각적인 인테리어의 디저트 카페입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 7,
name: '가톨릭대학교 캠퍼스',
address: '경기도 부천시',
description: '가톨릭대학교 부천 캠퍼스 전경입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 8,
name: '부천 관광지 A',
address: '경기도 부천시',
description: '부천의 인기 관광지 중 하나입니다.',
imageSrc: '/assets/sample1.jpg',
},
{
id: 9,
name: '장소 이름 9',
address: '속초시 영랑동',
description: '바다 앞 카페로 일몰이 아름답기로 유명합니다.',
imageSrc: '/assets/sample1.jpg',
},
];
7 changes: 7 additions & 0 deletions src/shared/constants/map/result/mapLocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const MAP_LOCATIONS = [
{ name: '한국만화박물관', lat: 37.505652, lng: 126.776922 },
{ name: '원미산 진달래동산', lat: 37.495293, lng: 126.783589 },
{ name: '상동호수공원', lat: 37.502117, lng: 126.753985 },
{ name: '부천자유시장', lat: 37.486203, lng: 126.781343 },
{ name: '부천아트벙커 B39', lat: 37.489797, lng: 126.764708 },
];
49 changes: 49 additions & 0 deletions src/shared/hooks/kakao/useKakaoMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';
import { useEffect } from 'react';
import { loadKakaoSdk } from '@/shared/utils/loadKakaoSdk';

interface Location {
name: string;
lat: number;
lng: number;
}

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(() => {
const map = new window.kakao.maps.Map(mapRef.current!, {
center: new window.kakao.maps.LatLng(37.498, 126.783),
level: 5,
});

locations.forEach((p) => {
const marker = new window.kakao.maps.Marker({
position: new window.kakao.maps.LatLng(p.lat, p.lng),
map,
});

const infoWindow = new window.kakao.maps.InfoWindow({
content: `<div style="padding:5px 10px; font-size:12px;">${p.name}</div>`,
});
Comment on lines +36 to +38
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.

이부분은 어떤 역할인가요? 왜 px로 되어잇는지 궁금합니다


window.kakao.maps.event.addListener(marker, 'click', () => {
infoWindow.open(map, marker);
});
});
});
};

loadKakaoSdk(initMap);
}, [mapRef, locations]);
}
1 change: 1 addition & 0 deletions src/shared/icons/iconNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const iconNames = [
"HeartStraight",
"HouseSimple",
"KakaoIcon",
"ListButton",
"MapPin",
"MapPin_",
"NextButton",
Expand Down
1 change: 1 addition & 0 deletions src/shared/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import './source/GoogleIcon.svg';
import './source/HeartStraight.svg';
import './source/HouseSimple.svg';
import './source/KakaoIcon.svg';
import './source/ListButton.svg';
import './source/MapPin.svg';
import './source/MapPin_.svg';
import './source/NextButton.svg';
Expand Down
1 change: 1 addition & 0 deletions src/shared/icons/source/ListButton.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/shared/types/kakao.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare global {
interface Window {
kakao: typeof kakao;
}
}
export {};
Comment on lines +1 to +6
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

TS 전역 타입 오류 위험: typeof kakao는 외부 선언 없으면 실패

현재 typeof kakao는 Kakao SDK가 타입을 제공하지 않으면 컴파일 에러를 유발합니다. 최소 안전 타입으로 교체하세요.

-declare global {
-  interface Window {
-    kakao: typeof kakao;
-  }
-}
-export {};
+declare global {
+  interface Window {
+    kakao?: {
+      maps?: unknown; // 필요 시 세부 타입 확장
+    };
+  }
+}
+export {};

추후 필요 범위에 맞춰 maps.Map, LatLng, Marker, InfoWindow, event.addListener 등만 정밀 선언하면 DX가 개선됩니다.

📝 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
declare global {
interface Window {
kakao: typeof kakao;
}
}
export {};
declare global {
interface Window {
kakao?: {
maps?: unknown; // 필요 시 세부 타입 확장
};
}
}
export {};
🤖 Prompt for AI Agents
In src/shared/types/kakao.d.ts lines 1-6, the Window.kakao property is typed as
"typeof kakao" which fails if there is no external kakao declaration; replace
that with a safe minimal type (e.g., any or an explicit minimal interface) to
avoid TS compile errors, then optionally add more specific declarations later
for maps.Map, LatLng, Marker, InfoWindow, and event.addListener as needed.

16 changes: 16 additions & 0 deletions src/shared/utils/loadKakaoSdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function loadKakaoSdk(callback: () => void) {
const scriptId = 'kakao-map-sdk';
const existing = document.getElementById(scriptId);

if (existing) {
callback();
return;
}
Comment on lines +5 to +8
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

스크립트 로딩 완료 여부를 확인해야 합니다.

스크립트 태그가 이미 존재하는 경우 즉시 callback을 호출하지만, 이 시점에 SDK 로딩이 완료되지 않았을 수 있습니다. 이로 인해 useKakaoMap 훅에서 window.kakao에 접근할 때 경쟁 상태(race condition)가 발생할 수 있습니다.

다음과 같이 수정하여 SDK 로딩 상태를 확인하세요:

  if (existing) {
-   callback();
+   if (window.kakao && window.kakao.maps) {
+     callback();
+   } else {
+     existing.addEventListener('load', callback);
+   }
    return;
  }
📝 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
if (existing) {
callback();
return;
}
if (existing) {
if (window.kakao && window.kakao.maps) {
callback();
} else {
existing.addEventListener('load', callback);
}
return;
}
🤖 Prompt for AI Agents
In src/shared/utils/loadKakaoSdk.ts around lines 5 to 8, the current code calls
callback immediately when a script tag already exists, but that may be before
the SDK has finished loading and causes a race when accessing window.kakao;
update the logic so that if the script tag already exists you check whether
window.kakao is present and ready and call callback immediately only if it is,
otherwise attach an onload (and onerror) handler to that existing script to
invoke the callback once the SDK finishes loading (and handle/report load
failure); similarly when creating a new script, ensure you set onload/onerror
handlers and only call callback after successful load.


const script = document.createElement('script');
script.id = scriptId;
script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_KEY}&autoload=false`;
script.async = true;
script.onload = callback;
document.head.appendChild(script);
}
Loading
Loading