UX improvements: mobile bottom sheet, cuisine taxonomy, search enhancements

- Add BottomSheet component for Google Maps-style restaurant detail on mobile
  (3-snap drag: 40%/55%/92%, velocity-based close, backdrop overlay)
- Mobile map mode now full-screen with bottom sheet overlay for details
- Collapsible filter panel on mobile with active filter badge count
- Standardized cuisine taxonomy (46 categories: 한식|국밥, 일식|스시 etc.)
  with LLM remap endpoint and admin UI button
- Enhanced search: keyword search now includes foods_mentioned + video title
- Search results include channels array for frontend filtering
- Channel filter moved to frontend filteredRestaurants (not API-level)
- LLM extraction prompt updated for pipe-delimited region + cuisine taxonomy
- Vector rebuild endpoint with rich JSON chunks per restaurant
- Geolocation-based auto region selection on page load
- Desktop filters split into two clean rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-09 10:54:28 +09:00
parent 3694730501
commit 2bddb0f764
16 changed files with 2277 additions and 308 deletions

View File

@@ -67,6 +67,8 @@ export interface Channel {
channel_id: string;
channel_name: string;
title_filter: string | null;
video_count: number;
last_scanned_at: string | null;
}
export interface Video {
@@ -107,6 +109,7 @@ export interface User {
email: string | null;
nickname: string | null;
avatar_url: string | null;
is_admin?: boolean;
}
export interface Review {
@@ -120,6 +123,17 @@ export interface Review {
user_avatar_url: string | null;
}
export interface DaemonConfig {
scan_enabled: boolean;
scan_interval_min: number;
process_enabled: boolean;
process_interval_min: number;
process_limit: number;
last_scan_at: string | null;
last_process_at: string | null;
updated_at: string | null;
}
export interface ReviewsResponse {
reviews: Review[];
avg_rating: number | null;
@@ -428,4 +442,29 @@ export const api = {
{ method: "PUT", body: JSON.stringify(data) }
);
},
// Daemon config
getDaemonConfig() {
return fetchApi<DaemonConfig>("/api/daemon/config");
},
updateDaemonConfig(data: Partial<DaemonConfig>) {
return fetchApi<{ ok: boolean }>("/api/daemon/config", {
method: "PUT",
body: JSON.stringify(data),
});
},
runDaemonScan() {
return fetchApi<{ ok: boolean; new_videos: number }>("/api/daemon/run/scan", {
method: "POST",
});
},
runDaemonProcess(limit: number = 10) {
return fetchApi<{ ok: boolean; restaurants_extracted: number }>(
`/api/daemon/run/process?limit=${limit}`,
{ method: "POST" }
);
},
};

View File

@@ -0,0 +1,49 @@
/**
* Cuisine type → icon mapping.
* Works with "대분류|소분류" format (e.g. "한식|국밥/해장국").
*/
const CUISINE_ICON_MAP: Record<string, string> = {
"한식": "🍚",
"일식": "🍣",
"중식": "🥟",
"양식": "🍝",
"아시아": "🍜",
"기타": "🍴",
};
// Sub-category overrides for more specific icons
const SUB_ICON_RULES: { keyword: string; icon: string }[] = [
{ keyword: "회/횟집", icon: "🐟" },
{ keyword: "해산물", icon: "🦐" },
{ keyword: "삼겹살/돼지구이", icon: "🥩" },
{ keyword: "소고기/한우구이", icon: "🥩" },
{ keyword: "곱창/막창", icon: "🥩" },
{ keyword: "닭/오리구이", icon: "🍗" },
{ keyword: "스테이크", icon: "🥩" },
{ keyword: "햄버거", icon: "🍔" },
{ keyword: "피자", icon: "🍕" },
{ keyword: "카페/디저트", icon: "☕" },
{ keyword: "베이커리", icon: "🥐" },
{ keyword: "치킨", icon: "🍗" },
{ keyword: "주점/포차", icon: "🍺" },
{ keyword: "이자카야", icon: "🍶" },
{ keyword: "라멘", icon: "🍜" },
{ keyword: "국밥/해장국", icon: "🍲" },
{ keyword: "분식", icon: "🍜" },
];
const DEFAULT_ICON = "🍴";
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;
}