Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
35 changes: 35 additions & 0 deletions public/assets/bannerMap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
19 changes: 19 additions & 0 deletions src/pages/map/result/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

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"
/>
);
}
46 changes: 46 additions & 0 deletions src/pages/map/result/components/ResultList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'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'
)}
>
{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>
))}

<style jsx global>{`
/* Chrome, Safari, Opera */
::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
}
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.

이거 수민님이 따로 global.css에서 작업항걸로 아는데 한번 확인해주세요


/* Firefox */
* {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
`}</style>
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

전역 스크롤바 숨김은 접근성 저해—컨테이너 단위로 한정하세요

<style jsx global>로 전역 ::-webkit-scrollbar/* { scrollbar-width: none }를 막아 앱 전반 스크롤바가 사라집니다. 스크린리더/키보드 사용자에게 치명적입니다. 래퍼에 한정된 클래스 적용으로 범위를 축소하세요.

-      <style jsx global>{`
-        /* Chrome, Safari, Opera */
-        ::-webkit-scrollbar {
-          width: 0 !important;
-          height: 0 !important;
-        }
-
-        /* Firefox */
-        * {
-          scrollbar-width: none !important;
-          -ms-overflow-style: none !important;
-        }
-      `}</style>
+      <style jsx>{`
+        .no-scrollbar::-webkit-scrollbar {
+          width: 0;
+          height: 0;
+        }
+        .no-scrollbar {
+          scrollbar-width: none;
+          -ms-overflow-style: none;
+        }
+      `}</style>

그리고 래퍼에 클래스 추가:

-      className={cn(
+      className={cn(
         'grid grid-cols-2 gap-[1.2rem] w-full',
         'justify-items-center pb-[15rem]',
-        'overflow-y-scroll' 
+        'overflow-y-scroll no-scrollbar'
       )}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/map/result/components/ResultList.tsx around lines 31 to 43, the
current <style jsx global> hides scrollbars globally which breaks accessibility;
instead scope the scrollbar rules to the component wrapper (e.g. add a className
like "resultListWrapper" to the outermost wrapper element) and move the CSS out
of global so selectors target .resultListWrapper ::-webkit-scrollbar and
.resultListWrapper { scrollbar-width: none; -ms-overflow-style: none; } (remove
the global * and avoid unnecessary !important), ensuring only this container
hides its scrollbars.

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.

스크롤바 숨김 경우 제가 최근에 올린 PR에서
global.css 로 빼놔서 확인해보시고, 머지되면 리팩토링 해보시면 될 거 같습니다!

</div>
);
}
38 changes: 38 additions & 0 deletions src/pages/map/result/components/ResultMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';
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"
>
<h2 id="map-title" className="sr-only">
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.

여기 피그마에서 정해진 폰트 있지 않나요??
유틸 클래스 명시 없이 h2태그로만 묶어도 괜찮나요?

코스 추천 미리보기 지도
</h2>

<div
ref={mapRef}
role="application"
className="w-full h-full"
/>

<button
onClick={() => router.push('/map/result/Map?from=map')}
aria-labelledby="전체화면 지도로 보기"
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>
);
}
90 changes: 90 additions & 0 deletions src/pages/map/result/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

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 Popup from '@/shared/components/container/Popup';
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 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');
}

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 h-screen overflow-hidden">
<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) => {
const nextMode = prev === 'list' ? 'map' : 'list';
console.log(`${prev} → ${nextMode}`);
return nextMode;
});
}}
/>

<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 && (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center">
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/shared/set/PopupSet.tsx 에
오버레이랑 같이 뜨도록 해놓은거 있어서

이거 이용하심 될 거 같습니다!
팝업 PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

감사합니다!!

<Popup
text="새로고침 시 결과가 초기화됩니다."
onClose={handlePopupClose}
/>
</div>
)}
</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 },
];
46 changes: 46 additions & 0 deletions src/shared/hooks/kakao/useKakaoMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'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) 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_",
"PressStamp",
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/PressStamp.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.
2 changes: 1 addition & 1 deletion src/shared/icons/source/Stamp.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.

Loading
Loading