diff --git a/public/trips-covers/destination-gamcheon-sunset.png b/public/trips-covers/destination-gamcheon-sunset.png new file mode 100644 index 0000000..4effe72 Binary files /dev/null and b/public/trips-covers/destination-gamcheon-sunset.png differ diff --git a/src/app/(app)/home/page.tsx b/src/app/(app)/home/page.tsx index fa87c52..f1b386a 100644 --- a/src/app/(app)/home/page.tsx +++ b/src/app/(app)/home/page.tsx @@ -4,22 +4,38 @@ import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import { ArrowRight, MapPin } from "lucide-react"; +import { ArrowRight, LoaderCircle, MapPin } from "lucide-react"; +import { toast } from "sonner"; import HomeHeader from "@/components/layout/HomeHeader"; import PlaceDetailSheet from "@/components/sheet/PlaceDetailSheet"; import PageFade from "@/components/ui/PageFade"; -import { getPopularPlaces } from "@/lib/api/places"; +import { getPopularPlaces, resolvePlace, searchPlaces } from "@/lib/api/places"; import { getPopularFeed } from "@/lib/api/posts"; import { getUserProfile } from "@/lib/api/users"; import type { PlaceSummary } from "@/types/api/place"; import type { FeedPost } from "@/types/api/post"; const POPULAR_FALLBACK_IMAGES = [ - "/trips-covers/cover-gwangalli.png", - "/trips-covers/cover-hillside.png", + "/trips-covers/destination-gwangalli-millak.png", + "/trips-covers/destination-gamcheon-sunset.png", ]; +const POPULAR_FALLBACK_PLACES = [ + { + key: "gwangalli", + imageUrl: POPULAR_FALLBACK_IMAGES[0], + subtitle: "해운대 · 광안리", + keyword: "광안리해수욕장", + }, + { + key: "gamcheon", + imageUrl: POPULAR_FALLBACK_IMAGES[1], + subtitle: "감천문화마을 · 태종대", + keyword: "감천문화마을", + }, +] as const; + const COMMUNITY_FALLBACK_IMAGES = [ "/trips-covers/cover-coastal-temple.png", "/trips-covers/cover-harbor-market.png", @@ -57,6 +73,9 @@ export default function HomePage() { const [profileImageUrl, setProfileImageUrl] = useState(null); const [communityPosts, setCommunityPosts] = useState([]); const [popularPlaces, setPopularPlaces] = useState([]); + const [popularLoading, setPopularLoading] = useState(true); + const [resolvingFallback, setResolvingFallback] = useState(null); + const [fallbackPlaceIds, setFallbackPlaceIds] = useState>({}); const [detailPlaceId, setDetailPlaceId] = useState(null); useEffect(() => { @@ -72,7 +91,12 @@ export default function HomePage() { .then((response) => { if (!cancelled) setPopularPlaces(response.items.slice(0, 2)); }) - .catch(() => {}); + .catch(() => { + if (!cancelled) setPopularPlaces([]); + }) + .finally(() => { + if (!cancelled) setPopularLoading(false); + }); return () => { cancelled = true; @@ -101,6 +125,35 @@ export default function HomePage() { const displayName = greetingName(userName ?? session?.user?.name); + async function openFallbackPlace(place: (typeof POPULAR_FALLBACK_PLACES)[number]) { + if (resolvingFallback) return; + + const cachedPlaceId = fallbackPlaceIds[place.key]; + if (cachedPlaceId != null) { + setDetailPlaceId(cachedPlaceId); + return; + } + + setResolvingFallback(place.key); + try { + const response = await searchPlaces(place.keyword); + const exactMatch = response.items.find((item) => item.name.includes(place.keyword)); + const match = exactMatch ?? response.items[0]; + if (!match) throw new Error("추천 장소를 찾지 못했습니다."); + + const resolvedPlaceId = + match.placeId !== null && match.resolved + ? match.placeId + : (await resolvePlace(match)).placeId; + setFallbackPlaceIds((current) => ({ ...current, [place.key]: resolvedPlaceId })); + setDetailPlaceId(resolvedPlaceId); + } catch { + toast.error("장소 정보를 불러오지 못했어요. 잠시 후 다시 시도해주세요."); + } finally { + setResolvingFallback(null); + } + } + return ( 지금 인기 여행지 - {popularPlaces.length > 0 ? ( + {popularLoading ? ( +
+ {[0, 1].map((item) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : popularPlaces.length > 0 ? (
{popularPlaces.map((place, index) => { const imageUrl = place.primaryImageUrl || POPULAR_FALLBACK_IMAGES[index % POPULAR_FALLBACK_IMAGES.length]; @@ -201,9 +269,39 @@ export default function HomePage() { })}
) : ( -

- 인기 여행지 정보를 준비 중이에요 -

+
+ {POPULAR_FALLBACK_PLACES.map((place) => { + const resolving = resolvingFallback === place.key; + return ( + + ); + })} +
)} diff --git a/src/app/(app)/trips/page.tsx b/src/app/(app)/trips/page.tsx index 54fd56e..45f2956 100644 --- a/src/app/(app)/trips/page.tsx +++ b/src/app/(app)/trips/page.tsx @@ -204,7 +204,7 @@ export default function TripsPage() { }, [sorted, today]); return ( -
+
{error}

) : ( -
+
{featured && ( -
+

가장 가까운 여행

-
+
)} -
+
-
+

모든 일정 {sorted.length} diff --git a/src/app/globals.css b/src/app/globals.css index b9ecf5f..3dccc8b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -53,5 +53,7 @@ body { } .scrollbar-none::-webkit-scrollbar { + width: 0; + height: 0; display: none; }