From f862006d09f185ccab6d346d41228978919d37cd Mon Sep 17 00:00:00 2001 From: yuji1202 Date: Wed, 29 Apr 2026 21:04:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=AA=A9=EB=A1=9D=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=9E=A5=EC=86=8C=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 목록 탭 저장 장소 리스트 UI 구현 - 지역 필터 및 카테고리 필터 UI 추가 - 장소 상세 화면 UI 추가 - 저장 장소 없음 / 필터 결과 없음 상태 UI 추가 - /dev/list 임시 확인 경로 추가 --- src/app/router/index.tsx | 1 + .../place-list/PlaceListCategoryTabs.tsx | 50 +++++++ .../place-list/PlaceListDetailPage.tsx | 79 +++++++++++ .../place-list/PlaceListEmptyState.tsx | 12 ++ src/components/place-list/PlaceListItem.tsx | 27 ++++ .../place-list/RegionFilterPanel.tsx | 103 +++++++++++++++ .../place-list/place-list-mock-data.ts | 123 ++++++++++++++++++ src/pages/tabs/PlaceListPage.tsx | 123 ++++++++++++++++-- 8 files changed, 509 insertions(+), 9 deletions(-) create mode 100644 src/components/place-list/PlaceListCategoryTabs.tsx create mode 100644 src/components/place-list/PlaceListDetailPage.tsx create mode 100644 src/components/place-list/PlaceListEmptyState.tsx create mode 100644 src/components/place-list/PlaceListItem.tsx create mode 100644 src/components/place-list/RegionFilterPanel.tsx create mode 100644 src/components/place-list/place-list-mock-data.ts diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index c9c6aa6..45322e8 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -29,6 +29,7 @@ export const router = createBrowserRouter([ { path: "dev/splash", element: }, { path: "dev/click_place", element: }, { path: "dev/SelectOption", element: }, + { path: "dev/list", element: }, { path: "login", element: }, { path: "app", element: }, { diff --git a/src/components/place-list/PlaceListCategoryTabs.tsx b/src/components/place-list/PlaceListCategoryTabs.tsx new file mode 100644 index 0000000..e7818a7 --- /dev/null +++ b/src/components/place-list/PlaceListCategoryTabs.tsx @@ -0,0 +1,50 @@ +import { cn } from "@/lib/utils"; + +import type { PlaceCategoryId, PlaceCategoryTab } from "./place-list-mock-data"; + +type PlaceListCategoryTabsProps = { + tabs: PlaceCategoryTab[]; + activeId: PlaceCategoryId; + regionLabel: string; + onRegionClick: () => void; + onSelect: (id: PlaceCategoryId) => void; +}; + +export function PlaceListCategoryTabs({ + tabs, + activeId, + regionLabel, + onRegionClick, + onSelect, +}: PlaceListCategoryTabsProps) { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/place-list/PlaceListDetailPage.tsx b/src/components/place-list/PlaceListDetailPage.tsx new file mode 100644 index 0000000..16febc7 --- /dev/null +++ b/src/components/place-list/PlaceListDetailPage.tsx @@ -0,0 +1,79 @@ +import { ArrowLeft, ChevronDown, MapPin, Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +import type { PlaceListItemData } from "./place-list-mock-data"; +import { PLACE_CATEGORY_TABS, PLACE_LIST_TEXT } from "./place-list-mock-data"; + +type PlaceListDetailPageProps = { + place: PlaceListItemData; + onBack: () => void; +}; + +function MapMarker({ className }: { className?: string }) { + return ( + + + + ); +} + +export function PlaceListDetailPage({ place, onBack }: PlaceListDetailPageProps) { + return ( +
+
+
+
+
+
+ + + +
+ +
+
+ + {PLACE_LIST_TEXT.detailMapTitle} +
+
+ +
+
+ {PLACE_LIST_TEXT.searchPlaceholder} + +
+
+ {PLACE_CATEGORY_TABS.filter((tab) => tab.id !== "all").map((tab) => ( + + {tab.label} + + ))} +
+
+
+ +
+
+ + +

{place.address}

+ + +
+

{place.openingStatus}

+

{place.openingNote}

+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/place-list/PlaceListEmptyState.tsx b/src/components/place-list/PlaceListEmptyState.tsx new file mode 100644 index 0000000..20c5228 --- /dev/null +++ b/src/components/place-list/PlaceListEmptyState.tsx @@ -0,0 +1,12 @@ +type PlaceListEmptyStateProps = { + message: string; +}; + +export function PlaceListEmptyState({ message }: PlaceListEmptyStateProps) { + return ( +
+
!
+

{message}

+
+ ); +} \ No newline at end of file diff --git a/src/components/place-list/PlaceListItem.tsx b/src/components/place-list/PlaceListItem.tsx new file mode 100644 index 0000000..81d633e --- /dev/null +++ b/src/components/place-list/PlaceListItem.tsx @@ -0,0 +1,27 @@ +import { MoreVertical } from "lucide-react"; + +import type { PlaceListItemData } from "./place-list-mock-data"; + +type PlaceListItemProps = { + place: PlaceListItemData; + onSelect: (place: PlaceListItemData) => void; +}; + +export function PlaceListItem({ place, onSelect }: PlaceListItemProps) { + return ( +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/place-list/RegionFilterPanel.tsx b/src/components/place-list/RegionFilterPanel.tsx new file mode 100644 index 0000000..8992d35 --- /dev/null +++ b/src/components/place-list/RegionFilterPanel.tsx @@ -0,0 +1,103 @@ +import { Search, X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +import { REGION_CITIES, REGION_DISTRICTS } from "./place-list-mock-data"; + +type RegionFilterPanelProps = { + selectedCity: string; + selectedDistrict: string; + searchMode?: boolean; + onCitySelect: (city: string) => void; + onDistrictSelect: (district: string) => void; + onClose: () => void; + onConfirm: () => void; +}; + +export function RegionFilterPanel({ + selectedCity, + selectedDistrict, + searchMode = false, + onCitySelect, + onDistrictSelect, + onClose, + onConfirm, +}: RegionFilterPanelProps) { + const confirmLabel = + selectedCity === "전체" && selectedDistrict === "전체" ? "지역 설정하기" : `${selectedCity} ${selectedDistrict} 설정하기`; + + if (searchMode) { + return ( +
+
+

지역설정

+ +
+ + + +
+ ); + } + + return ( +
+
+

지역설정

+ +
+
+ 지역명 검색 + +
+ +
+
시/도
+
시/구/군
+
+ +
+
+ {REGION_CITIES.map((city) => ( + + ))} +
+
+ {REGION_DISTRICTS.map((district) => ( + + ))} +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/place-list/place-list-mock-data.ts b/src/components/place-list/place-list-mock-data.ts new file mode 100644 index 0000000..e51eb05 --- /dev/null +++ b/src/components/place-list/place-list-mock-data.ts @@ -0,0 +1,123 @@ +export type PlaceCategoryId = "all" | "food" | "cafe" | "activity" | "etc"; + +export type PlaceCategoryTab = { + id: PlaceCategoryId; + label: string; +}; + +export type PlaceListItemData = { + id: string; + name: string; + address: string; + region: string; + category: Exclude; + memo?: string; + detailAddress: string; + openingStatus: string; + openingNote: string; + hours: string; +}; + +export const PLACE_LIST_TEXT = { + mapTitle: "심심한 두쭈구 지도", + detailMapTitle: "나만의 지도", + searchPlaceholder: "저장해둔 장소를 검색해보세요", + regionDefault: "지역", + regionConfirmDefault: "지역 설정하기", + emptySaved: "장소를 저장해 보세요!", + emptyFiltered: "해당하는 장소가 없습니다.", + reelsButton: "내가 봤던 릴스 다시보기", +}; + +export const PLACE_CATEGORY_TABS: PlaceCategoryTab[] = [ + { id: "all", label: "전체" }, + { id: "food", label: "맛집" }, + { id: "cafe", label: "카페" }, + { id: "activity", label: "놀거리" }, + { id: "etc", label: "기타" }, +]; + +export const REGION_CITIES = ["전체", "서울", "경기", "인천", "부산", "대구", "대전"]; + +export const REGION_DISTRICTS = ["전체", "강남구", "강동구", "강북구", "강서구", "관악구", "동대문구"]; + +export const PLACE_LIST_ITEMS: PlaceListItemData[] = [ + { + id: "gamdong", + name: "감동", + address: "서울 동대문구 회기로 25길 101-13 1층", + region: "서울 동대문구", + category: "cafe", + memo: "커피 완전 맛있음 ✨✨", + detailAddress: "회기로 25길 101-13 1층", + openingStatus: "영업 전 10:40 오픈", + openingNote: "공휴일 정상 영업", + hours: "토(4/11) 10:40 ~ 19:30", + }, + { + id: "im-pie", + name: "아임파이", + address: "서울 동대문구 회기로 116-12층 (회기동)", + region: "서울 동대문구", + category: "food", + detailAddress: "회기로 116-12층 (회기동)", + openingStatus: "영업 전 10:40 오픈", + openingNote: "공휴일 정상 영업", + hours: "토(4/11) 10:40 ~ 19:30", + }, + { + id: "hufs", + name: "한국외국어대학교 서울캠퍼스", + address: "서울 동대문구 회기로 116-12층 (회기동)", + region: "서울 동대문구", + category: "etc", + detailAddress: "이문로 107", + openingStatus: "상시 이용 가능", + openingNote: "캠퍼스 운영 시간은 시설별로 달라요", + hours: "매일 00:00 ~ 24:00", + }, + { + id: "chabaekdo", + name: "차백도 경희대점", + address: "서울 동대문구 경희대로 8-1 1층", + region: "서울 동대문구", + category: "cafe", + detailAddress: "경희대로 8-1 1층", + openingStatus: "영업 중", + openingNote: "테이크아웃 가능", + hours: "매일 09:00 ~ 22:00", + }, + { + id: "spring-garden", + name: "봄의정원 회기점", + address: "서울 동대문구 경희대로길 35 봄의정원 회기점", + region: "서울 동대문구", + category: "activity", + detailAddress: "경희대로길 35", + openingStatus: "영업 중", + openingNote: "예약 후 방문 추천", + hours: "매일 11:00 ~ 21:00", + }, + { + id: "easy-white", + name: "이지화이트 브런치", + address: "서울 동대문구 이문로 120 상가동 1층 A120호", + region: "서울 동대문구", + category: "food", + detailAddress: "이문로 120 1층", + openingStatus: "영업 중", + openingNote: "인기 메뉴 조기 품절 가능", + hours: "매일 08:30 ~ 20:00", + }, + { + id: "unni", + name: "언니네함바그", + address: "서울 동대문구 회기로 5길 59 1층 언니네함바그", + region: "서울 동대문구", + category: "food", + detailAddress: "회기로 5길 59 1층", + openingStatus: "영업 중", + openingNote: "점심 시간대 대기 가능", + hours: "매일 11:30 ~ 21:00", + }, +]; \ No newline at end of file diff --git a/src/pages/tabs/PlaceListPage.tsx b/src/pages/tabs/PlaceListPage.tsx index 9ee7a2b..31cf9c0 100644 --- a/src/pages/tabs/PlaceListPage.tsx +++ b/src/pages/tabs/PlaceListPage.tsx @@ -1,23 +1,128 @@ -import { Navigate } from "react-router-dom"; +import { MapPin, Search } from "lucide-react"; +import { useMemo, useState } from "react"; import { BottomNavigationBar } from "@/components/common/BottomNavigationBar"; import { BottomNavToast } from "@/components/common/BottomNavToast"; +import { PlaceListCategoryTabs } from "@/components/place-list/PlaceListCategoryTabs"; +import { PlaceListDetailPage } from "@/components/place-list/PlaceListDetailPage"; +import { PlaceListEmptyState } from "@/components/place-list/PlaceListEmptyState"; +import { PlaceListItem } from "@/components/place-list/PlaceListItem"; +import { + PLACE_CATEGORY_TABS, + PLACE_LIST_ITEMS, + PLACE_LIST_TEXT, + type PlaceCategoryId, + type PlaceListItemData, +} from "@/components/place-list/place-list-mock-data"; +import { RegionFilterPanel } from "@/components/place-list/RegionFilterPanel"; import { useBottomNavController } from "@/hooks/use-bottom-nav-controller"; -import { useRoomSelectionStore } from "@/store/room-selection-store"; -export default function PlaceListPage() { - const selectedRoom = useRoomSelectionStore((s) => s.selectedRoom); +type PlaceListPageProps = { + preview?: boolean; +}; + +function PlaceListMapHeader() { + return ( +
+
+
+
+
+ + + + + + +
+ +
+
+ + {PLACE_LIST_TEXT.mapTitle} +
+
+ +
+
+ {PLACE_LIST_TEXT.searchPlaceholder} + +
+
+
+ ); +} + +export default function PlaceListPage({ preview = false }: PlaceListPageProps) { const { toastMessage, handleSelectBottomNav } = useBottomNavController(); + const [activeCategory, setActiveCategory] = useState("all"); + const [selectedCity, setSelectedCity] = useState("서울"); + const [selectedDistrict, setSelectedDistrict] = useState("동대문구"); + const [isRegionPanelOpen, setIsRegionPanelOpen] = useState(false); + const [isRegionSearchMode, setIsRegionSearchMode] = useState(false); + const [selectedPlace, setSelectedPlace] = useState(null); + + const regionLabel = + selectedCity === "전체" && selectedDistrict === "전체" ? PLACE_LIST_TEXT.regionDefault : `${selectedCity} ${selectedDistrict}`; + + const filteredPlaces = useMemo(() => { + return PLACE_LIST_ITEMS.filter((place) => { + const matchesCategory = activeCategory === "all" || place.category === activeCategory; + const matchesCity = selectedCity === "전체" || place.region.includes(selectedCity); + const matchesDistrict = selectedDistrict === "전체" || place.region.includes(selectedDistrict); + return matchesCategory && matchesCity && matchesDistrict; + }); + }, [activeCategory, selectedCity, selectedDistrict]); - if (!selectedRoom) { - return ; + if (selectedPlace) { + return ( +
+ setSelectedPlace(null)} /> + + +
+ ); } return ( -
-
+
+ {preview ? : null} +
+ { + setIsRegionPanelOpen(true); + setIsRegionSearchMode(false); + }} + onSelect={setActiveCategory} + /> + + {isRegionPanelOpen ? ( + setIsRegionPanelOpen(false)} + onConfirm={() => setIsRegionPanelOpen(false)} + /> + ) : null} + + {filteredPlaces.length > 0 ? ( +
+ {filteredPlaces.map((place) => ( + + ))} +
+ ) : ( + + )} +
); -} +} \ No newline at end of file From 26e0e7e6a0365d11af73ffa4ec70266eb744a74a Mon Sep 17 00:00:00 2001 From: 1000hyehyang Date: Fri, 1 May 2026 01:46:02 +0900 Subject: [PATCH 2/3] fix: npm run lint:fix --- .../place-list/PlaceListCategoryTabs.tsx | 11 +++- .../place-list/PlaceListDetailPage.tsx | 44 +++++++++----- .../place-list/PlaceListEmptyState.tsx | 6 +- src/components/place-list/PlaceListItem.tsx | 12 +++- .../place-list/RegionFilterPanel.tsx | 57 +++++++++++++++---- .../place-list/place-list-mock-data.ts | 12 +++- src/pages/tabs/PlaceListPage.tsx | 35 +++++++----- 7 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/components/place-list/PlaceListCategoryTabs.tsx b/src/components/place-list/PlaceListCategoryTabs.tsx index e7818a7..c22999e 100644 --- a/src/components/place-list/PlaceListCategoryTabs.tsx +++ b/src/components/place-list/PlaceListCategoryTabs.tsx @@ -18,7 +18,10 @@ export function PlaceListCategoryTabs({ onSelect, }: PlaceListCategoryTabsProps) { return ( -
); -} \ No newline at end of file +} diff --git a/src/components/place-list/PlaceListEmptyState.tsx b/src/components/place-list/PlaceListEmptyState.tsx index 20c5228..5101fec 100644 --- a/src/components/place-list/PlaceListEmptyState.tsx +++ b/src/components/place-list/PlaceListEmptyState.tsx @@ -5,8 +5,10 @@ type PlaceListEmptyStateProps = { export function PlaceListEmptyState({ message }: PlaceListEmptyStateProps) { return (
-
!
+
+ ! +

{message}

); -} \ No newline at end of file +} diff --git a/src/components/place-list/PlaceListItem.tsx b/src/components/place-list/PlaceListItem.tsx index 81d633e..82650b2 100644 --- a/src/components/place-list/PlaceListItem.tsx +++ b/src/components/place-list/PlaceListItem.tsx @@ -15,13 +15,19 @@ export function PlaceListItem({ place, onSelect }: PlaceListItemProps) {

{place.name}

{place.address}

{place.memo ? ( -

{place.memo}

+

+ {place.memo} +

) : null} - ); -} \ No newline at end of file +} diff --git a/src/components/place-list/RegionFilterPanel.tsx b/src/components/place-list/RegionFilterPanel.tsx index 8992d35..dc1fd12 100644 --- a/src/components/place-list/RegionFilterPanel.tsx +++ b/src/components/place-list/RegionFilterPanel.tsx @@ -24,26 +24,44 @@ export function RegionFilterPanel({ onConfirm, }: RegionFilterPanelProps) { const confirmLabel = - selectedCity === "전체" && selectedDistrict === "전체" ? "지역 설정하기" : `${selectedCity} ${selectedDistrict} 설정하기`; + selectedCity === "전체" && selectedDistrict === "전체" + ? "지역 설정하기" + : `${selectedCity} ${selectedDistrict} 설정하기`; if (searchMode) { return ( -
+

지역설정

-
- - -
@@ -51,10 +69,15 @@ export function RegionFilterPanel({ } return ( -
+

지역설정

-
@@ -75,7 +98,10 @@ export function RegionFilterPanel({ key={city} type="button" onClick={() => onCitySelect(city)} - className={cn("h-10 w-full px-4 text-left text-[#777777]", selectedCity === city && "bg-[#fde5e5] font-semibold text-[#ef7373]")} + className={cn( + "h-10 w-full px-4 text-left text-[#777777]", + selectedCity === city && "bg-[#fde5e5] font-semibold text-[#ef7373]", + )} > {city} @@ -87,7 +113,10 @@ export function RegionFilterPanel({ key={district} type="button" onClick={() => onDistrictSelect(district)} - className={cn("h-10 w-full px-4 text-left text-[#777777]", selectedDistrict === district && "bg-[#fde5e5] font-semibold text-[#ef7373]")} + className={cn( + "h-10 w-full px-4 text-left text-[#777777]", + selectedDistrict === district && "bg-[#fde5e5] font-semibold text-[#ef7373]", + )} > {district} @@ -95,9 +124,13 @@ export function RegionFilterPanel({ -
); -} \ No newline at end of file +} diff --git a/src/components/place-list/place-list-mock-data.ts b/src/components/place-list/place-list-mock-data.ts index e51eb05..d8c5796 100644 --- a/src/components/place-list/place-list-mock-data.ts +++ b/src/components/place-list/place-list-mock-data.ts @@ -39,7 +39,15 @@ export const PLACE_CATEGORY_TABS: PlaceCategoryTab[] = [ export const REGION_CITIES = ["전체", "서울", "경기", "인천", "부산", "대구", "대전"]; -export const REGION_DISTRICTS = ["전체", "강남구", "강동구", "강북구", "강서구", "관악구", "동대문구"]; +export const REGION_DISTRICTS = [ + "전체", + "강남구", + "강동구", + "강북구", + "강서구", + "관악구", + "동대문구", +]; export const PLACE_LIST_ITEMS: PlaceListItemData[] = [ { @@ -120,4 +128,4 @@ export const PLACE_LIST_ITEMS: PlaceListItemData[] = [ openingNote: "점심 시간대 대기 가능", hours: "매일 11:30 ~ 21:00", }, -]; \ No newline at end of file +]; diff --git a/src/pages/tabs/PlaceListPage.tsx b/src/pages/tabs/PlaceListPage.tsx index 086a8b6..616686c 100644 --- a/src/pages/tabs/PlaceListPage.tsx +++ b/src/pages/tabs/PlaceListPage.tsx @@ -3,10 +3,6 @@ import { useMemo, useState } from "react"; import { BottomNavigationBar } from "@/components/common/BottomNavigationBar"; import { BottomNavToast } from "@/components/common/BottomNavToast"; -import { PlaceListCategoryTabs } from "@/components/place-list/PlaceListCategoryTabs"; -import { PlaceListDetailPage } from "@/components/place-list/PlaceListDetailPage"; -import { PlaceListEmptyState } from "@/components/place-list/PlaceListEmptyState"; -import { PlaceListItem } from "@/components/place-list/PlaceListItem"; import { PLACE_CATEGORY_TABS, PLACE_LIST_ITEMS, @@ -14,6 +10,10 @@ import { type PlaceCategoryId, type PlaceListItemData, } from "@/components/place-list/place-list-mock-data"; +import { PlaceListCategoryTabs } from "@/components/place-list/PlaceListCategoryTabs"; +import { PlaceListDetailPage } from "@/components/place-list/PlaceListDetailPage"; +import { PlaceListEmptyState } from "@/components/place-list/PlaceListEmptyState"; +import { PlaceListItem } from "@/components/place-list/PlaceListItem"; import { RegionFilterPanel } from "@/components/place-list/RegionFilterPanel"; import { useBottomNavController } from "@/hooks/use-bottom-nav-controller"; @@ -26,17 +26,17 @@ function PlaceListMapHeader() {
-
-
- +
+
+ - +
-
+
{PLACE_LIST_TEXT.mapTitle} @@ -63,13 +63,16 @@ export default function PlaceListPage({ preview = false }: PlaceListPageProps) { const [selectedPlace, setSelectedPlace] = useState(null); const regionLabel = - selectedCity === "전체" && selectedDistrict === "전체" ? PLACE_LIST_TEXT.regionDefault : `${selectedCity} ${selectedDistrict}`; + selectedCity === "전체" && selectedDistrict === "전체" + ? PLACE_LIST_TEXT.regionDefault + : `${selectedCity} ${selectedDistrict}`; const filteredPlaces = useMemo(() => { return PLACE_LIST_ITEMS.filter((place) => { const matchesCategory = activeCategory === "all" || place.category === activeCategory; const matchesCity = selectedCity === "전체" || place.region.includes(selectedCity); - const matchesDistrict = selectedDistrict === "전체" || place.region.includes(selectedDistrict); + const matchesDistrict = + selectedDistrict === "전체" || place.region.includes(selectedDistrict); return matchesCategory && matchesCity && matchesDistrict; }); }, [activeCategory, selectedCity, selectedDistrict]); @@ -118,11 +121,17 @@ export default function PlaceListPage({ preview = false }: PlaceListPageProps) { ))}
) : ( - + )}
); -} \ No newline at end of file +} From e8c6e69184b6b88c1ed1fd626094e65fc5efc4e7 Mon Sep 17 00:00:00 2001 From: 1000hyehyang Date: Fri, 1 May 2026 01:55:46 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router/index.tsx | 2 +- .../course-planner/RegionSelectionPanel.tsx | 2 +- src/components/mypage/SavedPlaceItem.tsx | 97 ++-- .../place-list/PlaceListCategoryTabs.tsx | 55 -- .../place-list/PlaceListDetailPage.tsx | 97 ---- .../place-list/PlaceListEmptyState.tsx | 14 - src/components/place-list/PlaceListItem.tsx | 33 -- .../place-list/RegionFilterPanel.tsx | 136 ----- .../place-list/place-list-mock-data.ts | 127 ----- src/pages/tabs/PlaceListPage.tsx | 474 +++++++++++++----- 10 files changed, 415 insertions(+), 622 deletions(-) delete mode 100644 src/components/place-list/PlaceListCategoryTabs.tsx delete mode 100644 src/components/place-list/PlaceListDetailPage.tsx delete mode 100644 src/components/place-list/PlaceListEmptyState.tsx delete mode 100644 src/components/place-list/PlaceListItem.tsx delete mode 100644 src/components/place-list/RegionFilterPanel.tsx diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index 28ff5c0..7078df7 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -30,7 +30,7 @@ export const router = createBrowserRouter([ { path: "dev/splash", element: }, { path: "dev/click_place", element: }, { path: "dev/SelectOption", element: }, - { path: "dev/list", element: }, + { path: "dev/list", element: }, { path: "dev/mypage", element: }, { path: "login", element: }, { path: "dev/course", element: }, diff --git a/src/components/course-planner/RegionSelectionPanel.tsx b/src/components/course-planner/RegionSelectionPanel.tsx index c96df54..5563d1b 100644 --- a/src/components/course-planner/RegionSelectionPanel.tsx +++ b/src/components/course-planner/RegionSelectionPanel.tsx @@ -14,7 +14,7 @@ type RegionSelectionPanelProps = { const cities = ["서울", "경기", "인천", "부산", "대구", "대전"]; const districtsByCity: Record = { - 서울: ["전체", "강남구", "강동구", "강북구", "강서구", "관악구"], + 서울: ["전체", "강남구", "강동구", "강북구", "강서구", "관악구", "동대문구"], 경기: ["전체", "성남시", "수원시", "고양시", "용인시", "하남시"], 인천: ["전체", "남동구", "연수구", "부평구", "서구", "중구"], 부산: ["전체", "해운대구", "수영구", "부산진구", "동래구", "남구"], diff --git a/src/components/mypage/SavedPlaceItem.tsx b/src/components/mypage/SavedPlaceItem.tsx index 796133c..2c2a5c0 100644 --- a/src/components/mypage/SavedPlaceItem.tsx +++ b/src/components/mypage/SavedPlaceItem.tsx @@ -8,23 +8,26 @@ import { SavedPlaceMemoEditor } from "./SavedPlaceMemoEditor"; type SavedPlaceItemProps = { place: SavedPlace; - isMenuOpen: boolean; - isEditing: boolean; - memoDraft: string; - onToggleMenu: (id: string) => void; - onStartMemo: (place: SavedPlace) => void; - onChangeMemo: (value: string) => void; - onSaveMemo: () => void; - onClearMemo: () => void; - onDelete: (id: string) => void; + /** true면 마이페이지용 메뉴·메모 편집 없이 카드만 표시(목록 탭 등) */ + readOnly?: boolean; + isMenuOpen?: boolean; + isEditing?: boolean; + memoDraft?: string; + onToggleMenu?: (id: string) => void; + onStartMemo?: (place: SavedPlace) => void; + onChangeMemo?: (value: string) => void; + onSaveMemo?: () => void; + onClearMemo?: () => void; + onDelete?: (id: string) => void; onSelect: (place: SavedPlace) => void; }; export function SavedPlaceItem({ place, - isMenuOpen, - isEditing, - memoDraft, + readOnly = false, + isMenuOpen = false, + isEditing = false, + memoDraft = "", onToggleMenu, onStartMemo, onChangeMemo, @@ -35,9 +38,9 @@ export function SavedPlaceItem({ }: SavedPlaceItemProps) { const menuChromeRef = useRef(null); const closeMenu = useCallback(() => { - onToggleMenu(place.id); + onToggleMenu?.(place.id); }, [onToggleMenu, place.id]); - usePointerDownOutside(menuChromeRef, isMenuOpen, closeMenu); + usePointerDownOutside(menuChromeRef, !readOnly && isMenuOpen, closeMenu); return (
@@ -47,35 +50,37 @@ export function SavedPlaceItem({

{place.address}

-
- + {!readOnly ? ( +
+ - {isMenuOpen ? ( -
- - -
- ) : null} -
+ {isMenuOpen ? ( +
+ + +
+ ) : null} +
+ ) : null}
{place.memo && !isEditing ? ( @@ -84,12 +89,12 @@ export function SavedPlaceItem({

) : null} - {isEditing ? ( + {!readOnly && isEditing ? ( {})} + onSave={onSaveMemo ?? (() => {})} + onClear={onClearMemo ?? (() => {})} /> ) : null} diff --git a/src/components/place-list/PlaceListCategoryTabs.tsx b/src/components/place-list/PlaceListCategoryTabs.tsx deleted file mode 100644 index c22999e..0000000 --- a/src/components/place-list/PlaceListCategoryTabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { cn } from "@/lib/utils"; - -import type { PlaceCategoryId, PlaceCategoryTab } from "./place-list-mock-data"; - -type PlaceListCategoryTabsProps = { - tabs: PlaceCategoryTab[]; - activeId: PlaceCategoryId; - regionLabel: string; - onRegionClick: () => void; - onSelect: (id: PlaceCategoryId) => void; -}; - -export function PlaceListCategoryTabs({ - tabs, - activeId, - regionLabel, - onRegionClick, - onSelect, -}: PlaceListCategoryTabsProps) { - return ( - - ); -} diff --git a/src/components/place-list/PlaceListDetailPage.tsx b/src/components/place-list/PlaceListDetailPage.tsx deleted file mode 100644 index 27d7882..0000000 --- a/src/components/place-list/PlaceListDetailPage.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { ArrowLeft, ChevronDown, MapPin, Search } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -import type { PlaceListItemData } from "./place-list-mock-data"; -import { PLACE_CATEGORY_TABS, PLACE_LIST_TEXT } from "./place-list-mock-data"; - -type PlaceListDetailPageProps = { - place: PlaceListItemData; - onBack: () => void; -}; - -function MapMarker({ className }: { className?: string }) { - return ( - - - - ); -} - -export function PlaceListDetailPage({ place, onBack }: PlaceListDetailPageProps) { - return ( -
-
-
-
-
-
- - - -
- -
-
- - {PLACE_LIST_TEXT.detailMapTitle} -
-
- -
-
- - {PLACE_LIST_TEXT.searchPlaceholder} - - -
-
- {PLACE_CATEGORY_TABS.filter((tab) => tab.id !== "all").map((tab) => ( - - {tab.label} - - ))} -
-
-
- -
-
- - -

{place.address}

- - -
-

{place.openingStatus}

-

{place.openingNote}

- -
-
-
- ); -} diff --git a/src/components/place-list/PlaceListEmptyState.tsx b/src/components/place-list/PlaceListEmptyState.tsx deleted file mode 100644 index 5101fec..0000000 --- a/src/components/place-list/PlaceListEmptyState.tsx +++ /dev/null @@ -1,14 +0,0 @@ -type PlaceListEmptyStateProps = { - message: string; -}; - -export function PlaceListEmptyState({ message }: PlaceListEmptyStateProps) { - return ( -
-
- ! -
-

{message}

-
- ); -} diff --git a/src/components/place-list/PlaceListItem.tsx b/src/components/place-list/PlaceListItem.tsx deleted file mode 100644 index 82650b2..0000000 --- a/src/components/place-list/PlaceListItem.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { MoreVertical } from "lucide-react"; - -import type { PlaceListItemData } from "./place-list-mock-data"; - -type PlaceListItemProps = { - place: PlaceListItemData; - onSelect: (place: PlaceListItemData) => void; -}; - -export function PlaceListItem({ place, onSelect }: PlaceListItemProps) { - return ( -
-
- - -
-
- ); -} diff --git a/src/components/place-list/RegionFilterPanel.tsx b/src/components/place-list/RegionFilterPanel.tsx deleted file mode 100644 index dc1fd12..0000000 --- a/src/components/place-list/RegionFilterPanel.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Search, X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -import { REGION_CITIES, REGION_DISTRICTS } from "./place-list-mock-data"; - -type RegionFilterPanelProps = { - selectedCity: string; - selectedDistrict: string; - searchMode?: boolean; - onCitySelect: (city: string) => void; - onDistrictSelect: (district: string) => void; - onClose: () => void; - onConfirm: () => void; -}; - -export function RegionFilterPanel({ - selectedCity, - selectedDistrict, - searchMode = false, - onCitySelect, - onDistrictSelect, - onClose, - onConfirm, -}: RegionFilterPanelProps) { - const confirmLabel = - selectedCity === "전체" && selectedDistrict === "전체" - ? "지역 설정하기" - : `${selectedCity} ${selectedDistrict} 설정하기`; - - if (searchMode) { - return ( -
-
-

지역설정

- -
- - - -
- ); - } - - return ( -
-
-

지역설정

- -
-
- 지역명 검색 - -
- -
-
시/도
-
시/구/군
-
- -
-
- {REGION_CITIES.map((city) => ( - - ))} -
-
- {REGION_DISTRICTS.map((district) => ( - - ))} -
-
- - -
- ); -} diff --git a/src/components/place-list/place-list-mock-data.ts b/src/components/place-list/place-list-mock-data.ts index d8c5796..23fce8a 100644 --- a/src/components/place-list/place-list-mock-data.ts +++ b/src/components/place-list/place-list-mock-data.ts @@ -1,131 +1,4 @@ -export type PlaceCategoryId = "all" | "food" | "cafe" | "activity" | "etc"; - -export type PlaceCategoryTab = { - id: PlaceCategoryId; - label: string; -}; - -export type PlaceListItemData = { - id: string; - name: string; - address: string; - region: string; - category: Exclude; - memo?: string; - detailAddress: string; - openingStatus: string; - openingNote: string; - hours: string; -}; - export const PLACE_LIST_TEXT = { - mapTitle: "심심한 두쭈구 지도", - detailMapTitle: "나만의 지도", - searchPlaceholder: "저장해둔 장소를 검색해보세요", - regionDefault: "지역", - regionConfirmDefault: "지역 설정하기", emptySaved: "장소를 저장해 보세요!", emptyFiltered: "해당하는 장소가 없습니다.", - reelsButton: "내가 봤던 릴스 다시보기", }; - -export const PLACE_CATEGORY_TABS: PlaceCategoryTab[] = [ - { id: "all", label: "전체" }, - { id: "food", label: "맛집" }, - { id: "cafe", label: "카페" }, - { id: "activity", label: "놀거리" }, - { id: "etc", label: "기타" }, -]; - -export const REGION_CITIES = ["전체", "서울", "경기", "인천", "부산", "대구", "대전"]; - -export const REGION_DISTRICTS = [ - "전체", - "강남구", - "강동구", - "강북구", - "강서구", - "관악구", - "동대문구", -]; - -export const PLACE_LIST_ITEMS: PlaceListItemData[] = [ - { - id: "gamdong", - name: "감동", - address: "서울 동대문구 회기로 25길 101-13 1층", - region: "서울 동대문구", - category: "cafe", - memo: "커피 완전 맛있음 ✨✨", - detailAddress: "회기로 25길 101-13 1층", - openingStatus: "영업 전 10:40 오픈", - openingNote: "공휴일 정상 영업", - hours: "토(4/11) 10:40 ~ 19:30", - }, - { - id: "im-pie", - name: "아임파이", - address: "서울 동대문구 회기로 116-12층 (회기동)", - region: "서울 동대문구", - category: "food", - detailAddress: "회기로 116-12층 (회기동)", - openingStatus: "영업 전 10:40 오픈", - openingNote: "공휴일 정상 영업", - hours: "토(4/11) 10:40 ~ 19:30", - }, - { - id: "hufs", - name: "한국외국어대학교 서울캠퍼스", - address: "서울 동대문구 회기로 116-12층 (회기동)", - region: "서울 동대문구", - category: "etc", - detailAddress: "이문로 107", - openingStatus: "상시 이용 가능", - openingNote: "캠퍼스 운영 시간은 시설별로 달라요", - hours: "매일 00:00 ~ 24:00", - }, - { - id: "chabaekdo", - name: "차백도 경희대점", - address: "서울 동대문구 경희대로 8-1 1층", - region: "서울 동대문구", - category: "cafe", - detailAddress: "경희대로 8-1 1층", - openingStatus: "영업 중", - openingNote: "테이크아웃 가능", - hours: "매일 09:00 ~ 22:00", - }, - { - id: "spring-garden", - name: "봄의정원 회기점", - address: "서울 동대문구 경희대로길 35 봄의정원 회기점", - region: "서울 동대문구", - category: "activity", - detailAddress: "경희대로길 35", - openingStatus: "영업 중", - openingNote: "예약 후 방문 추천", - hours: "매일 11:00 ~ 21:00", - }, - { - id: "easy-white", - name: "이지화이트 브런치", - address: "서울 동대문구 이문로 120 상가동 1층 A120호", - region: "서울 동대문구", - category: "food", - detailAddress: "이문로 120 1층", - openingStatus: "영업 중", - openingNote: "인기 메뉴 조기 품절 가능", - hours: "매일 08:30 ~ 20:00", - }, - { - id: "unni", - name: "언니네함바그", - address: "서울 동대문구 회기로 5길 59 1층 언니네함바그", - region: "서울 동대문구", - category: "food", - detailAddress: "회기로 5길 59 1층", - openingStatus: "영업 중", - openingNote: "점심 시간대 대기 가능", - hours: "매일 11:30 ~ 21:00", - }, -]; diff --git a/src/pages/tabs/PlaceListPage.tsx b/src/pages/tabs/PlaceListPage.tsx index 616686c..d2bc3ac 100644 --- a/src/pages/tabs/PlaceListPage.tsx +++ b/src/pages/tabs/PlaceListPage.tsx @@ -1,137 +1,387 @@ -import { MapPin, Search } from "lucide-react"; -import { useMemo, useState } from "react"; +import "@/components/map/filter-bar.css"; + +import { AlertCircle, ArrowLeft, MapPin } from "lucide-react"; +import { lazy, Suspense, useMemo, useRef, useState } from "react"; import { BottomNavigationBar } from "@/components/common/BottomNavigationBar"; import { BottomNavToast } from "@/components/common/BottomNavToast"; +import { CoursePlannerBottomSheet } from "@/components/course-planner/CoursePlannerBottomSheet"; +import { RegionSelectionPanel } from "@/components/course-planner/RegionSelectionPanel"; +import { FilterBar } from "@/components/map/FilterBar"; import { - PLACE_CATEGORY_TABS, - PLACE_LIST_ITEMS, - PLACE_LIST_TEXT, - type PlaceCategoryId, - type PlaceListItemData, -} from "@/components/place-list/place-list-mock-data"; -import { PlaceListCategoryTabs } from "@/components/place-list/PlaceListCategoryTabs"; -import { PlaceListDetailPage } from "@/components/place-list/PlaceListDetailPage"; -import { PlaceListEmptyState } from "@/components/place-list/PlaceListEmptyState"; -import { PlaceListItem } from "@/components/place-list/PlaceListItem"; -import { RegionFilterPanel } from "@/components/place-list/RegionFilterPanel"; + mapPlacesMatchingMySaved, + weightedMapCenter, +} from "@/components/mypage/map-places-from-my-saved"; +import type { SavedPlace } from "@/components/mypage/mypage-mock-data"; +import { SavedPlaceItem } from "@/components/mypage/SavedPlaceItem"; +import { PlaceDetailSheet } from "@/components/place/PlaceDetailSheet"; +import { PLACE_LIST_TEXT } from "@/components/place-list/place-list-mock-data"; +import { useMapSearchFilters } from "@/features/map/hooks/use-map-search-filters"; +import { usePlaceFilterData } from "@/features/map/hooks/use-place-filter-data"; +import { FALLBACK_PLACE_FILTER_DATA } from "@/features/map/lib/fallback-place-filter-data"; import { useBottomNavController } from "@/hooks/use-bottom-nav-controller"; +import { usePlaceDetailOpenEvent } from "@/hooks/use-place-detail-open-event"; +import { usePointerDownOutside } from "@/hooks/use-pointer-down-outside"; +import { cn } from "@/lib/utils"; +import { SAVED_PLACE_MOCKS } from "@/pages/map/map-home-mock"; +import { resolveSavedPlacesBusinessHours, useKoreanNow } from "@/shared/lib/place-business-hours"; +import { MAP_ALL_CATEGORY_FILTER_CHIP, type MapPrimaryCategory } from "@/shared/types/map-home"; +import { usePlaceDetailStore } from "@/store/placeDetailStore"; -type PlaceListPageProps = { - preview?: boolean; -}; - -function PlaceListMapHeader() { - return ( -
-
-
-
-
- - - - - - -
+const KAKAO_MAP_APP_KEY = import.meta.env.VITE_KAKAO_MAP_APP_KEY; +const KakaoMapView = lazy(() => + import("@/components/map/KakaoMapView").then((module) => ({ default: module.KakaoMapView })), +); -
-
- - {PLACE_LIST_TEXT.mapTitle} -
-
+function formatCount(count: number) { + return count > 999 ? "999+" : String(count); +} -
-
- {PLACE_LIST_TEXT.searchPlaceholder} - -
-
-
- ); +function placeMatchesRegion(placeAddress: string, city: string, district: string) { + const matchesCity = placeAddress.includes(city); + const matchesDistrict = district === "전체" || placeAddress.includes(district); + return matchesCity && matchesDistrict; } -export default function PlaceListPage({ preview = false }: PlaceListPageProps) { +export default function PlaceListPage() { + const now = useKoreanNow(); const { toastMessage, toastPlacement, handleSelectBottomNav } = useBottomNavController(); - const [activeCategory, setActiveCategory] = useState("all"); + + const resolvedPlaces = useMemo( + () => resolveSavedPlacesBusinessHours(SAVED_PLACE_MOCKS, now), + [now], + ); + + const { + filterCategories: apiFilterCategories, + isInitialLoading: isTaxonomyLoading, + isInitialError: isTaxonomyError, + retryLoad, + } = usePlaceFilterData(); + + const filterCategories = useMemo(() => { + if (apiFilterCategories.length > 0) return apiFilterCategories; + if (isTaxonomyLoading && !isTaxonomyError) return []; + return FALLBACK_PLACE_FILTER_DATA.categories; + }, [apiFilterCategories, isTaxonomyError, isTaxonomyLoading]); + + const categories = useMemo( + () => [MAP_ALL_CATEGORY_FILTER_CHIP, ...filterCategories.map((category) => category.code)], + [filterCategories], + ); + + const categoryNameByCode = useMemo( + () => + filterCategories.reduce( + (accumulator, category) => { + accumulator[category.code as MapPrimaryCategory] = category.name; + return accumulator; + }, + {} as Record, + ), + [filterCategories], + ); + + const isCategoryLoading = filterCategories.length === 0 && isTaxonomyLoading && !isTaxonomyError; + + const { + activeCategories, + focusedCategory, + toggleCategory, + closeTagPanel, + isTagPanelOpen, + selectedTagKeysByCategory, + selectedTagCountByCategory, + toggleTagInCategory, + resetFocusedCategoryTags, + filteredPlaces, + } = useMapSearchFilters({ + places: resolvedPlaces, + filterCategories, + categoriesOnly: true, + }); + const [selectedCity, setSelectedCity] = useState("서울"); const [selectedDistrict, setSelectedDistrict] = useState("동대문구"); + const [draftCity, setDraftCity] = useState("서울"); + const [draftDistrict, setDraftDistrict] = useState("동대문구"); const [isRegionPanelOpen, setIsRegionPanelOpen] = useState(false); - const [isRegionSearchMode, setIsRegionSearchMode] = useState(false); - const [selectedPlace, setSelectedPlace] = useState(null); - - const regionLabel = - selectedCity === "전체" && selectedDistrict === "전체" - ? PLACE_LIST_TEXT.regionDefault - : `${selectedCity} ${selectedDistrict}`; - - const filteredPlaces = useMemo(() => { - return PLACE_LIST_ITEMS.filter((place) => { - const matchesCategory = activeCategory === "all" || place.category === activeCategory; - const matchesCity = selectedCity === "전체" || place.region.includes(selectedCity); - const matchesDistrict = - selectedDistrict === "전체" || place.region.includes(selectedDistrict); - return matchesCategory && matchesCity && matchesDistrict; + + const categoryFilteredPlaces = filteredPlaces; + + const listPlacesBase = useMemo((): SavedPlace[] => { + return categoryFilteredPlaces + .filter((place) => placeMatchesRegion(place.address, selectedCity, selectedDistrict)) + .map((place) => ({ + id: place.id, + name: place.name, + address: place.address, + category: place.category, + tagKeys: place.tagKeys, + })); + }, [categoryFilteredPlaces, selectedCity, selectedDistrict]); + + const [openMenuId, setOpenMenuId] = useState(null); + const [editingPlaceId, setEditingPlaceId] = useState(null); + const [memoDraft, setMemoDraft] = useState(""); + const [placeMemos, setPlaceMemos] = useState>({}); + const [removedPlaceIds, setRemovedPlaceIds] = useState([]); + + const displayedPlaces = useMemo((): SavedPlace[] => { + const removed = new Set(removedPlaceIds); + return listPlacesBase + .filter((place) => !removed.has(place.id)) + .map((place) => ({ + ...place, + memo: placeMemos[place.id] ?? place.memo, + })); + }, [listPlacesBase, placeMemos, removedPlaceIds]); + + const mapPins = useMemo( + () => resolveSavedPlacesBusinessHours(mapPlacesMatchingMySaved(displayedPlaces), now), + [displayedPlaces, now], + ); + + const detailOpen = usePlaceDetailStore((s) => s.isOpen); + const selectedPlaceId = usePlaceDetailStore((s) => s.selectedPlaceId); + const closeDetail = usePlaceDetailStore((s) => s.closeDetail); + const openPlaceDetail = usePlaceDetailStore((s) => s.openDetail); + + usePlaceDetailOpenEvent(true); + + const filterChromeRef = useRef(null); + usePointerDownOutside(filterChromeRef, isTagPanelOpen, closeTagPanel); + + const mapCenter = useMemo(() => { + if (selectedPlaceId) { + const pin = mapPins.find((p) => p.id === selectedPlaceId); + if (pin) { + return { latitude: pin.latitude, longitude: pin.longitude }; + } + } + return weightedMapCenter(mapPins); + }, [mapPins, selectedPlaceId]); + + const regionFieldValue = + selectedDistrict === "전체" ? selectedCity : `${selectedCity} ${selectedDistrict}`; + + const handleOpenRegionSelect = () => { + closeTagPanel(); + setDraftCity(selectedCity); + setDraftDistrict(selectedDistrict); + setIsRegionPanelOpen(true); + }; + + const handleSelectDraftCity = (city: string) => { + setDraftCity(city); + setDraftDistrict("전체"); + }; + + const handleConfirmRegion = () => { + setSelectedCity(draftCity); + setSelectedDistrict(draftDistrict); + setIsRegionPanelOpen(false); + }; + + const shownCount = displayedPlaces.length; + const regionTotal = listPlacesBase.length; + const categoryTotal = categoryFilteredPlaces.length; + const displayedCountLabel = + shownCount === regionTotal && regionTotal === categoryTotal + ? `${formatCount(shownCount)}개` + : shownCount === regionTotal + ? `${formatCount(shownCount)}개 · 전체 ${formatCount(categoryTotal)}` + : `${formatCount(shownCount)}개 · 전체 ${formatCount(regionTotal)}`; + + const emptyMessage = + resolvedPlaces.length === 0 ? PLACE_LIST_TEXT.emptySaved : PLACE_LIST_TEXT.emptyFiltered; + + const handleHeaderBack = () => { + if (detailOpen) { + closeDetail(); + } + }; + + const handleStartMemo = (place: SavedPlace) => { + setOpenMenuId(null); + setEditingPlaceId(place.id); + setMemoDraft(place.memo ?? ""); + }; + + const handleSaveMemo = () => { + if (!editingPlaceId) { + return; + } + + const nextMemo = memoDraft.trim(); + setPlaceMemos((previous) => { + const next = { ...previous }; + if (nextMemo) { + next[editingPlaceId] = nextMemo; + } else { + delete next[editingPlaceId]; + } + return next; }); - }, [activeCategory, selectedCity, selectedDistrict]); + setEditingPlaceId(null); + setMemoDraft(""); + }; - if (selectedPlace) { - return ( -
- setSelectedPlace(null)} /> - - -
- ); - } + const handleDeletePlace = (id: string) => { + setRemovedPlaceIds((previous) => (previous.includes(id) ? previous : [...previous, id])); + setOpenMenuId(null); + setPlaceMemos((previous) => { + if (!(id in previous)) { + return previous; + } + const next = { ...previous }; + delete next[id]; + return next; + }); + if (editingPlaceId === id) { + setEditingPlaceId(null); + setMemoDraft(""); + } + if (selectedPlaceId === id) { + closeDetail(); + } + }; return (
- {preview ? : null} -
- { - setIsRegionPanelOpen(true); - setIsRegionSearchMode(false); - }} - onSelect={setActiveCategory} - /> + {detailOpen ? ( +
+ }> + + +
+ ) : null} - {isRegionPanelOpen ? ( - setIsRegionPanelOpen(false)} - onConfirm={() => setIsRegionPanelOpen(false)} - /> - ) : null} +
+
+ {detailOpen ? ( + + ) : ( + + )} +

+ 목록 +

+ + {displayedCountLabel} + +
+ + {!detailOpen ? ( +
+ - {filteredPlaces.length > 0 ? ( -
- {filteredPlaces.map((place) => ( - - ))} +
+ { + void retryLoad(); + }} + activeCategories={activeCategories} + focusedCategory={focusedCategory} + onToggleCategory={toggleCategory} + isTagPanelOpen={isTagPanelOpen} + selectedTagKeysByCategory={selectedTagKeysByCategory} + selectedTagCountByCategory={selectedTagCountByCategory} + onToggleTagInCategory={toggleTagInCategory} + onResetFocusedCategoryTags={resetFocusedCategoryTags} + onCloseTagPanel={closeTagPanel} + /> +
- ) : ( - - )} -
- - + ) : null} + + + setIsRegionPanelOpen(false)} + > + setIsRegionPanelOpen(false)} + onConfirm={handleConfirmRegion} + /> + + + {!detailOpen ? ( +
+ {displayedPlaces.length > 0 ? ( +
+ {displayedPlaces.map((place) => ( + setOpenMenuId((current) => (current === id ? null : id))} + onStartMemo={handleStartMemo} + onChangeMemo={setMemoDraft} + onSaveMemo={handleSaveMemo} + onClearMemo={() => setMemoDraft("")} + onDelete={handleDeletePlace} + onSelect={(p) => openPlaceDetail(p.id)} + /> + ))} +
+ ) : ( +
+ + + +

{emptyMessage}

+
+ )} +
+ ) : null} + +
+ + +
+ +
); }