diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 96a64a3..cfa6fb5 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -132,7 +132,7 @@ export default function Home() { const { user, login, logout, isLoading: authLoading } = useAuth(); const [restaurants, setRestaurants] = useState([]); const [selected, setSelected] = useState(null); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [showDetail, setShowDetail] = useState(false); const [channels, setChannels] = useState([]); const [channelFilter, setChannelFilter] = useState(""); @@ -199,10 +199,12 @@ export default function Home() { // Load restaurants on mount and when channel filter changes useEffect(() => { + setLoading(true); api .getRestaurants({ limit: 500, channel: channelFilter || undefined }) .then(setRestaurants) - .catch(console.error); + .catch(console.error) + .finally(() => setLoading(false)); }, [channelFilter]); // Auto-select region from user's geolocation (once) @@ -388,6 +390,7 @@ export default function Home() { restaurants={filteredRestaurants} selectedId={selected?.id} onSelect={handleSelectRestaurant} + loading={loading} /> ); @@ -410,6 +413,7 @@ export default function Home() { restaurants={filteredRestaurants} selectedId={selected?.id} onSelect={handleSelectRestaurant} + loading={loading} /> ); diff --git a/frontend/src/components/RestaurantDetail.tsx b/frontend/src/components/RestaurantDetail.tsx index bb7b3eb..5bd51c7 100644 --- a/frontend/src/components/RestaurantDetail.tsx +++ b/frontend/src/components/RestaurantDetail.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { api, getToken } from "@/lib/api"; import type { Restaurant, VideoLink } from "@/lib/api"; import ReviewSection from "@/components/ReviewSection"; +import { RestaurantDetailSkeleton } from "@/components/Skeleton"; interface RestaurantDetailProps { restaurant: Restaurant; @@ -137,7 +138,21 @@ export default function RestaurantDetail({

관련 영상

{loading ? ( -

로딩 중...

+
+ {[1, 2].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
) : videos.length === 0 ? (

관련 영상이 없습니다

) : ( diff --git a/frontend/src/components/RestaurantList.tsx b/frontend/src/components/RestaurantList.tsx index 747a95a..5be7858 100644 --- a/frontend/src/components/RestaurantList.tsx +++ b/frontend/src/components/RestaurantList.tsx @@ -2,18 +2,25 @@ import type { Restaurant } from "@/lib/api"; import { getCuisineIcon } from "@/lib/cuisine-icons"; +import { RestaurantListSkeleton } from "@/components/Skeleton"; interface RestaurantListProps { restaurants: Restaurant[]; selectedId?: string; onSelect: (r: Restaurant) => void; + loading?: boolean; } export default function RestaurantList({ restaurants, selectedId, onSelect, + loading, }: RestaurantListProps) { + if (loading) { + return ; + } + if (!restaurants.length) { return (
diff --git a/frontend/src/components/ReviewSection.tsx b/frontend/src/components/ReviewSection.tsx index f116c15..a19e242 100644 --- a/frontend/src/components/ReviewSection.tsx +++ b/frontend/src/components/ReviewSection.tsx @@ -200,7 +200,18 @@ export default function ReviewSection({ restaurantId }: ReviewSectionProps) {

리뷰

{loading ? ( -

로딩 중...

+
+
+
+
+
+ {[1, 2].map((i) => ( +
+
+
+
+ ))} +
) : ( <> {reviewCount > 0 && avgRating !== null && ( diff --git a/frontend/src/components/Skeleton.tsx b/frontend/src/components/Skeleton.tsx new file mode 100644 index 0000000..eec43f9 --- /dev/null +++ b/frontend/src/components/Skeleton.tsx @@ -0,0 +1,80 @@ +"use client"; + +/** Pulsing skeleton block */ +function Block({ className = "" }: { className?: string }) { + return
; +} + +/** Skeleton for a single restaurant list item */ +export function RestaurantCardSkeleton() { + return ( +
+ +
+ + + +
+
+ +
+
+ ); +} + +/** Skeleton for the restaurant list (multiple cards) */ +export function RestaurantListSkeleton({ count = 8 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }, (_, i) => ( + + ))} +
+ ); +} + +/** Skeleton for restaurant detail view */ +export function RestaurantDetailSkeleton() { + return ( +
+ {/* Title + close */} +
+ + +
+ + {/* Rating */} +
+ + +
+ + {/* Info lines */} +
+ + + + +
+ + {/* Videos section */} +
+ + {[1, 2].map((i) => ( +
+
+ + +
+ +
+ + +
+ +
+ ))} +
+
+ ); +}