홈 탭 장르 카드 UI + Tabler Icons 적용 + 지역 필터 추가
- 홈 탭: 장르 가로 스크롤 카드 (Tabler Icons 픽토그램) - 홈 탭: 가격/지역/내위치 필터 2줄 배치 - 리스트 탭: 기존 바텀시트 필터 UI 유지 - cuisine-icons: Tabler 아이콘 매핑 추가 (getTablerCuisineIcon) - 드래그 스크롤 장르 카드에 적용 - 배포 가이드 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
frontend/package-lock.json
generated
27
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@react-oauth/google": "^0.13.4",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"@vis.gl/react-google-maps": "^1.7.1",
|
||||
"next": "16.1.6",
|
||||
@@ -1257,6 +1258,32 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.40.0.tgz",
|
||||
"integrity": "sha512-V/Q4VgNPKubRTiLdmWjV/zscYcj5IIk+euicUtaVVqF6luSC9rDngYWgST5/yh3Mrg/mYUwRv1YVTk71Jp0twQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons-react": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.40.0.tgz",
|
||||
"integrity": "sha512-oO5+6QCnna4a//mYubx4euZfECtzQZFDGsDMIdzZUhbdyBCT+3bRVFBPueGIcemWld4Vb/0UQ39C/cmGfGylAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tabler/icons": "3.40.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-oauth/google": "^0.13.4",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"@vis.gl/react-google-maps": "^1.7.1",
|
||||
"next": "16.1.6",
|
||||
|
||||
@@ -13,8 +13,9 @@ 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 { getCuisineIcon, getTablerCuisineIcon } from "@/lib/cuisine-icons";
|
||||
import Icon from "@/components/Icon";
|
||||
import * as TablerIcons from "@tabler/icons-react";
|
||||
|
||||
function useDragScroll() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -201,6 +202,7 @@ export default function Home() {
|
||||
const geoApplied = useRef(false);
|
||||
const dd = useDragScroll();
|
||||
const dm = useDragScroll();
|
||||
const dg = useDragScroll(); // genre card drag scroll
|
||||
|
||||
const regionTree = useMemo(() => buildRegionTree(restaurants), [restaurants]);
|
||||
const countries = useMemo(() => [...regionTree.keys()].sort(), [regionTree]);
|
||||
@@ -990,117 +992,254 @@ export default function Home() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Filters - always visible, 2 lines */}
|
||||
{/* Row 2: Filters */}
|
||||
<div className="space-y-1.5">
|
||||
{/* Line 1: 음식 장르 + 가격 + 결과수 */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<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"}`} />
|
||||
<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"}`} />
|
||||
<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} />
|
||||
</button>
|
||||
)}
|
||||
<span className="text-[10px] text-gray-400 ml-auto tabular-nums">{filteredRestaurants.length}개</span>
|
||||
</div>
|
||||
{/* Line 2: 나라 + 시 + 구 + 내위치 */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<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"}`} />
|
||||
<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 && (
|
||||
{/* Home tab: 장르 가로 스크롤 */}
|
||||
{mobileTab === "home" && (
|
||||
<div ref={dg.ref} onMouseDown={dg.onMouseDown} onMouseMove={dg.onMouseMove} onMouseUp={dg.onMouseUp} onMouseLeave={dg.onMouseLeave} onClickCapture={dg.onClickCapture} style={dg.style} className="flex gap-2 overflow-x-auto scrollbar-hide -mx-1 px-1 pb-1 select-none">
|
||||
{(() => {
|
||||
const allCards = [
|
||||
{ label: "전체", value: "", icon: "Bowl" },
|
||||
...CUISINE_TAXONOMY.flatMap((g) => [
|
||||
{ label: g.category, value: g.category, icon: getTablerCuisineIcon(g.category) },
|
||||
...g.items.map((item) => ({ label: item, value: `${g.category}|${item}`, icon: getTablerCuisineIcon(`${g.category}|${item}`) })),
|
||||
]),
|
||||
];
|
||||
return allCards.map((card) => {
|
||||
const isCategory = card.value === "" || !card.value.includes("|");
|
||||
const selected = card.value === ""
|
||||
? !cuisineFilter
|
||||
: isCategory
|
||||
? cuisineFilter === card.value || cuisineFilter.startsWith(card.value + "|")
|
||||
: cuisineFilter === card.value;
|
||||
const TablerIcon = (TablerIcons as unknown as Record<string, React.ComponentType<{ size?: number; stroke?: number; className?: string }>>)[`Icon${card.icon}`] || TablerIcons.IconBowl;
|
||||
return (
|
||||
<button
|
||||
key={card.value || "__all__"}
|
||||
onClick={() => {
|
||||
if (card.value === "") { setCuisineFilter(""); }
|
||||
else if (cuisineFilter === card.value) { setCuisineFilter(""); }
|
||||
else { setCuisineFilter(card.value); setBoundsFilterOn(false); }
|
||||
}}
|
||||
className={`shrink-0 flex flex-col items-center gap-1 rounded-xl px-2.5 py-2 min-w-[56px] transition-all ${
|
||||
selected
|
||||
? "bg-brand-500 text-white shadow-sm"
|
||||
: isCategory
|
||||
? "bg-brand-50 border border-brand-200 text-brand-700"
|
||||
: "bg-white border border-gray-100 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<TablerIcon size={22} stroke={1.5} className={selected ? "text-white" : isCategory ? "text-brand-500" : "text-gray-400"} />
|
||||
<span className={`text-[11px] whitespace-nowrap ${isCategory ? "font-semibold" : "font-medium"}`}>{card.label}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{/* Home tab: 가격 + 지역 + 내위치 + 개수 */}
|
||||
{mobileTab === "home" && (
|
||||
<div className="flex items-center gap-1.5 text-xs flex-wrap">
|
||||
<button
|
||||
onClick={() => setOpenSheet("city")}
|
||||
onClick={() => setOpenSheet("price")}
|
||||
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"
|
||||
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={cityFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{cityFilter || "시/도"}
|
||||
<Icon name="payments" size={14} className={`mr-1 ${priceFilter ? "text-brand-500" : "text-gray-400"}`} />
|
||||
<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>
|
||||
)}
|
||||
{cityFilter && districts.length > 0 && (
|
||||
<button
|
||||
onClick={() => setOpenSheet("district")}
|
||||
onClick={() => setOpenSheet("country")}
|
||||
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"
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<span className={districtFilter ? "text-brand-600 dark:text-brand-400 font-medium" : "text-gray-500 dark:text-gray-400"}>
|
||||
{districtFilter || "구/군"}
|
||||
<Icon name="public" size={14} className={`mr-1 ${countryFilter ? "text-brand-500" : "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 && (
|
||||
<button onClick={() => { setCountryFilter(""); setCityFilter(""); setDistrictFilter(""); setRegionFlyTo(null); }} className="text-gray-400 hover:text-brand-500">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = !boundsFilterOn;
|
||||
setBoundsFilterOn(next);
|
||||
if (next) {
|
||||
setCuisineFilter("");
|
||||
setPriceFilter("");
|
||||
setCountryFilter("");
|
||||
setCityFilter("");
|
||||
setDistrictFilter("");
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },
|
||||
() => setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 }),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
} else {
|
||||
setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 });
|
||||
{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"
|
||||
}`}
|
||||
>
|
||||
<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 && (
|
||||
<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>
|
||||
)}
|
||||
{(cuisineFilter || priceFilter || countryFilter) && (
|
||||
<button onClick={() => { setCuisineFilter(""); setPriceFilter(""); setCountryFilter(""); setCityFilter(""); setDistrictFilter(""); }} className="text-gray-400 hover:text-brand-500">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = !boundsFilterOn;
|
||||
setBoundsFilterOn(next);
|
||||
if (next) {
|
||||
setCuisineFilter("");
|
||||
setPriceFilter("");
|
||||
setCountryFilter("");
|
||||
setCityFilter("");
|
||||
setDistrictFilter("");
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },
|
||||
() => setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 }),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
} else {
|
||||
setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`flex items-center gap-0.5 rounded-lg px-2 py-1 border transition-colors ${
|
||||
boundsFilterOn
|
||||
? "bg-brand-50 dark:bg-brand-900/30 border-brand-300 dark:border-brand-700 text-brand-600 dark:text-brand-400"
|
||||
: "border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon name="location_on" size={12} />
|
||||
<span>{boundsFilterOn ? "내위치 ON" : "내위치"}</span>
|
||||
</button>
|
||||
</div>
|
||||
}}
|
||||
className={`inline-flex items-center gap-0.5 rounded-full px-3 py-1.5 transition-colors ${
|
||||
boundsFilterOn
|
||||
? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700 text-brand-600 dark:text-brand-400"
|
||||
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon name="location_on" size={14} />
|
||||
<span>{boundsFilterOn ? "내위치 ON" : "내위치"}</span>
|
||||
</button>
|
||||
<span className="text-[10px] text-gray-400 ml-auto tabular-nums">{filteredRestaurants.length}개</span>
|
||||
</div>
|
||||
)}
|
||||
{/* List tab: 기존 필터 UI */}
|
||||
{mobileTab === "list" && (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<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"}`} />
|
||||
<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"}`} />
|
||||
<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} />
|
||||
</button>
|
||||
)}
|
||||
<span className="text-[10px] text-gray-400 ml-auto tabular-nums">{filteredRestaurants.length}개</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<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"}`} />
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
<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 && (
|
||||
<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">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = !boundsFilterOn;
|
||||
setBoundsFilterOn(next);
|
||||
if (next) {
|
||||
setCuisineFilter("");
|
||||
setPriceFilter("");
|
||||
setCountryFilter("");
|
||||
setCityFilter("");
|
||||
setDistrictFilter("");
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => { setUserLoc({ lat: pos.coords.latitude, lng: pos.coords.longitude }); setRegionFlyTo({ lat: pos.coords.latitude, lng: pos.coords.longitude, zoom: 15 }); },
|
||||
() => setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 }),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
} else {
|
||||
setRegionFlyTo({ lat: 37.498, lng: 127.0276, zoom: 15 });
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`inline-flex items-center gap-0.5 rounded-full px-3 py-1.5 transition-colors ${
|
||||
boundsFilterOn
|
||||
? "bg-brand-50 dark:bg-brand-900/30 ring-1 ring-brand-300 dark:ring-brand-700 text-brand-600 dark:text-brand-400"
|
||||
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon name="location_on" size={14} />
|
||||
<span>{boundsFilterOn ? "내위치 ON" : "내위치"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,49 +1,154 @@
|
||||
/**
|
||||
* Cuisine type → Material Symbols icon name mapping.
|
||||
* Cuisine type → icon mapping.
|
||||
* Material Symbols icon name for RestaurantList (existing usage).
|
||||
* Tabler icon component name for genre card chips (home tab).
|
||||
*
|
||||
* Works with "대분류|소분류" format (e.g. "한식|국밥/해장국").
|
||||
*/
|
||||
|
||||
// ── Material Symbols (for RestaurantList etc.) ──
|
||||
|
||||
const CUISINE_ICON_MAP: Record<string, string> = {
|
||||
"한식": "rice_bowl",
|
||||
"일식": "set_meal",
|
||||
"중식": "ramen_dining",
|
||||
"중식": "skillet",
|
||||
"양식": "dinner_dining",
|
||||
"아시아": "ramen_dining",
|
||||
"아시아": "restaurant",
|
||||
"기타": "flatware",
|
||||
};
|
||||
|
||||
// Sub-category overrides for more specific icons
|
||||
const SUB_ICON_RULES: { keyword: string; icon: string }[] = [
|
||||
{ keyword: "회/횟집", icon: "set_meal" },
|
||||
{ keyword: "해산물", icon: "set_meal" },
|
||||
{ keyword: "백반/한정식", icon: "rice_bowl" },
|
||||
{ keyword: "국밥/해장국", icon: "soup_kitchen" },
|
||||
{ keyword: "찌개/전골/탕", icon: "outdoor_grill" },
|
||||
{ keyword: "삼겹살/돼지구이", icon: "kebab_dining" },
|
||||
{ keyword: "소고기/한우구이", icon: "kebab_dining" },
|
||||
{ keyword: "곱창/막창", icon: "kebab_dining" },
|
||||
{ keyword: "소고기/한우구이", icon: "local_fire_department" },
|
||||
{ keyword: "곱창/막창", icon: "local_fire_department" },
|
||||
{ keyword: "닭/오리구이", icon: "takeout_dining" },
|
||||
{ keyword: "스테이크", icon: "kebab_dining" },
|
||||
{ keyword: "족발/보쌈", icon: "stockpot" },
|
||||
{ keyword: "회/횟집", icon: "phishing" },
|
||||
{ keyword: "해산물", icon: "set_meal" },
|
||||
{ keyword: "분식", icon: "egg_alt" },
|
||||
{ keyword: "면", icon: "ramen_dining" },
|
||||
{ keyword: "죽/죽집", icon: "soup_kitchen" },
|
||||
{ keyword: "순대/순대국", icon: "soup_kitchen" },
|
||||
{ keyword: "장어/민물", icon: "phishing" },
|
||||
{ keyword: "주점/포차", icon: "local_bar" },
|
||||
{ keyword: "파인다이닝/코스", icon: "auto_awesome" },
|
||||
{ keyword: "스시/오마카세", icon: "set_meal" },
|
||||
{ keyword: "라멘", icon: "ramen_dining" },
|
||||
{ keyword: "돈카츠", icon: "lunch_dining" },
|
||||
{ keyword: "텐동/튀김", icon: "tapas" },
|
||||
{ keyword: "이자카야", icon: "sake" },
|
||||
{ keyword: "야키니쿠", icon: "kebab_dining" },
|
||||
{ keyword: "카레", icon: "skillet" },
|
||||
{ keyword: "소바/우동", icon: "ramen_dining" },
|
||||
{ keyword: "중화요리", icon: "skillet" },
|
||||
{ keyword: "마라/훠궈", icon: "outdoor_grill" },
|
||||
{ keyword: "딤섬/만두", icon: "egg_alt" },
|
||||
{ keyword: "양꼬치", icon: "kebab_dining" },
|
||||
{ keyword: "파스타/이탈리안", icon: "dinner_dining" },
|
||||
{ keyword: "스테이크", icon: "restaurant" },
|
||||
{ keyword: "햄버거", icon: "lunch_dining" },
|
||||
{ keyword: "피자", icon: "local_pizza" },
|
||||
{ keyword: "프렌치", icon: "auto_awesome" },
|
||||
{ keyword: "바베큐", icon: "outdoor_grill" },
|
||||
{ keyword: "브런치", icon: "brunch_dining" },
|
||||
{ keyword: "비건/샐러드", icon: "eco" },
|
||||
{ keyword: "베트남", icon: "ramen_dining" },
|
||||
{ keyword: "태국", icon: "restaurant" },
|
||||
{ keyword: "인도/중동", icon: "skillet" },
|
||||
{ keyword: "동남아기타", icon: "restaurant" },
|
||||
{ keyword: "치킨", icon: "takeout_dining" },
|
||||
{ keyword: "카페/디저트", icon: "coffee" },
|
||||
{ keyword: "베이커리", icon: "bakery_dining" },
|
||||
{ keyword: "치킨", icon: "takeout_dining" },
|
||||
{ keyword: "주점/포차", icon: "local_bar" },
|
||||
{ keyword: "이자카야", icon: "sake" },
|
||||
{ keyword: "라멘", icon: "ramen_dining" },
|
||||
{ keyword: "국밥/해장국", icon: "soup_kitchen" },
|
||||
{ keyword: "분식", icon: "ramen_dining" },
|
||||
{ keyword: "뷔페", icon: "brunch_dining" },
|
||||
{ keyword: "퓨전", icon: "auto_awesome" },
|
||||
];
|
||||
|
||||
const DEFAULT_ICON = "flatware";
|
||||
|
||||
export function getCuisineIcon(cuisineType: string | null | undefined): string {
|
||||
if (!cuisineType) return DEFAULT_ICON;
|
||||
|
||||
// Check sub-category first
|
||||
for (const rule of SUB_ICON_RULES) {
|
||||
if (cuisineType.includes(rule.keyword)) return rule.icon;
|
||||
}
|
||||
|
||||
// Fall back to main category (prefix before |)
|
||||
const main = cuisineType.split("|")[0];
|
||||
return CUISINE_ICON_MAP[main] || DEFAULT_ICON;
|
||||
}
|
||||
|
||||
// ── Tabler Icons (for genre card chips) ──
|
||||
// Returns Tabler icon component name (PascalCase without "Icon" prefix)
|
||||
|
||||
const TABLER_CUISINE_MAP: Record<string, string> = {
|
||||
"한식": "BowlChopsticks",
|
||||
"일식": "Fish",
|
||||
"중식": "Soup",
|
||||
"양식": "Pizza",
|
||||
"아시아": "BowlSpoon",
|
||||
"기타": "Cookie",
|
||||
};
|
||||
|
||||
const TABLER_SUB_RULES: { keyword: string; icon: string }[] = [
|
||||
// 한식
|
||||
{ keyword: "백반/한정식", icon: "BowlChopsticks" },
|
||||
{ keyword: "국밥/해장국", icon: "Soup" },
|
||||
{ keyword: "찌개/전골/탕", icon: "Cooker" },
|
||||
{ keyword: "삼겹살/돼지구이", icon: "Meat" },
|
||||
{ keyword: "소고기/한우구이", icon: "Grill" },
|
||||
{ keyword: "곱창/막창", icon: "GrillFork" },
|
||||
{ keyword: "닭/오리구이", icon: "Meat" },
|
||||
{ keyword: "족발/보쌈", icon: "Meat" },
|
||||
{ keyword: "회/횟집", icon: "Fish" },
|
||||
{ keyword: "해산물", icon: "Fish" },
|
||||
{ keyword: "분식", icon: "EggFried" },
|
||||
{ keyword: "면", icon: "BowlChopsticks" },
|
||||
{ keyword: "죽/죽집", icon: "BowlSpoon" },
|
||||
{ keyword: "순대/순대국", icon: "Soup" },
|
||||
{ keyword: "장어/민물", icon: "Fish" },
|
||||
{ keyword: "주점/포차", icon: "Beer" },
|
||||
{ keyword: "파인다이닝/코스", icon: "GlassChampagne" },
|
||||
// 일식
|
||||
{ keyword: "스시/오마카세", icon: "Fish" },
|
||||
{ keyword: "라멘", icon: "Soup" },
|
||||
{ keyword: "돈카츠", icon: "Meat" },
|
||||
{ keyword: "텐동/튀김", icon: "EggFried" },
|
||||
{ keyword: "이자카야", icon: "GlassCocktail" },
|
||||
{ keyword: "야키니쿠", icon: "Grill" },
|
||||
{ keyword: "카레", icon: "BowlSpoon" },
|
||||
{ keyword: "소바/우동", icon: "BowlChopsticks" },
|
||||
// 중식
|
||||
{ keyword: "중화요리", icon: "Soup" },
|
||||
{ keyword: "마라/훠궈", icon: "Pepper" },
|
||||
{ keyword: "딤섬/만두", icon: "Egg" },
|
||||
{ keyword: "양꼬치", icon: "Grill" },
|
||||
// 양식
|
||||
{ keyword: "파스타/이탈리안", icon: "BowlSpoon" },
|
||||
{ keyword: "스테이크", icon: "Meat" },
|
||||
{ keyword: "햄버거", icon: "Burger" },
|
||||
{ keyword: "피자", icon: "Pizza" },
|
||||
{ keyword: "프렌치", icon: "GlassChampagne" },
|
||||
{ keyword: "바베큐", icon: "GrillSpatula" },
|
||||
{ keyword: "브런치", icon: "EggFried" },
|
||||
{ keyword: "비건/샐러드", icon: "Salad" },
|
||||
// 아시아
|
||||
{ keyword: "베트남", icon: "BowlChopsticks" },
|
||||
{ keyword: "태국", icon: "Pepper" },
|
||||
{ keyword: "인도/중동", icon: "BowlSpoon" },
|
||||
{ keyword: "동남아기타", icon: "BowlSpoon" },
|
||||
// 기타
|
||||
{ keyword: "치킨", icon: "Meat" },
|
||||
{ keyword: "카페/디저트", icon: "Coffee" },
|
||||
{ keyword: "베이커리", icon: "Bread" },
|
||||
{ keyword: "뷔페", icon: "Cheese" },
|
||||
{ keyword: "퓨전", icon: "Cookie" },
|
||||
];
|
||||
|
||||
export function getTablerCuisineIcon(cuisineType: string | null | undefined): string {
|
||||
if (!cuisineType) return "Bowl";
|
||||
for (const rule of TABLER_SUB_RULES) {
|
||||
if (cuisineType.includes(rule.keyword)) return rule.icon;
|
||||
}
|
||||
const main = cuisineType.split("|")[0];
|
||||
return TABLER_CUISINE_MAP[main] || "Bowl";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user