채널 카드 필터 UI, 캐시 초기화, 나라 필터 수정
- 채널 설명/태그 DB 컬럼 추가 및 백오피스 편집 기능 - 채널 드롭다운을 유튜브 아이콘 토글 카드로 변경 (데스크톱 최대 4개 표시, 스크롤) - 모바일 홈탭 채널 카드 가로 스크롤 - region "나라" 값 필터 옵션에서 제외 - 관리자 캐시 초기화 버튼 및 API 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ function buildRegionTree(restaurants: Restaurant[]) {
|
||||
const tree = new Map<string, Map<string, Set<string>>>();
|
||||
for (const r of restaurants) {
|
||||
const p = parseRegion(r.region);
|
||||
if (!p || !p.country) continue;
|
||||
if (!p || !p.country || p.country === "나라") continue;
|
||||
if (!tree.has(p.country)) tree.set(p.country, new Map());
|
||||
const cityMap = tree.get(p.country)!;
|
||||
if (p.city) {
|
||||
@@ -109,7 +109,7 @@ function findRegionFromCoords(
|
||||
const groups = new Map<string, { country: string; city: string; lats: number[]; lngs: number[] }>();
|
||||
for (const r of restaurants) {
|
||||
const p = parseRegion(r.region);
|
||||
if (!p || !p.country || !p.city) continue;
|
||||
if (!p || !p.country || p.country === "나라" || !p.city) continue;
|
||||
const key = `${p.country}|${p.city}`;
|
||||
if (!groups.has(key)) groups.set(key, { country: p.country, city: p.city, lats: [], lngs: [] });
|
||||
const g = groups.get(key)!;
|
||||
@@ -564,22 +564,6 @@ export default function Home() {
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-5 h-5 fill-current"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
</button>
|
||||
<select
|
||||
value={channelFilter}
|
||||
onChange={(e) => {
|
||||
setChannelFilter(e.target.value);
|
||||
setSelected(null);
|
||||
setShowDetail(false);
|
||||
}}
|
||||
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
|
||||
>
|
||||
<option value="">📺 채널</option>
|
||||
{channels.map((ch) => (
|
||||
<option key={ch.id} value={ch.channel_name}>
|
||||
📺 {ch.channel_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={cuisineFilter}
|
||||
onChange={(e) => setCuisineFilter(e.target.value)}
|
||||
@@ -705,6 +689,37 @@ export default function Home() {
|
||||
{filteredRestaurants.length}개
|
||||
</span>
|
||||
</div>
|
||||
{/* Row 3: Channel cards (toggle filter) - max 4 visible, scroll for rest */}
|
||||
{!isSearchResult && channels.length > 0 && (
|
||||
<div className="overflow-x-auto scrollbar-hide" style={{ maxWidth: `${4 * 200 + 3 * 8}px` }}>
|
||||
<div className="flex gap-2">
|
||||
{channels.map((ch) => (
|
||||
<button
|
||||
key={ch.id}
|
||||
onClick={() => {
|
||||
setChannelFilter(channelFilter === ch.channel_name ? "" : ch.channel_name);
|
||||
setSelected(null);
|
||||
setShowDetail(false);
|
||||
}}
|
||||
className={`shrink-0 flex items-center gap-2 rounded-lg px-3 py-1.5 border transition-all text-left ${
|
||||
channelFilter === ch.channel_name
|
||||
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700"
|
||||
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-orange-200 dark:hover:border-orange-800"
|
||||
}`}
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0 text-red-500" viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`text-xs font-semibold truncate ${
|
||||
channelFilter === ch.channel_name ? "text-orange-600 dark:text-orange-400" : "dark:text-gray-200"
|
||||
}`}>{ch.channel_name}</p>
|
||||
{ch.description && <p className="text-[10px] text-gray-400 dark:text-gray-500 truncate">{ch.description}</p>}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User area */}
|
||||
@@ -742,6 +757,43 @@ export default function Home() {
|
||||
<div className={`md:hidden px-4 pb-3 space-y-2 ${mobileTab === "favorites" || mobileTab === "profile" ? "hidden" : ""}`}>
|
||||
{/* Row 1: Search */}
|
||||
<SearchBar key={resetCount} onSearch={handleSearch} isLoading={loading} />
|
||||
{/* Channel cards - toggle filter */}
|
||||
{mobileTab === "home" && !isSearchResult && channels.length > 0 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 -mx-1 px-1 scrollbar-hide">
|
||||
{channels.map((ch) => (
|
||||
<button
|
||||
key={ch.id}
|
||||
onClick={() => {
|
||||
setChannelFilter(channelFilter === ch.channel_name ? "" : ch.channel_name);
|
||||
setSelected(null);
|
||||
setShowDetail(false);
|
||||
}}
|
||||
className={`shrink-0 rounded-xl px-3 py-2 text-left border transition-all ${
|
||||
channelFilter === ch.channel_name
|
||||
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700"
|
||||
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
|
||||
}`}
|
||||
style={{ minWidth: "140px", maxWidth: "170px" }}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 shrink-0 text-red-500" viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
<p className={`text-xs font-semibold truncate ${
|
||||
channelFilter === ch.channel_name ? "text-orange-600 dark:text-orange-400" : "dark:text-gray-200"
|
||||
}`}>{ch.channel_name}</p>
|
||||
</div>
|
||||
{ch.description && <p className="text-[10px] text-gray-500 dark:text-gray-400 truncate mt-0.5">{ch.description}</p>}
|
||||
{ch.tags && (
|
||||
<div className="flex gap-1 mt-1 overflow-hidden">
|
||||
{ch.tags.split(",").slice(0, 2).map((t) => (
|
||||
<span key={t} className="text-[9px] px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded-full whitespace-nowrap">{t.trim()}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Toolbar */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -769,22 +821,6 @@ export default function Home() {
|
||||
<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
|
||||
value={channelFilter}
|
||||
onChange={(e) => {
|
||||
setChannelFilter(e.target.value);
|
||||
setSelected(null);
|
||||
setShowDetail(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>
|
||||
{channels.map((ch) => (
|
||||
<option key={ch.id} value={ch.channel_name}>
|
||||
📺 {ch.channel_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={cuisineFilter}
|
||||
onChange={(e) => setCuisineFilter(e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user