diff --git a/src/app/(app)/home/page.tsx b/src/app/(app)/home/page.tsx index c5aa46e..fa87c52 100644 --- a/src/app/(app)/home/page.tsx +++ b/src/app/(app)/home/page.tsx @@ -1,16 +1,55 @@ "use client"; -import { useEffect, useState } from "react"; +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 HomeHeader from "@/components/layout/HomeHeader"; -import PageFade from "@/components/ui/PageFade"; import PlaceDetailSheet from "@/components/sheet/PlaceDetailSheet"; -import { getPopularFeed } from "@/lib/api/posts"; +import PageFade from "@/components/ui/PageFade"; import { getPopularPlaces } from "@/lib/api/places"; +import { getPopularFeed } from "@/lib/api/posts"; import { getUserProfile } from "@/lib/api/users"; -import type { FeedPost } from "@/types/api/post"; 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", +]; + +const COMMUNITY_FALLBACK_IMAGES = [ + "/trips-covers/cover-coastal-temple.png", + "/trips-covers/cover-harbor-market.png", +]; + +function avatarLabel(name: string | null | undefined) { + const normalized = (name ?? "누비").replace(/[0-9\s_-]/g, ""); + if (/^[가-힣]+$/.test(normalized) && normalized.length >= 3) { + return normalized.slice(1, 3); + } + return normalized.slice(0, 2).toUpperCase() || "누비"; +} + +function greetingName(name: string | null | undefined) { + const normalized = (name ?? "").replace(/\d+/g, "").trim(); + return normalized || null; +} + +function CardImage({ src, alt }: { src: string; alt: string }) { + return ( + {alt} + ); +} export default function HomePage() { const { data: session, status } = useSession(); @@ -22,116 +61,183 @@ export default function HomePage() { useEffect(() => { if (status === "loading") return; - getPopularFeed({ size: 2 }) - .then((res) => setCommunityPosts(res.items)) + let cancelled = false; + + void getPopularFeed({ size: 2 }) + .then((response) => { + if (!cancelled) setCommunityPosts(response.items.slice(0, 2)); + }) .catch(() => {}); - getPopularPlaces({ size: 10 }) - .then((res) => setPopularPlaces(res.items)) + void getPopularPlaces({ size: 2 }) + .then((response) => { + if (!cancelled) setPopularPlaces(response.items.slice(0, 2)); + }) .catch(() => {}); + + return () => { + cancelled = true; + }; }, [status]); useEffect(() => { const userId = session?.user?.id != null ? Number(session.user.id) : null; if (!userId) return; + let cancelled = false; + getUserProfile(userId) .then((profile) => { + if (cancelled) return; setUserName(profile.nickname); setProfileImageUrl(profile.profileImageUrl); }) - .catch(() => setUserName(session?.user?.name ?? null)); + .catch(() => { + if (!cancelled) setUserName(session?.user?.name ?? null); + }); + + return () => { + cancelled = true; + }; }, [session]); + const displayName = greetingName(userName ?? session?.user?.name); + return ( - - + + + +
+ +
+
-
-

- 어서오세요{userName ? ` ${userName}님` : ""}, +

+ 어서오세요{displayName ? ` ${displayName}님` : ""},
오늘은 어디로 떠날까요?

+
-
-
-
-
-

AI 맞춤 일정 만들기

-

- 취향만 알려주면 3초 만에 +

+
+ 광안대교와 부산 바다 + +
+ +
+

+ AI 맞춤 일정 만들기 +

+

+ 취향만 알려주면 3분 만에
완벽한 여행 코스를 짜드려요

- 시작하기 → + 시작하기 +
-
+ +
-

지금 인기 여행지

+

+ 지금 인기 여행지 +

{popularPlaces.length > 0 ? ( -
- {popularPlaces.map((place) => ( - - ))} -
+
+ {popularPlaces.map((place, index) => { + const imageUrl = place.primaryImageUrl || POPULAR_FALLBACK_IMAGES[index % POPULAR_FALLBACK_IMAGES.length]; + return ( + + ); + })} +
) : ( -

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

+

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

)}
{communityPosts.length > 0 && (
-

커뮤니티 인기글

-
- {communityPosts.map((post) => ( - -
- {post.thumbnailUrl && ( - + 커뮤니티 인기글 + +
+ {communityPosts.map((post, index) => { + const imageUrl = post.thumbnailUrl || COMMUNITY_FALLBACK_IMAGES[index % COMMUNITY_FALLBACK_IMAGES.length]; + return ( + + + - )} -
-
-

{post.placeName ?? post.content}

-

- {post.categories[0] ?? "여행후기"} · 댓글 {post.commentCount} · {post.createdAgo} -

-
- - ))} + + + + {post.author.nickname} + + + {post.categories[0] ?? "여행후기"} · 댓글 {post.commentCount} + + + + ); + })}
)} diff --git a/src/components/layout/HomeHeader.tsx b/src/components/layout/HomeHeader.tsx index 21468b3..be90380 100644 --- a/src/components/layout/HomeHeader.tsx +++ b/src/components/layout/HomeHeader.tsx @@ -12,7 +12,13 @@ import { useNotificationStream } from "@/lib/notifications/use-notification-stre const NOTIFICATION_COUNT_REFRESH_EVENT = "notifications:count-refresh"; const NOTIFICATION_COUNT_POLLING_MS = 60_000; -export default function HomeHeader({ profileImageUrl }: { profileImageUrl?: string | null }) { +export default function HomeHeader({ + profileImageUrl, + profileLabel = "누비", +}: { + profileImageUrl?: string | null; + profileLabel?: string; +}) { const { status } = useSession(); const profileImage = profileImageUrl; const [unreadCount, setUnreadCount] = useState(0); @@ -52,14 +58,14 @@ export default function HomeHeader({ profileImageUrl }: { profileImageUrl?: stri }, [refreshUnreadCount, status]); return ( -
+
누비
{status === "authenticated" && unreadCount > 0 && ( @@ -71,10 +77,19 @@ export default function HomeHeader({ profileImageUrl }: { profileImageUrl?: stri - {profileImage && ( - 프로필 + {profileImage ? ( + 프로필 + ) : ( + {profileLabel} )}