-
Notifications
You must be signed in to change notification settings - Fork 1
✨ Feat: course result 페이지 및 지도 페이지 구현 #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
5fe4d88
333ff06
13a1658
9b1228a
a86412e
9dfcfc3
35424ae
2cef263
6a7e7f1
8520bc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"> | ||
| <Header | ||
| title="코스 추천" | ||
| onClick={() => router.push('/map/result?from=map')} | ||
| /> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <FullMap /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Map; | ||
| 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" | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||
| 'use client'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| import { cn } from '@/shared/lib'; | ||||||
| import LocationCard from '@/shared/components/container/LocationCard'; | ||||||
|
||||||
| 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> | ||||||
| ); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||
| import { useRef } from 'react'; | ||||||
| import { useRouter } from 'next/router'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Next.js App Router에서는
다음과 같이 수정하세요: -import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희는 page Router이라 next/router가 맞을거같아요 하지만 'use client';는 수정해야할 것 같네요
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jjangminii 네, 맞습니다! Pages Router를 사용하시는 거였군요. 정정해주셔서 감사합니다. Pages Router에서는:
Pages Router에서는 해당 파일( ✏️ Learnings added
|
||||||
| 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> | ||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| </section> | ||||||
| ); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use client'; | ||
|
|
||
| import Tag from '@/shared/components/tag/Tag'; | ||
|
||
| 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> | ||
| ); | ||
| } | ||
| 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'); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| if (router.query.from === 'map') { | ||
| setViewMode('map'); | ||
| } | ||
| }, [router.query.from]); | ||
|
Comment on lines
+17
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ); | ||
| } | ||
| 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', | ||
| }, | ||
| ]; |
| 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 }, | ||
| ]; |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이부분은 어떤 역할인가요? 왜 px로 되어잇는지 궁금합니다 |
||
|
|
||
| window.kakao.maps.event.addListener(marker, 'click', () => { | ||
| infoWindow.open(map, marker); | ||
| }); | ||
| }); | ||
| }); | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| loadKakaoSdk(initMap); | ||
| }, [mapRef, locations]); | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TS 전역 타입 오류 위험: 현재 -declare global {
- interface Window {
- kakao: typeof kakao;
- }
-}
-export {};
+declare global {
+ interface Window {
+ kakao?: {
+ maps?: unknown; // 필요 시 세부 타입 확장
+ };
+ }
+}
+export {};추후 필요 범위에 맞춰 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스크립트 로딩 완료 여부를 확인해야 합니다. 스크립트 태그가 이미 존재하는 경우 즉시 callback을 호출하지만, 이 시점에 SDK 로딩이 완료되지 않았을 수 있습니다. 이로 인해 다음과 같이 수정하여 SDK 로딩 상태를 확인하세요: if (existing) {
- callback();
+ if (window.kakao && window.kakao.maps) {
+ callback();
+ } else {
+ existing.addEventListener('load', callback);
+ }
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분 bg-mint-300 으로 유틸 설정해놔서 이거 쓰심 될 거 같습니다!