-
Notifications
You must be signed in to change notification settings - Fork 1
refactor: 8호선 데이터, 이용약관/개인정보처리방침 추가 및 지도 컴포넌트 리팩토링 #33
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 all commits
ba6b1a8
392b646
24043bc
35096e3
5dfa616
b30f93a
c0c3d91
4495a92
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 | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,15 +1,15 @@ | ||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import { useState, useMemo, Suspense } from 'react'; | ||||||||||||||||||||||||||||||||||||||
| import { useRouter, useSearchParams } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||
| import { useRouter, useSearchParams } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||
| import Image from 'next/image'; | ||||||||||||||||||||||||||||||||||||||
| import KakaoMapRecommend from '@/components/map/kakaoMapRecommend'; | ||||||||||||||||||||||||||||||||||||||
| import { useRecommend } from '@/hooks/api/query/useRecommend'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function RecommendContent() { | ||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||
| const searchParams = useSearchParams(); | ||||||||||||||||||||||||||||||||||||||
| const meetingId = searchParams.get('meetingId') || ''; | ||||||||||||||||||||||||||||||||||||||
| const meetingId = searchParams.get(`meetingId`) || ''; | ||||||||||||||||||||||||||||||||||||||
| const midPlace = searchParams.get('midPlace') || ''; | ||||||||||||||||||||||||||||||||||||||
| const lat = searchParams.get('lat'); | ||||||||||||||||||||||||||||||||||||||
| const lng = searchParams.get('lng'); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -21,23 +21,25 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| // 현재 선택된 장소 ID (기본값: 첫 번째) | ||||||||||||||||||||||||||||||||||||||
| const [selectedPlaceId, setSelectedPlaceId] = useState<number>(1); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 선택된 카테고리 (기본값: localStorage의 meetingCategory) | ||||||||||||||||||||||||||||||||||||||
| const [selectedCategory, setSelectedCategory] = useState<string>(() => { | ||||||||||||||||||||||||||||||||||||||
| // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용) | ||||||||||||||||||||||||||||||||||||||
| const meetingCategory = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||
| const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`); | ||||||||||||||||||||||||||||||||||||||
| return cachedCategory || ''; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| if (cachedCategory) { | ||||||||||||||||||||||||||||||||||||||
| return cachedCategory; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 카테고리 변경 핸들러 | ||||||||||||||||||||||||||||||||||||||
| const handleCategoryChange = (category: string) => { | ||||||||||||||||||||||||||||||||||||||
| setSelectedCategory(category); | ||||||||||||||||||||||||||||||||||||||
| setSelectedPlaceId(1); // 카테고리 변경 시 첫 번째 장소 선택 | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||||||||
| }, [meetingId]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 장소 추천 API 호출 | ||||||||||||||||||||||||||||||||||||||
| const { data: recommendData, isLoading, isError } = useRecommend({ | ||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||
| data: recommendData, | ||||||||||||||||||||||||||||||||||||||
| isLoading, | ||||||||||||||||||||||||||||||||||||||
| isError, | ||||||||||||||||||||||||||||||||||||||
| } = useRecommend({ | ||||||||||||||||||||||||||||||||||||||
| meetingId, | ||||||||||||||||||||||||||||||||||||||
| midPlace, | ||||||||||||||||||||||||||||||||||||||
| category: selectedCategory, | ||||||||||||||||||||||||||||||||||||||
| category: meetingCategory, | ||||||||||||||||||||||||||||||||||||||
| page: 1, | ||||||||||||||||||||||||||||||||||||||
| size: 15, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -61,7 +63,6 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||
| }, [recommendData]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const handleBack = () => { | ||||||||||||||||||||||||||||||||||||||
| router.back(); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,7 +90,7 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 모바일 전용 지도 (작게 표시) */} | ||||||||||||||||||||||||||||||||||||||
| <div className="bg-gray-1 relative block aspect-video h-93.5 md:hidden"> | ||||||||||||||||||||||||||||||||||||||
| <div className="bg-gray-1 relative block h-93.5 md:hidden"> | ||||||||||||||||||||||||||||||||||||||
| <KakaoMapRecommend | ||||||||||||||||||||||||||||||||||||||
| className="h-full w-full" | ||||||||||||||||||||||||||||||||||||||
| midPlace={midPlace} | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,8 +99,6 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| places={places} | ||||||||||||||||||||||||||||||||||||||
| selectedPlaceId={selectedPlaceId} | ||||||||||||||||||||||||||||||||||||||
| onSelectPlace={setSelectedPlaceId} | ||||||||||||||||||||||||||||||||||||||
| selectedCategory={selectedCategory} | ||||||||||||||||||||||||||||||||||||||
| onCategoryChange={handleCategoryChange} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -118,64 +117,68 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-4 md:gap-5"> | ||||||||||||||||||||||||||||||||||||||
| {places.map((place) => ( | ||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||
| key={place.id} | ||||||||||||||||||||||||||||||||||||||
| onClick={() => setSelectedPlaceId(place.id)} | ||||||||||||||||||||||||||||||||||||||
| className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${ | ||||||||||||||||||||||||||||||||||||||
| selectedPlaceId === place.id | ||||||||||||||||||||||||||||||||||||||
| ? 'border-blue-5 border-2' // 선택 시 파란 테두리 | ||||||||||||||||||||||||||||||||||||||
| : 'border-gray-2 hover:bg-gray-1 bg-white' | ||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {/* 상단: 이름 및 카테고리 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start justify-between gap-2"> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-wrap items-center gap-2 px-0.5 flex-1 min-w-0"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-xl font-semibold break-words">{place.name}</span> | ||||||||||||||||||||||||||||||||||||||
| <span className="bg-gray-2 text-gray-7 shrink-0 rounded px-2 py-px text-sm font-semibold"> | ||||||||||||||||||||||||||||||||||||||
| {place.category} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||
| key={place.id} | ||||||||||||||||||||||||||||||||||||||
| onClick={() => setSelectedPlaceId(place.id)} | ||||||||||||||||||||||||||||||||||||||
| className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${ | ||||||||||||||||||||||||||||||||||||||
| selectedPlaceId === place.id | ||||||||||||||||||||||||||||||||||||||
| ? 'border-blue-5 border-2' // 선택 시 파란 테두리 | ||||||||||||||||||||||||||||||||||||||
| : 'border-gray-2 hover:bg-gray-1 bg-white' | ||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+128
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. 선택/비선택 시 border 두께 변경으로 레이아웃 쉬프트 발생 가능. 선택된 카드는 🔧 수정 제안: 항상 border-2를 유지하고 색상만 변경 className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${
selectedPlaceId === place.id
- ? 'border-blue-5 border-2' // 선택 시 파란 테두리
- : 'border-gray-2 hover:bg-gray-1 bg-white'
+ ? 'border-blue-5 border-2'
+ : 'border-gray-2 hover:bg-gray-1 border-2 border-transparent bg-white'
}`}또는 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| {/* 상단: 이름 및 카테고리 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start justify-between gap-2"> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 px-0.5"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-xl font-semibold break-words"> | ||||||||||||||||||||||||||||||||||||||
| {place.name} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <span className="bg-gray-2 text-gray-7 shrink-0 rounded px-2 py-px text-sm font-semibold"> | ||||||||||||||||||||||||||||||||||||||
| {place.category} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| {/* 공유 아이콘 (이미지에 살짝 보임) */} | ||||||||||||||||||||||||||||||||||||||
| <button className="text-gray-5 shrink-0 cursor-pointer"> | ||||||||||||||||||||||||||||||||||||||
| <Image src="/icon/gray_share.svg" alt="공유" width={24} height={24} /> | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+140
to
+142
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. 공유 버튼에 공유 아이콘 버튼이 렌더링되지만 클릭 시 아무 동작도 하지 않습니다. 기능이 아직 구현되지 않았다면 비활성화 상태로 표시하거나 숨기는 것이 좋습니다. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| {/* 공유 아이콘 (이미지에 살짝 보임) */} | ||||||||||||||||||||||||||||||||||||||
| <button className="text-gray-5 cursor-pointer shrink-0"> | ||||||||||||||||||||||||||||||||||||||
| <Image src="/icon/gray_share.svg" alt="공유" width={24} height={24} /> | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 설명 */} | ||||||||||||||||||||||||||||||||||||||
| <p className="text-gray-6 text-[16px]">{place.description}</p> | ||||||||||||||||||||||||||||||||||||||
| {/* 설명 */} | ||||||||||||||||||||||||||||||||||||||
| <p className="text-gray-6 text-[16px]">{place.description}</p> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 전화번호 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="text-gray-8 flex items-center gap-2 text-[16px]"> | ||||||||||||||||||||||||||||||||||||||
| <Image src="/icon/phone.svg" alt="전화" width={20} height={20} /> | ||||||||||||||||||||||||||||||||||||||
| {place.phone} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 주소 정보 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-2 text-[12px]"> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start gap-2.5"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-5 border-gray-1 m-0.5 shrink-0 rounded border px-2 py-px"> | ||||||||||||||||||||||||||||||||||||||
| 지번 | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-[16px] break-words">{place.address}</span> | ||||||||||||||||||||||||||||||||||||||
| {/* 전화번호 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="text-gray-8 flex items-center gap-2 text-[16px]"> | ||||||||||||||||||||||||||||||||||||||
| <Image src="/icon/phone.svg" alt="전화" width={20} height={20} /> | ||||||||||||||||||||||||||||||||||||||
| {place.phone} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start gap-2"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-5 border-gray-1 shrink-0 rounded border px-2 py-px"> | ||||||||||||||||||||||||||||||||||||||
| 도로명 | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-[16px] break-words">{place.roadAddress}</span> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 주소 정보 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-2 text-[12px]"> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start gap-2.5"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-5 border-gray-1 m-0.5 shrink-0 rounded border px-2 py-px"> | ||||||||||||||||||||||||||||||||||||||
| 지번 | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-[16px] break-words">{place.address}</span> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-start gap-2"> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-5 border-gray-1 shrink-0 rounded border px-2 py-px"> | ||||||||||||||||||||||||||||||||||||||
| 도로명 | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| <span className="text-gray-8 text-[16px] break-words"> | ||||||||||||||||||||||||||||||||||||||
| {place.roadAddress} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 하단 버튼은 조건부 렌더링 */} | ||||||||||||||||||||||||||||||||||||||
| {selectedPlaceId === place.id ? ( | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| onClick={(e) => handleOpenKakaoMap(e, place.placeUrl)} | ||||||||||||||||||||||||||||||||||||||
| className="bg-gray-8 w-full rounded py-1 text-[15px] text-white" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| 카카오맵에서 보기 | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| {/* 하단 버튼은 조건부 렌더링 */} | ||||||||||||||||||||||||||||||||||||||
| {selectedPlaceId === place.id ? ( | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| onClick={(e) => handleOpenKakaoMap(e, place.placeUrl)} | ||||||||||||||||||||||||||||||||||||||
| className="bg-gray-8 w-full rounded py-1 text-[15px] text-white" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| 카카오맵에서 보기 | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -192,8 +195,6 @@ function RecommendContent() { | |||||||||||||||||||||||||||||||||||||
| places={places} | ||||||||||||||||||||||||||||||||||||||
| selectedPlaceId={selectedPlaceId} | ||||||||||||||||||||||||||||||||||||||
| onSelectPlace={setSelectedPlaceId} | ||||||||||||||||||||||||||||||||||||||
| selectedCategory={selectedCategory} | ||||||||||||||||||||||||||||||||||||||
| onCategoryChange={setSelectedCategory} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
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.
useMemo내에서localStorage접근은 SSR 안전성 문제가 있습니다.localStorage는 브라우저 전용 API입니다. 현재Suspense+useSearchParams조합으로 클라이언트 렌더링이 보장될 수 있지만, 이 의존성은 암묵적이고 깨지기 쉽습니다. 컴포넌트가 다른 곳으로 이동하거나Suspense래퍼가 제거되면 SSR 시ReferenceError가 발생합니다.또한
useMemo는 순수 계산용이며, 외부 저장소 읽기에는useState+useEffect또는useSyncExternalStore가 더 적절합니다.추가로,
localStorage에 해당 키가 없으면meetingCategory가 빈 문자열이 되어useRecommend의enabled조건(!!category)이false가 됩니다. 이 경우 사용자는 "추천 장소가 없습니다"를 보게 되며 복구 방법이 없습니다.🔧 useState + useEffect 패턴으로 수정 제안
useEffect를 사용하면 SSR 안전성이 보장되고,localStorage접근이 클라이언트 마운트 이후에만 발생합니다.📝 Committable suggestion
🤖 Prompt for AI Agents