모바일 필터 바텀시트 UI 적용
- FilterSheet 컴포넌트 신규: 바텀시트로 올라오는 필터 선택 UI - 장르/가격/지역 필터 모두 네이티브 select 대신 바텀시트 사용 - 카테고리별 그룹핑 + sticky 헤더 + 선택 체크 표시 - slide-up 애니메이션 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<FlyTo | null>(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<FilterOption[]>(() => {
|
||||
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<FilterOption[]>(() =>
|
||||
PRICE_GROUPS.map((g) => ({ label: g.label, value: g.label })),
|
||||
[]);
|
||||
|
||||
const countryOptions = useMemo<FilterOption[]>(() =>
|
||||
countries.map((c) => ({ label: c, value: c })),
|
||||
[countries]);
|
||||
|
||||
const cityOptions = useMemo<FilterOption[]>(() =>
|
||||
cities.map((c) => ({ label: c, value: c })),
|
||||
[cities]);
|
||||
|
||||
const districtOptions = useMemo<FilterOption[]>(() =>
|
||||
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() {
|
||||
<div className="space-y-1.5">
|
||||
{/* Line 1: 음식 장르 + 가격 + 결과수 */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<div className={`relative inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
cuisineFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => setOpenSheet("cuisine")}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
cuisineFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Icon name="restaurant" size={14} className={`mr-1 ${cuisineFilter ? "text-brand-500" : "text-gray-400"}`} />
|
||||
<select
|
||||
value={cuisineFilter}
|
||||
onChange={(e) => { setCuisineFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
||||
className={`bg-transparent border-none outline-none appearance-none pr-4 cursor-pointer ${
|
||||
cuisineFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<option value="">장르</option>
|
||||
{CUISINE_TAXONOMY.map((g) => (
|
||||
<optgroup key={g.category} label={`── ${g.category} ──`}>
|
||||
<option value={g.category}>{g.category} 전체</option>
|
||||
{g.items.map((item) => (
|
||||
<option key={`${g.category}|${item}`} value={`${g.category}|${item}`}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<Icon name="expand_more" size={14} className="absolute right-2 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
<div className={`relative inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
priceFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}>
|
||||
<span className={cuisineFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{cuisineFilter ? (cuisineFilter.includes("|") ? cuisineFilter.split("|")[1] : cuisineFilter) : "장르"}
|
||||
</span>
|
||||
<Icon name="expand_more" size={14} className="ml-0.5 text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpenSheet("price")}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
priceFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Icon name="payments" size={14} className={`mr-1 ${priceFilter ? "text-brand-500" : "text-gray-400"}`} />
|
||||
<select
|
||||
value={priceFilter}
|
||||
onChange={(e) => { setPriceFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
||||
className={`bg-transparent border-none outline-none appearance-none pr-4 cursor-pointer ${
|
||||
priceFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<option value="">가격</option>
|
||||
{PRICE_GROUPS.map((g) => (
|
||||
<option key={g.label} value={g.label}>{g.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<Icon name="expand_more" size={14} className="absolute right-2 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
<span className={priceFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{priceFilter || "가격"}
|
||||
</span>
|
||||
<Icon name="expand_more" size={14} className="ml-0.5 text-gray-400" />
|
||||
</button>
|
||||
{(cuisineFilter || priceFilter) && (
|
||||
<button onClick={() => { setCuisineFilter(""); setPriceFilter(""); }} className="text-gray-400 hover:text-brand-500">
|
||||
<Icon name="close" size={14} />
|
||||
@@ -1009,61 +1020,43 @@ export default function Home() {
|
||||
</div>
|
||||
{/* Line 2: 나라 + 시 + 구 + 내위치 */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<div className={`relative inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
countryFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => setOpenSheet("country")}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
countryFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Icon name="public" size={14} className={`mr-1 ${countryFilter ? "text-brand-500" : "text-gray-400"}`} />
|
||||
<select
|
||||
value={countryFilter}
|
||||
onChange={(e) => handleCountryChange(e.target.value)}
|
||||
className={`bg-transparent border-none outline-none appearance-none pr-4 cursor-pointer ${
|
||||
countryFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
|
||||
<span className={countryFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{countryFilter || "나라"}
|
||||
</span>
|
||||
<Icon name="expand_more" size={14} className="ml-0.5 text-gray-400" />
|
||||
</button>
|
||||
{countryFilter && cities.length > 0 && (
|
||||
<button
|
||||
onClick={() => setOpenSheet("city")}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
cityFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<option value="">나라</option>
|
||||
{countries.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<Icon name="expand_more" size={14} className="absolute right-2 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
{countryFilter && cities.length > 0 && (
|
||||
<div className={`relative inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
cityFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}>
|
||||
<select
|
||||
value={cityFilter}
|
||||
onChange={(e) => handleCityChange(e.target.value)}
|
||||
className={`bg-transparent border-none outline-none appearance-none pr-4 cursor-pointer ${
|
||||
cityFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<option value="">시/도</option>
|
||||
{cities.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<Icon name="expand_more" size={14} className="absolute right-2 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
<span className={cityFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{cityFilter || "시/도"}
|
||||
</span>
|
||||
<Icon name="expand_more" size={14} className="ml-0.5 text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
{cityFilter && districts.length > 0 && (
|
||||
<div className={`relative inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
districtFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}>
|
||||
<select
|
||||
value={districtFilter}
|
||||
onChange={(e) => handleDistrictChange(e.target.value)}
|
||||
className={`bg-transparent border-none outline-none appearance-none pr-4 cursor-pointer ${
|
||||
districtFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<option value="">구/군</option>
|
||||
{districts.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
<Icon name="expand_more" size={14} className="absolute right-2 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setOpenSheet("district")}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1.5 ${
|
||||
districtFilter ? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700" : "bg-gray-100 dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<span className={districtFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{districtFilter || "구/군"}
|
||||
</span>
|
||||
<Icon name="expand_more" size={14} className="ml-0.5 text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
{countryFilter && (
|
||||
<button onClick={() => { setCountryFilter(""); setCityFilter(""); setDistrictFilter(""); setRegionFlyTo(null); }} className="text-gray-400 hover:text-brand-500">
|
||||
@@ -1313,6 +1306,47 @@ export default function Home() {
|
||||
SDJ Labs Co., Ltd.
|
||||
</span>
|
||||
</footer>
|
||||
{/* Mobile Filter Sheets */}
|
||||
<FilterSheet
|
||||
open={openSheet === "cuisine"}
|
||||
onClose={() => setOpenSheet(null)}
|
||||
title="음식 장르"
|
||||
options={cuisineOptions}
|
||||
value={cuisineFilter}
|
||||
onChange={(v) => { setCuisineFilter(v); if (v) setBoundsFilterOn(false); }}
|
||||
/>
|
||||
<FilterSheet
|
||||
open={openSheet === "price"}
|
||||
onClose={() => setOpenSheet(null)}
|
||||
title="가격대"
|
||||
options={priceOptions}
|
||||
value={priceFilter}
|
||||
onChange={(v) => { setPriceFilter(v); if (v) setBoundsFilterOn(false); }}
|
||||
/>
|
||||
<FilterSheet
|
||||
open={openSheet === "country"}
|
||||
onClose={() => setOpenSheet(null)}
|
||||
title="나라"
|
||||
options={countryOptions}
|
||||
value={countryFilter}
|
||||
onChange={handleCountryChange}
|
||||
/>
|
||||
<FilterSheet
|
||||
open={openSheet === "city"}
|
||||
onClose={() => setOpenSheet(null)}
|
||||
title="시/도"
|
||||
options={cityOptions}
|
||||
value={cityFilter}
|
||||
onChange={handleCityChange}
|
||||
/>
|
||||
<FilterSheet
|
||||
open={openSheet === "district"}
|
||||
onClose={() => setOpenSheet(null)}
|
||||
title="구/군"
|
||||
options={districtOptions}
|
||||
value={districtFilter}
|
||||
onChange={handleDistrictChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user