diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index c8fe978..3dd33eb 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -101,3 +101,12 @@ html, body, #__next { .safe-area-bottom { padding-bottom: env(safe-area-inset-bottom, 0px); } + +/* Filter sheet slide-up animation */ +@keyframes slide-up { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} +.animate-slide-up { + animation: slide-up 0.25s ease-out; +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 9e6c1a8..e93cbef 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -12,6 +12,7 @@ import RestaurantList from "@/components/RestaurantList"; import RestaurantDetail from "@/components/RestaurantDetail"; import MyReviewsList from "@/components/MyReviewsList"; import BottomSheet from "@/components/BottomSheet"; +import FilterSheet, { FilterOption } from "@/components/FilterSheet"; import { getCuisineIcon } from "@/lib/cuisine-icons"; import Icon from "@/components/Icon"; @@ -187,6 +188,7 @@ export default function Home() { const [countryFilter, setCountryFilter] = useState(""); const [cityFilter, setCityFilter] = useState(""); const [districtFilter, setDistrictFilter] = useState(""); + const [openSheet, setOpenSheet] = useState<"cuisine" | "price" | "country" | "city" | "district" | null>(null); const [regionFlyTo, setRegionFlyTo] = useState(null); const [showFavorites, setShowFavorites] = useState(false); const [showMyReviews, setShowMyReviews] = useState(false); @@ -247,6 +249,34 @@ export default function Home() { }); }, [restaurants, isSearchResult, channelFilter, cuisineFilter, priceFilter, countryFilter, cityFilter, districtFilter, boundsFilterOn, mapBounds, userLoc]); + // FilterSheet option builders + const cuisineOptions = useMemo(() => { + const opts: FilterOption[] = []; + for (const g of CUISINE_TAXONOMY) { + opts.push({ label: `${g.category} 전체`, value: g.category, group: g.category }); + for (const item of g.items) { + opts.push({ label: item, value: `${g.category}|${item}`, group: g.category }); + } + } + return opts; + }, []); + + const priceOptions = useMemo(() => + PRICE_GROUPS.map((g) => ({ label: g.label, value: g.label })), + []); + + const countryOptions = useMemo(() => + countries.map((c) => ({ label: c, value: c })), + [countries]); + + const cityOptions = useMemo(() => + cities.map((c) => ({ label: c, value: c })), + [cities]); + + const districtOptions = useMemo(() => + districts.map((d) => ({ label: d, value: d })), + [districts]); + // Set desktop default to map mode on mount + get user location useEffect(() => { if (window.innerWidth >= 768) setViewMode("map"); @@ -957,49 +987,30 @@ export default function Home() {
{/* Line 1: 음식 장르 + 가격 + 결과수 */}
-
+
-
+ + {cuisineFilter ? (cuisineFilter.includes("|") ? cuisineFilter.split("|")[1] : cuisineFilter) : "장르"} + + + +
+ + {priceFilter || "가격"} + + + {(cuisineFilter || priceFilter) && (
{/* Line 2: 나라 + 시 + 구 + 내위치 */}
-
+
- {countryFilter && cities.length > 0 && ( -
- - -
+ + {cityFilter || "시/도"} + + + )} {cityFilter && districts.length > 0 && ( -
- - -
+ )} {countryFilter && (
); } diff --git a/frontend/src/components/FilterSheet.tsx b/frontend/src/components/FilterSheet.tsx new file mode 100644 index 0000000..689bffb --- /dev/null +++ b/frontend/src/components/FilterSheet.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import Icon from "@/components/Icon"; + +export interface FilterOption { + label: string; + value: string; + group?: string; +} + +interface FilterSheetProps { + open: boolean; + onClose: () => void; + title: string; + options: FilterOption[]; + value: string; + onChange: (value: string) => void; +} + +export default function FilterSheet({ open, onClose, title, options, value, onChange }: FilterSheetProps) { + const sheetRef = useRef(null); + + useEffect(() => { + if (!open) return; + document.body.style.overflow = "hidden"; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + // Group options by group field + const grouped = options.reduce>((acc, opt) => { + const key = opt.group || ""; + if (!acc[key]) acc[key] = []; + acc[key].push(opt); + return acc; + }, {}); + const groups = Object.keys(grouped); + + const handleSelect = (v: string) => { + onChange(v); + onClose(); + }; + + if (!open) return null; + + return ( + <> + {/* Backdrop */} +
+ + {/* Sheet */} +
+ {/* Handle */} +
+
+
+ + {/* Header */} +
+

{title}

+ +
+ + {/* Options */} +
+ {/* 전체(초기화) */} + + + {groups.map((group) => ( +
+ {group && ( +
+ {group} +
+ )} + {grouped[group].map((opt) => ( + + ))} +
+ ))} +
+
+ + ); +}