Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/entities/story/api/getStoryDetail.ts
Original file line number Diff line number Diff line change
@@ -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<StoryFeedItem> {
export async function getStoryDetail(userId: string): Promise<UserStoriesData> {
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
}
19 changes: 19 additions & 0 deletions src/entities/story/api/getUserStories.ts
Original file line number Diff line number Diff line change
@@ -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<UserStoriesData> {
const response = await instance.get(`api/v1/stories/user/${userId}`)
const raw = await response.json()
const parsed = UserStoriesResponseSchema.parse(raw)

return parsed.data
}
10 changes: 10 additions & 0 deletions src/entities/story/model/hooks/useUserStoriesQuery.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
5 changes: 5 additions & 0 deletions src/entities/story/model/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
5 changes: 5 additions & 0 deletions src/entities/story/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export interface StoryResponse<T> {
message: string
data: T
}

export interface UserStoriesData {
hasUnseenStory: boolean
stories: Story[]
}
21 changes: 7 additions & 14 deletions src/mocks/handlers/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}),
]
16 changes: 15 additions & 1 deletion src/routes/stories/$user_id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,13 +14,25 @@ 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],
queryFn: () => getStoryDetail(user_id),
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 []

Expand All @@ -44,7 +58,7 @@ function RouteComponent() {
<StoryViewer
feed={mergedFeed}
userId={user_id}
detailUser={detailData}
detailUser={detailUser}
isDetailLoading={isDetailLoading}
/>
</div>
Expand Down
59 changes: 51 additions & 8 deletions src/widgets/profile-header/ui/ProfileAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Link } from '@tanstack/react-router'
import { cn } from '@/shared/lib/utils'
import { DefaultProfileImage } from '@/shared/ui/default-profile-image'

Expand All @@ -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 ? (
<img
src={avatarUrl}
alt={`${nickname} 프로필 이미지`}
className={cn(AVATAR_SIZE_CLASSNAME, 'rounded-full object-cover')}
/>
) : (
<DefaultProfileImage className={AVATAR_SIZE_CLASSNAME} />
)

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 = (
<div className="relative flex cursor-pointer items-center justify-center">
<div
className={cn(
'absolute size-[164px] rounded-full p-[3px]',
ringClassName
)}
>
<div className="size-full rounded-full bg-white" />
</div>
<div className="relative">{avatarContent}</div>
</div>
)

if (firstStoryId) {
return (
<img
src={avatarUrl}
alt={`${nickname} 프로필 이미지`}
className={cn(AVATAR_SIZE_CLASSNAME, 'rounded-full object-cover')}
/>
<Link
to="/stories/$profile_name/$story_id"
params={{ profile_name: nickname, story_id: String(firstStoryId) }}
>
{content}
</Link>
)
}

return <DefaultProfileImage className={AVATAR_SIZE_CLASSNAME} />
return content
}
12 changes: 11 additions & 1 deletion src/widgets/profile-header/ui/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -76,7 +80,13 @@ export function ProfileHeader({
<section className={cn(PROFILE_CONTAINER_CLASSNAME, className)}>
<div className="flex gap-8 md:gap-16">
<div className="flex w-[180px] justify-center">
<ProfileAvatar avatarUrl={avatarUrl} nickname={nickname} />
<ProfileAvatar
avatarUrl={avatarUrl}
nickname={nickname}
hasStory={hasStory}
hasUnseenStory={userStoriesData?.hasUnseenStory}
firstStoryId={userStoriesData?.stories[0]?.id}
/>
</div>

<div className="min-w-0 flex-1">
Expand Down