Backend (FastAPI + Oracle ADB), Frontend (Next.js), daemon worker. Features: channel/video/restaurant management, semantic search, Google OAuth, user reviews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { GoogleLogin } from "@react-oauth/google";
|
|
import { api } from "@/lib/api";
|
|
import type { Restaurant } from "@/lib/api";
|
|
import { useAuth } from "@/lib/auth-context";
|
|
import MapView from "@/components/MapView";
|
|
import SearchBar from "@/components/SearchBar";
|
|
import RestaurantList from "@/components/RestaurantList";
|
|
import RestaurantDetail from "@/components/RestaurantDetail";
|
|
|
|
export default function Home() {
|
|
const { user, login, logout, isLoading: authLoading } = useAuth();
|
|
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
|
|
const [selected, setSelected] = useState<Restaurant | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [showDetail, setShowDetail] = useState(false);
|
|
|
|
// Load all restaurants on mount
|
|
useEffect(() => {
|
|
api
|
|
.getRestaurants({ limit: 200 })
|
|
.then(setRestaurants)
|
|
.catch(console.error);
|
|
}, []);
|
|
|
|
const handleSearch = useCallback(
|
|
async (query: string, mode: "keyword" | "semantic" | "hybrid") => {
|
|
setLoading(true);
|
|
try {
|
|
const results = await api.search(query, mode);
|
|
setRestaurants(results);
|
|
setSelected(null);
|
|
setShowDetail(false);
|
|
} catch (e) {
|
|
console.error("Search failed:", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleSelectRestaurant = useCallback((r: Restaurant) => {
|
|
setSelected(r);
|
|
setShowDetail(true);
|
|
}, []);
|
|
|
|
const handleCloseDetail = useCallback(() => {
|
|
setShowDetail(false);
|
|
}, []);
|
|
|
|
const handleReset = useCallback(() => {
|
|
setLoading(true);
|
|
api
|
|
.getRestaurants({ limit: 200 })
|
|
.then((data) => {
|
|
setRestaurants(data);
|
|
setSelected(null);
|
|
setShowDetail(false);
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col">
|
|
{/* Header */}
|
|
<header className="bg-white border-b px-4 py-3 flex items-center gap-4 shrink-0">
|
|
<button onClick={handleReset} className="text-lg font-bold whitespace-nowrap">
|
|
Tasteby
|
|
</button>
|
|
<div className="flex-1 max-w-xl">
|
|
<SearchBar onSearch={handleSearch} isLoading={loading} />
|
|
</div>
|
|
<span className="text-sm text-gray-500">
|
|
{restaurants.length}개 식당
|
|
</span>
|
|
<div className="shrink-0">
|
|
{authLoading ? null : user ? (
|
|
<div className="flex items-center gap-2">
|
|
{user.avatar_url && (
|
|
<img
|
|
src={user.avatar_url}
|
|
alt=""
|
|
className="w-7 h-7 rounded-full"
|
|
/>
|
|
)}
|
|
<span className="text-sm">{user.nickname || user.email}</span>
|
|
<button
|
|
onClick={logout}
|
|
className="text-xs text-gray-500 hover:text-gray-700"
|
|
>
|
|
로그아웃
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<GoogleLogin
|
|
onSuccess={(credentialResponse) => {
|
|
if (credentialResponse.credential) {
|
|
login(credentialResponse.credential).catch(console.error);
|
|
}
|
|
}}
|
|
onError={() => console.error("Google login failed")}
|
|
size="small"
|
|
/>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Body */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Sidebar */}
|
|
<aside className="w-80 bg-white border-r overflow-y-auto shrink-0">
|
|
{showDetail && selected ? (
|
|
<RestaurantDetail
|
|
restaurant={selected}
|
|
onClose={handleCloseDetail}
|
|
/>
|
|
) : (
|
|
<RestaurantList
|
|
restaurants={restaurants}
|
|
selectedId={selected?.id}
|
|
onSelect={handleSelectRestaurant}
|
|
/>
|
|
)}
|
|
</aside>
|
|
|
|
{/* Map */}
|
|
<main className="flex-1">
|
|
<MapView
|
|
restaurants={restaurants}
|
|
onSelectRestaurant={handleSelectRestaurant}
|
|
/>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|