모바일 UI 개선: 내주변 지도전용, 필터 상시노출, 채널필터 전탭 확장

- 내주변 탭: 지도만 전체 표시 (리스트 제거), 마커 클릭 시 바텀시트 상세보기 유지
- 유튜버 채널 필터: 홈/식당목록/내주변 탭 모두에서 표시
- 모바일 필터: 토글 패널 → 항상 보이는 2줄 레이아웃 (장르+가격 / 나라+내위치)
- 모바일 헤더에 찜/리뷰 버튼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-11 20:49:42 +09:00
parent e85e135c8b
commit ec8330a978

View File

@@ -862,17 +862,37 @@ export default function Home() {
</div>
{/* 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 ? (
<div className="flex items-center gap-2">
<div className="flex items-center">
{user.avatar_url ? (
<img
src={user.avatar_url}
alt=""
className="w-8 h-8 rounded-full border border-gray-200"
/>
<img src={user.avatar_url} alt="" className="w-7 h-7 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()}
</div>
)}
@@ -888,7 +908,7 @@ export default function Home() {
{/* Row 1: Search */}
<SearchBar key={resetCount} onSearch={handleSearch} isLoading={loading} />
{/* 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">
{channels.map((ch) => (
<button
@@ -924,133 +944,127 @@ export default function Home() {
</div>
)}
{/* Row 2: Toolbar */}
<div className="flex items-center gap-2">
<button
onClick={() => setShowMobileFilters(!showMobileFilters)}
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>
{/* Row 2: Filters - always visible, 2 lines */}
<div className="space-y-1.5">
{/* Line 1: 음식 장르 + 가격 + 결과수 */}
<div className="flex items-center gap-1.5 text-xs">
<select
value={cuisineFilter}
onChange={(e) => { setCuisineFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
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>
{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}`}>
&nbsp;&nbsp;{item}
</option>
))}
</optgroup>
))}
</select>
<select
value={priceFilter}
onChange={(e) => { setPriceFilter(e.target.value); if (e.target.value) setBoundsFilterOn(false); }}
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>
{PRICE_GROUPS.map((g) => (
<option key={g.label} value={g.label}>{g.label}</option>
))}
</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>
)}
</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">
<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">
<select
value={countryFilter}
onChange={(e) => handleCountryChange(e.target.value)}
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>
{countries.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
{countryFilter && cities.length > 0 && (
<select
value={cuisineFilter}
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"
>
<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}`}>
&nbsp;&nbsp;{item}
</option>
))}
</optgroup>
))}
</select>
<select
value={priceFilter}
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"
>
<option value="">💰 </option>
{PRICE_GROUPS.map((g) => (
<option key={g.label} value={g.label}>💰 {g.label}</option>
))}
</select>
</div>
{/* Region filters */}
<div className="flex items-center gap-2 flex-wrap">
<select
value={countryFilter}
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"
>
<option value="">🌍 </option>
{countries.map((c) => (
<option key={c} value={c}>🌍 {c}</option>
))}
</select>
{countryFilter && cities.length > 0 && (
<select
value={cityFilter}
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"
>
<option value="">🏙 /</option>
{cities.map((c) => (
<option key={c} value={c}>🏙 {c}</option>
))}
</select>
)}
{cityFilter && districts.length > 0 && (
<select
value={districtFilter}
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"
>
<option value="">🏘 /</option>
{districts.map((d) => (
<option key={d} value={d}>🏘 {d}</option>
))}
</select>
)}
</div>
{/* Toggle buttons */}
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => {
const next = !boundsFilterOn;
setBoundsFilterOn(next);
if (next) {
// 내위치 ON 시 다른 필터 초기화
setCuisineFilter("");
setPriceFilter("");
setCountryFilter("");
setCityFilter("");
setDistrictFilter("");
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => 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={`px-2.5 py-1.5 text-xs border rounded-lg transition-colors ${
boundsFilterOn
? "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"
value={cityFilter}
onChange={(e) => handleCityChange(e.target.value)}
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"
}`}
>
{boundsFilterOn ? "📍 내위치 ON" : "📍 내위치"}
<option value="">/</option>
{cities.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
)}
{cityFilter && districts.length > 0 && (
<select
value={districtFilter}
onChange={(e) => handleDistrictChange(e.target.value)}
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>
{districts.map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
)}
{countryFilter && (
<button onClick={() => { setCountryFilter(""); setCityFilter(""); setDistrictFilter(""); setRegionFlyTo(null); }} 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>
</div>
)}
<button
onClick={() => {
const next = !boundsFilterOn;
setBoundsFilterOn(next);
if (next) {
setCuisineFilter("");
setPriceFilter("");
setCountryFilter("");
setCityFilter("");
setDistrictFilter("");
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => 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-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"
}`}
>
<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>
</div>
)}
</div>
</div>
</header>
@@ -1109,32 +1123,27 @@ export default function Home() {
<div className="md:hidden flex-1 flex flex-col overflow-hidden pb-14">
{/* Tab content — takes all remaining space above fixed nav */}
{mobileTab === "nearby" ? (
/* 내주변: 지도 + 리스트 분할, 영역필터 ON */
<div className="flex-1 flex flex-col overflow-hidden">
<div className="h-[45%] relative shrink-0">
<MapView
restaurants={filteredRestaurants}
selected={selected}
onSelectRestaurant={handleSelectRestaurant}
onBoundsChanged={handleBoundsChanged}
flyTo={regionFlyTo}
onMyLocation={handleMyLocation}
activeChannel={channelFilter || undefined}
/>
<div className="absolute top-2 left-2 bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm rounded-lg px-3 py-1.5 shadow-sm z-10">
<span className="text-xs font-medium text-orange-600 dark:text-orange-400">
{filteredRestaurants.length}
</span>
/* 내주변: 지도만 전체 표시, 영역필터 ON */
<div className="flex-1 relative overflow-hidden">
<MapView
restaurants={filteredRestaurants}
selected={selected}
onSelectRestaurant={handleSelectRestaurant}
onBoundsChanged={handleBoundsChanged}
flyTo={regionFlyTo}
onMyLocation={handleMyLocation}
activeChannel={channelFilter || undefined}
/>
<div className="absolute top-2 left-2 bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm rounded-lg px-3 py-1.5 shadow-sm z-10">
<span className="text-xs font-medium text-orange-600 dark:text-orange-400">
{filteredRestaurants.length}
</span>
</div>
{visits && (
<div className="absolute bottom-1 right-2 bg-white/60 backdrop-blur-sm text-gray-700 text-[10px] px-2.5 py-1 rounded-lg shadow-sm z-10">
{visits.today} · {visits.total.toLocaleString()}
</div>
{visits && (
<div className="absolute bottom-1 right-2 bg-white/60 backdrop-blur-sm text-gray-700 text-[10px] px-2.5 py-1 rounded-lg shadow-sm z-10">
{visits.today} · {visits.total.toLocaleString()}
</div>
)}
</div>
<div className="flex-1 overflow-y-auto">
{mobileListContent}
</div>
)}
</div>
) : mobileTab === "profile" ? (
/* 내정보 */