모바일 UI 개선: 내주변 지도전용, 필터 상시노출, 채널필터 전탭 확장
- 내주변 탭: 지도만 전체 표시 (리스트 제거), 마커 클릭 시 바텀시트 상세보기 유지 - 유튜버 채널 필터: 홈/식당목록/내주변 탭 모두에서 표시 - 모바일 필터: 토글 패널 → 항상 보이는 2줄 레이아웃 (장르+가격 / 나라+내위치) - 모바일 헤더에 찜/리뷰 버튼 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -862,17 +862,37 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User area (mobile only - desktop moved to Row 1) */}
|
{/* User area (mobile only - desktop moved to Row 1) */}
|
||||||
<div className="shrink-0 flex items-center gap-3 ml-auto md:hidden">
|
<div className="shrink-0 flex items-center gap-2 ml-auto md:hidden">
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleFavorites}
|
||||||
|
className={`px-2 py-0.5 text-[10px] rounded-full border transition-colors ${
|
||||||
|
showFavorites
|
||||||
|
? "bg-rose-50 dark:bg-rose-900/30 border-rose-300 dark:border-rose-700 text-rose-600 dark:text-rose-400"
|
||||||
|
: "border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{showFavorites ? "♥ 찜" : "♡ 찜"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleMyReviews}
|
||||||
|
className={`px-2 py-0.5 text-[10px] rounded-full border transition-colors ${
|
||||||
|
showMyReviews
|
||||||
|
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
|
||||||
|
: "border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{showMyReviews ? "✎ 리뷰" : "✎ 리뷰"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{authLoading ? null : user ? (
|
{authLoading ? null : user ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center">
|
||||||
{user.avatar_url ? (
|
{user.avatar_url ? (
|
||||||
<img
|
<img src={user.avatar_url} alt="" className="w-7 h-7 rounded-full border border-gray-200" />
|
||||||
src={user.avatar_url}
|
|
||||||
alt=""
|
|
||||||
className="w-8 h-8 rounded-full border border-gray-200"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-sm font-semibold border border-orange-200">
|
<div className="w-7 h-7 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-semibold border border-orange-200">
|
||||||
{(user.nickname || user.email || "?").charAt(0).toUpperCase()}
|
{(user.nickname || user.email || "?").charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -888,7 +908,7 @@ export default function Home() {
|
|||||||
{/* Row 1: Search */}
|
{/* Row 1: Search */}
|
||||||
<SearchBar key={resetCount} onSearch={handleSearch} isLoading={loading} />
|
<SearchBar key={resetCount} onSearch={handleSearch} isLoading={loading} />
|
||||||
{/* Channel cards - toggle filter */}
|
{/* Channel cards - toggle filter */}
|
||||||
{mobileTab === "home" && !isSearchResult && channels.length > 0 && (
|
{(mobileTab === "home" || mobileTab === "list" || mobileTab === "nearby") && !isSearchResult && channels.length > 0 && (
|
||||||
<div ref={dm.ref} onMouseDown={dm.onMouseDown} onMouseMove={dm.onMouseMove} onMouseUp={dm.onMouseUp} onMouseLeave={dm.onMouseLeave} onClickCapture={dm.onClickCapture} style={dm.style} className="flex gap-2 overflow-x-auto pb-1 -mx-1 px-1 scrollbar-hide select-none">
|
<div ref={dm.ref} onMouseDown={dm.onMouseDown} onMouseMove={dm.onMouseMove} onMouseUp={dm.onMouseUp} onMouseLeave={dm.onMouseLeave} onClickCapture={dm.onClickCapture} style={dm.style} className="flex gap-2 overflow-x-auto pb-1 -mx-1 px-1 scrollbar-hide select-none">
|
||||||
{channels.map((ch) => (
|
{channels.map((ch) => (
|
||||||
<button
|
<button
|
||||||
@@ -924,42 +944,21 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Row 2: Toolbar */}
|
{/* Row 2: Filters - always visible, 2 lines */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-1.5">
|
||||||
<button
|
{/* Line 1: 음식 장르 + 가격 + 결과수 */}
|
||||||
onClick={() => setShowMobileFilters(!showMobileFilters)}
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
className={`px-3 py-1.5 text-xs border rounded-lg transition-colors relative ${
|
|
||||||
showMobileFilters || channelFilter || cuisineFilter || priceFilter || countryFilter
|
|
||||||
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
|
|
||||||
: "text-gray-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{showMobileFilters ? "✕ 닫기" : "🔽 필터"}
|
|
||||||
{!showMobileFilters && (channelFilter || cuisineFilter || priceFilter || countryFilter) && (
|
|
||||||
<span className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-orange-500 text-white rounded-full text-[9px] flex items-center justify-center">
|
|
||||||
{[channelFilter, cuisineFilter, priceFilter, countryFilter].filter(Boolean).length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="text-xs text-gray-400 ml-auto">
|
|
||||||
{filteredRestaurants.length}개
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Collapsible filter panel */}
|
|
||||||
{showMobileFilters && (
|
|
||||||
<div className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-md rounded-xl p-3.5 space-y-3 border border-white/50 dark:border-gray-700/50 shadow-sm">
|
|
||||||
{/* Dropdown filters */}
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<select
|
<select
|
||||||
value={cuisineFilter}
|
value={cuisineFilter}
|
||||||
onChange={(e) => { setCuisineFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
onChange={(e) => { setCuisineFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
||||||
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
|
className={`border dark:border-gray-700 rounded-lg px-2 py-1 bg-white dark:bg-gray-800 ${
|
||||||
|
cuisineFilter ? "text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-700" : "text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">🍽 장르</option>
|
<option value="">🍽 장르</option>
|
||||||
{CUISINE_TAXONOMY.map((g) => (
|
{CUISINE_TAXONOMY.map((g) => (
|
||||||
<optgroup key={g.category} label={`── ${g.category} ──`}>
|
<optgroup key={g.category} label={`── ${g.category} ──`}>
|
||||||
<option value={g.category}>🍽 {g.category} 전체</option>
|
<option value={g.category}>{g.category} 전체</option>
|
||||||
{g.items.map((item) => (
|
{g.items.map((item) => (
|
||||||
<option key={`${g.category}|${item}`} value={`${g.category}|${item}`}>
|
<option key={`${g.category}|${item}`} value={`${g.category}|${item}`}>
|
||||||
{item}
|
{item}
|
||||||
@@ -971,35 +970,47 @@ export default function Home() {
|
|||||||
<select
|
<select
|
||||||
value={priceFilter}
|
value={priceFilter}
|
||||||
onChange={(e) => { setPriceFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
onChange={(e) => { setPriceFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
|
||||||
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
|
className={`border dark:border-gray-700 rounded-lg px-2 py-1 bg-white dark:bg-gray-800 ${
|
||||||
|
priceFilter ? "text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-700" : "text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">💰 가격</option>
|
<option value="">💰 가격</option>
|
||||||
{PRICE_GROUPS.map((g) => (
|
{PRICE_GROUPS.map((g) => (
|
||||||
<option key={g.label} value={g.label}>💰 {g.label}</option>
|
<option key={g.label} value={g.label}>{g.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{(cuisineFilter || priceFilter) && (
|
||||||
|
<button onClick={() => { setCuisineFilter(""); setPriceFilter(""); }} className="text-gray-400 hover:text-orange-500">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5 fill-current"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-gray-400 ml-auto tabular-nums">{filteredRestaurants.length}개</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Region filters */}
|
{/* Line 2: 나라 + 시 + 구 + 내위치 */}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
<select
|
<select
|
||||||
value={countryFilter}
|
value={countryFilter}
|
||||||
onChange={(e) => handleCountryChange(e.target.value)}
|
onChange={(e) => handleCountryChange(e.target.value)}
|
||||||
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
|
className={`border dark:border-gray-700 rounded-lg px-2 py-1 bg-white dark:bg-gray-800 ${
|
||||||
|
countryFilter ? "text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-700" : "text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">🌍 나라</option>
|
<option value="">🌍 나라</option>
|
||||||
{countries.map((c) => (
|
{countries.map((c) => (
|
||||||
<option key={c} value={c}>🌍 {c}</option>
|
<option key={c} value={c}>{c}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{countryFilter && cities.length > 0 && (
|
{countryFilter && cities.length > 0 && (
|
||||||
<select
|
<select
|
||||||
value={cityFilter}
|
value={cityFilter}
|
||||||
onChange={(e) => handleCityChange(e.target.value)}
|
onChange={(e) => handleCityChange(e.target.value)}
|
||||||
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
|
className={`border dark:border-gray-700 rounded-lg px-2 py-1 bg-white dark:bg-gray-800 ${
|
||||||
|
cityFilter ? "text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-700" : "text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">🏙 전체 시/도</option>
|
<option value="">시/도</option>
|
||||||
{cities.map((c) => (
|
{cities.map((c) => (
|
||||||
<option key={c} value={c}>🏙 {c}</option>
|
<option key={c} value={c}>{c}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
@@ -1007,23 +1018,26 @@ export default function Home() {
|
|||||||
<select
|
<select
|
||||||
value={districtFilter}
|
value={districtFilter}
|
||||||
onChange={(e) => handleDistrictChange(e.target.value)}
|
onChange={(e) => handleDistrictChange(e.target.value)}
|
||||||
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
|
className={`border dark:border-gray-700 rounded-lg px-2 py-1 bg-white dark:bg-gray-800 ${
|
||||||
|
districtFilter ? "text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-700" : "text-gray-500 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">🏘 전체 구/군</option>
|
<option value="">구/군</option>
|
||||||
{districts.map((d) => (
|
{districts.map((d) => (
|
||||||
<option key={d} value={d}>🏘 {d}</option>
|
<option key={d} value={d}>{d}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
</div>
|
{countryFilter && (
|
||||||
{/* Toggle buttons */}
|
<button onClick={() => { setCountryFilter(""); setCityFilter(""); setDistrictFilter(""); setRegionFlyTo(null); }} className="text-gray-400 hover:text-orange-500">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5 fill-current"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = !boundsFilterOn;
|
const next = !boundsFilterOn;
|
||||||
setBoundsFilterOn(next);
|
setBoundsFilterOn(next);
|
||||||
if (next) {
|
if (next) {
|
||||||
// 내위치 ON 시 다른 필터 초기화
|
|
||||||
setCuisineFilter("");
|
setCuisineFilter("");
|
||||||
setPriceFilter("");
|
setPriceFilter("");
|
||||||
setCountryFilter("");
|
setCountryFilter("");
|
||||||
@@ -1040,17 +1054,17 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`px-2.5 py-1.5 text-xs border rounded-lg transition-colors ${
|
className={`flex items-center gap-0.5 rounded-lg px-2 py-1 border transition-colors ${
|
||||||
boundsFilterOn
|
boundsFilterOn
|
||||||
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
|
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
|
||||||
: "text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800"
|
: "border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{boundsFilterOn ? "📍 내위치 ON" : "📍 내위치"}
|
<svg viewBox="0 0 24 24" className="w-3 h-3 fill-current"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>
|
||||||
|
<span>{boundsFilterOn ? "내위치 ON" : "내위치"}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -1109,9 +1123,8 @@ export default function Home() {
|
|||||||
<div className="md:hidden flex-1 flex flex-col overflow-hidden pb-14">
|
<div className="md:hidden flex-1 flex flex-col overflow-hidden pb-14">
|
||||||
{/* Tab content — takes all remaining space above fixed nav */}
|
{/* Tab content — takes all remaining space above fixed nav */}
|
||||||
{mobileTab === "nearby" ? (
|
{mobileTab === "nearby" ? (
|
||||||
/* 내주변: 지도 + 리스트 분할, 영역필터 ON */
|
/* 내주변: 지도만 전체 표시, 영역필터 ON */
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 relative overflow-hidden">
|
||||||
<div className="h-[45%] relative shrink-0">
|
|
||||||
<MapView
|
<MapView
|
||||||
restaurants={filteredRestaurants}
|
restaurants={filteredRestaurants}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
@@ -1132,10 +1145,6 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{mobileListContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : mobileTab === "profile" ? (
|
) : mobileTab === "profile" ? (
|
||||||
/* 내정보 */
|
/* 내정보 */
|
||||||
<div className="flex-1 overflow-y-auto bg-white dark:bg-gray-950">
|
<div className="flex-1 overflow-y-auto bg-white dark:bg-gray-950">
|
||||||
|
|||||||
Reference in New Issue
Block a user