diff --git a/src/entities/story/api/getStoryDetail.ts b/src/entities/story/api/getStoryDetail.ts index e4397f1..f0ee236 100644 --- a/src/entities/story/api/getStoryDetail.ts +++ b/src/entities/story/api/getStoryDetail.ts @@ -1,10 +1,10 @@ import { instance } from '@/shared/api/ky' -import { StoryFeedItemSchema } from '@/entities/story/model/schema' -import type { StoryFeedItem } from '@/entities/story/model/types' +import { UserStoriesDataSchema } from '@/entities/story/model/schema' +import type { UserStoriesData } from '@/entities/story/model/types' -export async function getStoryDetail(userId: string): Promise { +export async function getStoryDetail(userId: string): Promise { const response = await instance.get(`api/v1/stories/user/${userId}`) const raw = (await response.json()) as { data: unknown } - const parsed = StoryFeedItemSchema.parse(raw.data) + const parsed = UserStoriesDataSchema.parse(raw.data) return parsed } diff --git a/src/entities/story/api/getUserStories.ts b/src/entities/story/api/getUserStories.ts new file mode 100644 index 0000000..1e77696 --- /dev/null +++ b/src/entities/story/api/getUserStories.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' +import { instance } from '@/shared/api/ky' +import { UserStoriesDataSchema } from '@/entities/story/model/schema' +import type { UserStoriesData } from '@/entities/story/model/types' + +const UserStoriesResponseSchema = z.object({ + isSuccess: z.boolean(), + code: z.string(), + message: z.string(), + data: UserStoriesDataSchema, +}) + +export async function getUserStories(userId: number): Promise { + const response = await instance.get(`api/v1/stories/user/${userId}`) + const raw = await response.json() + const parsed = UserStoriesResponseSchema.parse(raw) + + return parsed.data +} diff --git a/src/entities/story/model/hooks/useUserStoriesQuery.ts b/src/entities/story/model/hooks/useUserStoriesQuery.ts new file mode 100644 index 0000000..5c217a2 --- /dev/null +++ b/src/entities/story/model/hooks/useUserStoriesQuery.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' +import { getUserStories } from '@/entities/story/api/getUserStories' + +export function useUserStoriesQuery(userId: number) { + return useQuery({ + queryKey: ['stories', 'user', userId], + queryFn: () => getUserStories(userId), + enabled: userId > 0, + }) +} diff --git a/src/entities/story/model/schema.ts b/src/entities/story/model/schema.ts index ad83c6c..9c5e582 100644 --- a/src/entities/story/model/schema.ts +++ b/src/entities/story/model/schema.ts @@ -23,3 +23,8 @@ export const StoryFeedResponseSchema = z.object({ message: z.string(), data: z.array(StoryFeedItemSchema), }) + +export const UserStoriesDataSchema = z.object({ + hasUnseenStory: z.boolean(), + stories: z.array(StorySchema), +}) diff --git a/src/entities/story/model/types.ts b/src/entities/story/model/types.ts index 7b346f5..003bb59 100644 --- a/src/entities/story/model/types.ts +++ b/src/entities/story/model/types.ts @@ -21,3 +21,8 @@ export interface StoryResponse { message: string data: T } + +export interface UserStoriesData { + hasUnseenStory: boolean + stories: Story[] +} diff --git a/src/mocks/handlers/story.ts b/src/mocks/handlers/story.ts index 27f2a84..760f7ea 100644 --- a/src/mocks/handlers/story.ts +++ b/src/mocks/handlers/story.ts @@ -139,21 +139,14 @@ export const storyHandlers = [ viewCount: s.viewCount, })) - const responseData = StoryFeedItemSchema.parse({ - userId: String(user.userId), - nickname: user.nickname, - profileImageUrl: user.profileImageUrl, - hasUnseenStory: false, - stories: userStories, - }) - - const responseBody = ApiResponseSchema(StoryFeedItemSchema).parse({ - code: '200', - message: '요청에 성공하였습니다.', - data: responseData, + return HttpResponse.json({ + code: 'COMMON200', + message: '성공입니다.', + data: { + hasUnseenStory: Math.random() > 0.5, + stories: userStories, + }, isSuccess: true, }) - - return HttpResponse.json(responseBody) }), ] diff --git a/src/routes/stories/$user_id.tsx b/src/routes/stories/$user_id.tsx index 424b31e..b44292b 100644 --- a/src/routes/stories/$user_id.tsx +++ b/src/routes/stories/$user_id.tsx @@ -3,7 +3,9 @@ import { StoryViewer } from '@/features/story-viewer/ui/StoryViewer' import { useStoryFeedQuery } from '@/entities/story/model/hooks/useStoryFeedQuery' import { useQuery } from '@tanstack/react-query' import { getStoryDetail } from '@/entities/story/api/getStoryDetail' +import { useProfile } from '@/entities/user/model/hooks/useProfile' import { useMemo } from 'react' +import type { StoryFeedItem } from '@/entities/story/model/types' export const Route = createFileRoute('/stories/$user_id')({ component: RouteComponent, @@ -12,6 +14,7 @@ export const Route = createFileRoute('/stories/$user_id')({ function RouteComponent() { const { user_id } = Route.useParams() const { data: feedData } = useStoryFeedQuery() + const { data: profileData } = useProfile(Number(user_id)) const { data: detailData, isLoading: isDetailLoading } = useQuery({ queryKey: ['stories', 'user', user_id], @@ -19,6 +22,17 @@ function RouteComponent() { enabled: !!user_id, }) + const detailUser: StoryFeedItem | undefined = useMemo(() => { + if (!detailData || !profileData) return undefined + return { + userId: String(profileData.userId), + nickname: profileData.nickname, + profileImageUrl: profileData.profileImageUrl, + hasUnseenStory: detailData.hasUnseenStory, + stories: detailData.stories, + } + }, [detailData, profileData]) + const mergedFeed = useMemo(() => { if (!feedData) return [] @@ -44,7 +58,7 @@ function RouteComponent() { diff --git a/src/widgets/profile-header/ui/ProfileAvatar.tsx b/src/widgets/profile-header/ui/ProfileAvatar.tsx index b03d41a..db844a2 100644 --- a/src/widgets/profile-header/ui/ProfileAvatar.tsx +++ b/src/widgets/profile-header/ui/ProfileAvatar.tsx @@ -1,3 +1,4 @@ +import { Link } from '@tanstack/react-router' import { cn } from '@/shared/lib/utils' import { DefaultProfileImage } from '@/shared/ui/default-profile-image' @@ -6,18 +7,60 @@ import { AVATAR_SIZE_CLASSNAME } from './constants' type ProfileAvatarProps = { avatarUrl?: string | null nickname: string + hasStory?: boolean + hasUnseenStory?: boolean + firstStoryId?: number } -export function ProfileAvatar({ avatarUrl, nickname }: ProfileAvatarProps) { - if (avatarUrl) { +export function ProfileAvatar({ + avatarUrl, + nickname, + hasStory = false, + hasUnseenStory = false, + firstStoryId, +}: ProfileAvatarProps) { + const avatarContent = avatarUrl ? ( + {`${nickname} + ) : ( + + ) + + if (!hasStory) { + return avatarContent + } + + const ringClassName = hasUnseenStory + ? 'bg-gradient-to-tr from-yellow-400 via-red-500 to-purple-600' + : 'bg-gray-300' + + const content = ( +
+
+
+
+
{avatarContent}
+
+ ) + + if (firstStoryId) { return ( - {`${nickname} + + {content} + ) } - return + return content } diff --git a/src/widgets/profile-header/ui/ProfileHeader.tsx b/src/widgets/profile-header/ui/ProfileHeader.tsx index d270152..80d8830 100644 --- a/src/widgets/profile-header/ui/ProfileHeader.tsx +++ b/src/widgets/profile-header/ui/ProfileHeader.tsx @@ -8,6 +8,7 @@ import { FollowListModal } from '@/features/follow-user/ui/FollowListModal' import { useState } from 'react' import { type FollowListType } from '@/features/follow-user/ui/FollowListModal' import { Link } from '@tanstack/react-router' +import { useUserStoriesQuery } from '@/entities/story/model/hooks/useUserStoriesQuery' interface ProfileHeaderProps { className?: string @@ -44,6 +45,9 @@ export function ProfileHeader({ type: null, }) + const { data: userStoriesData } = useUserStoriesQuery(userId) + const hasStory = (userStoriesData?.stories.length ?? 0) > 0 + const handleFollowListClick = () => { setModalState({ open: true, @@ -76,7 +80,13 @@ export function ProfileHeader({
- +