Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 0 additions & 3 deletions src/assets/icons/icn_cta_arrow.svg

This file was deleted.

4 changes: 2 additions & 2 deletions src/assets/icons/icn_right_arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,4 @@ export { default as BgCd } from './icn_bg_cd.svg?react'
export { default as BgHeadphone } from './icn_bg_headphone.svg?react'
export { default as BgMusic } from './icn_bg_music.svg?react'
export { default as Volume } from './icn_volume.svg?react'
export { default as CtaArrow } from './icn_cta_arrow.svg?react'
export { default as LikeStroke } from './icn_like_stroke.svg?react'
Binary file modified src/assets/images/img_character_home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/features/chat/api/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { ChatHistoryParams, ChatHistoryResponse } from '@/features/chat/types/chat'
import type {
ChatCountResponse,
ChatHistoryParams,
ChatHistoryResponse,
} from '@/features/chat/types/chat'
import { api } from '@/shared/api/httpClient'

export const getChatHistory = (roomId: string, params: ChatHistoryParams = {}) => {
Expand All @@ -10,3 +14,7 @@ export const getChatHistory = (roomId: string, params: ChatHistoryParams = {}) =
export const deleteChatMessage = async (roomId: string, messageId: string) => {
return api.delete(`/chat/rooms/${roomId}/messages/${messageId}`)
}

export const getChatCount = (roomId: string) => {
return api.get<ChatCountResponse>(`/chat/rooms/${roomId}/count/chat`)
}
11 changes: 10 additions & 1 deletion src/features/chat/model/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
type InfiniteData,
} from '@tanstack/react-query'

import { deleteChatMessage, getChatHistory } from '@/features/chat/api/chat'
import { deleteChatMessage, getChatCount, getChatHistory } from '@/features/chat/api/chat'
import type { ChatHistoryResponse } from '@/features/chat/types/chat'

export const useInfiniteChatHistory = (roomId: string, limit = 50) => {
Expand Down Expand Up @@ -36,3 +37,11 @@ export const useDeleteChatMessage = (roomId: string, removeMessage: (id: string)
},
})
}

export const useChatCount = (roomId: string) => {
return useQuery({
queryKey: ['chatCount', roomId],
queryFn: () => getChatCount(roomId),
staleTime: 0,
})
}
4 changes: 4 additions & 0 deletions src/features/chat/types/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export interface ChatHistoryResponse {
messages: ChatHistoryMessage[]
nextCursor?: string
}

export interface ChatCountResponse {
totalCount: number
}
3 changes: 2 additions & 1 deletion src/features/like/api/like.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LikeStatusResponse } from '@/features/like/type/like'
import { api } from '@/shared/api/httpClient'

export const postLike = (playlistId: number) => {
Expand All @@ -9,5 +10,5 @@ export const deleteLike = (playlistId: number) => {
}

export const getLikeStatus = (playlistId: number) => {
return api.get(`/main/likes/${playlistId}`)
return api.get<LikeStatusResponse>(`/main/likes/${playlistId}`)
}
94 changes: 65 additions & 29 deletions src/features/like/model/useLike.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,80 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

import { useAuthStore } from '@/features/auth/store/authStore'
import { postLike, deleteLike, getLikeStatus } from '@/features/like/api/like'
import type { LikeStatusResponse } from '@/features/like/type/like'

const useLike = (playlistId: number, initialIsLiked: boolean) => {
export const useLikeStatus = (playlistId: number, options?: { enabled?: boolean }) => {
return useQuery({
queryKey: ['likeStatus', playlistId],
queryFn: () => getLikeStatus(playlistId),
staleTime: 0,
enabled: playlistId !== undefined && (options?.enabled ?? true),
})
}

const useLike = (playlistId: number) => {
const queryClient = useQueryClient()
const [isLiked, setIsLiked] = useState(initialIsLiked)
const navigate = useNavigate()
const { isLogin } = useAuthStore()
const navigate = useNavigate()

const { data: statusData, isLoading } = useLikeStatus(playlistId, { enabled: isLogin })
const isLiked = statusData?.isLiked ?? false

const likeMutation = useMutation({
mutationFn: (playlistId: number) => postLike(playlistId),
onSuccess: () => {
setIsLiked(true)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
mutationFn: () => postLike(playlistId),

onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['likeStatus', playlistId] })
const previous = queryClient.getQueryData(['likeStatus', playlistId])

// 낙관적 업데이트
queryClient.setQueryData<LikeStatusResponse>(['likeStatus', playlistId], (old) => ({
...(old ?? { isLiked: false }),
isLiked: true,
}))

return { previous }
},
onError: (_err, _vars, context) => {
// context는 onMutate의 return 값(previous)
if (context?.previous) {
// 실패 시 UI를 원래대로 돌림
queryClient.setQueryData(['likeStatus', playlistId], context.previous)
}
},

// 성공 실패 관계 없이 무조건 실행
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['likeStatus', playlistId] })
},
})

const unlikeMutation = useMutation({
mutationFn: (playlistId: number) => deleteLike(playlistId),
onSuccess: () => {
setIsLiked(false)
queryClient.invalidateQueries({ queryKey: ['playlistDetail', playlistId] })
mutationFn: () => deleteLike(playlistId),

onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['likeStatus', playlistId] })
const previous = queryClient.getQueryData(['likeStatus', playlistId])

queryClient.setQueryData<LikeStatusResponse>(['likeStatus', playlistId], (old) => ({
...(old ?? { isLiked: false }),
isLiked: true,
}))

return { previous }
},

onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(['likeStatus', playlistId], context.previous)
}
},

onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['likeStatus', playlistId] })
},
})

Expand All @@ -34,25 +84,11 @@ const useLike = (playlistId: number, initialIsLiked: boolean) => {
return
}

if (likeMutation.isPending || unlikeMutation.isPending) return

if (isLiked) {
unlikeMutation.mutate(playlistId)
} else {
likeMutation.mutate(playlistId)
}
if (isLiked) unlikeMutation.mutate()
else likeMutation.mutate()
}

return { liked: isLiked, toggleLike, likeMutation, unlikeMutation }
return { liked: isLiked, toggleLike, isLoading }
}

export default useLike

export const useLikeStatus = (playlistId: number, options?: { enabled?: boolean }) => {
return useQuery({
queryKey: ['likeStatus', playlistId],
queryFn: () => getLikeStatus(playlistId),
staleTime: 0,
enabled: playlistId !== undefined && (options?.enabled ?? true),
})
}
3 changes: 3 additions & 0 deletions src/features/like/type/like.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface LikeStatusResponse {
isLiked: boolean
}
8 changes: 3 additions & 5 deletions src/features/like/ui/LikeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import styled, { useTheme } from 'styled-components'

import { Like, LikeStroke } from '@/assets/icons'
import { useLike } from '@/features/like'
import { flexRowCenter, myCdButton } from '@/shared/styles/mixins'
import { myCdButton } from '@/shared/styles/mixins'
import SvgButton from '@/shared/ui/SvgButton'

interface LikeButtonProps {
playlistId: number
isLiked: boolean
type?: 'HOME' | 'DISCOVER' | 'MY'
}

Expand All @@ -19,9 +18,9 @@ const ICON_STYLE = {
MY: { size: 16, Icon: LikeStroke },
} as const

const LikeButton = ({ playlistId, isLiked, type = 'HOME' }: LikeButtonProps) => {
const LikeButton = ({ playlistId, type = 'HOME' }: LikeButtonProps) => {
const theme = useTheme()
const { liked, toggleLike } = useLike(playlistId, isLiked)
const { liked, toggleLike } = useLike(playlistId)

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
Expand Down Expand Up @@ -58,6 +57,5 @@ export default LikeButton

const Wrapper = styled.div<{ $opacity?: number; $isMy: boolean }>`
opacity: ${({ $opacity }) => $opacity};
${flexRowCenter};
${({ $isMy }) => $isMy && myCdButton};
`
1 change: 0 additions & 1 deletion src/features/share/ui/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,5 @@ const ButtonBar = styled.div`
`

const ButtonWrapper = styled.div<{ $isMy: boolean }>`
${flexRowCenter};
${({ $isMy }) => $isMy && myCdButton};
`
10 changes: 6 additions & 4 deletions src/pages/home/config/messages.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export const BUTTON_TEXT = {
MEMBER: 'CD 커버에 내 취향을 담아요',
GUEST: '로그인으로 내 CD를 꾸며요',
MEMBER: '새로운 CD에 취향 담기',
MEMBER_NO_CD: '나의 첫 CD 만들기',
GUEST: '로그인하고, CD 만들기',
} as const

export const TITLE_TEXT = {
MEMBER: (name: string) => `${name}님!\n 오늘의 첫 곡, 여기서 시작하세요`,
GUEST: `오늘의 무드에 어울리는 \n 플레이리스트를 들어 보세요`,
MEMBER: (name: string) => `${name}님! 오늘의 첫 곡,\n 여기서 시작하세요`,
MEMBER_NO_CD: `아직 나만의 CD가 없어요\n오늘의 첫 곡을 담아볼까요?`,
GUEST: `오늘의 무드에 어울리는\n 트랙리스트를 들어 보세요`,
} as const
82 changes: 14 additions & 68 deletions src/pages/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@ import { createSearchParams, useNavigate } from 'react-router-dom'
import styled, { css } from 'styled-components'

import { CARD_IMAGES_LARGE } from '@/assets/card'
import { CtaArrow, Logo, Notification, Search } from '@/assets/icons'
import { HomeCharacter } from '@/assets/images'
import { useAuthStore } from '@/features/auth/store/authStore'
import { Logo, Notification, Search } from '@/assets/icons'
import {
useRecommendationsByRecent,
useRecommendationsByFollow,
useRecommendedGenres,
} from '@/features/recommend'
import { TITLE_TEXT } from '@/pages/home/config/messages'
import { flexRowCenter } from '@/shared/styles/mixins'
import { FirstSection } from '@/pages/home/ui'
import { Header, SvgButton, ScrollCarousel } from '@/shared/ui'
import { Playlist, PlaylistWithSong } from '@/widgets/playlist'

const HomePage = () => {
const navigate = useNavigate()
const { isLogin, userInfo } = useAuthStore()

const handleNotiClick = () => navigate('/mypage/notification')
const handleSearchClick = () => navigate('/search')
Expand All @@ -36,30 +32,20 @@ const HomePage = () => {
})}`,
})
}

return (
<PageLayout>
<TopSection>
<Header
left={<Logo />}
right={
<>
<SvgButton icon={Notification} onClick={handleNotiClick} />
<SvgButton icon={Search} onClick={handleSearchClick} />
</>
}
/>

<FirstSection>
<h1>{isLogin ? TITLE_TEXT.MEMBER(userInfo.username) : TITLE_TEXT.GUEST}</h1>

<CtaButton onClick={() => (isLogin ? navigate('/mypage/customize') : navigate('/login'))}>
CD 커버에 내 취향을 담아요 <CtaArrow />
</CtaButton>

<CharacterBg src={HomeCharacter} />
</FirstSection>
</TopSection>
<Header
left={<Logo />}
right={
<>
<SvgButton icon={Notification} onClick={handleNotiClick} />
<SvgButton icon={Search} onClick={handleSearchClick} />
</>
}
/>

<FirstSection />

<SecondSection>
<h1>퇴근길, 귀에 붙는 노래</h1>
<ScrollCarousel gap={14}>
Expand All @@ -70,7 +56,6 @@ const HomePage = () => {
title={item.playlistName}
username={item.creatorNickname}
stickers={item.onlyCdResponse?.cdItems}
isLiked={false} // TODO: 실제 값으로 수정 필요
/>
))}
</ScrollCarousel>
Expand Down Expand Up @@ -133,25 +118,6 @@ const PageLayout = styled.div`
flex-direction: column;
`

const TopSection = styled.section`
margin: 0 -20px;
padding: 0 20px;
background-color: #282c36;
`

const FirstSection = styled.section`
h1 {
${({ theme }) => theme.FONT.heading2};
color: ${({ theme }) => theme.COLOR['gray-50']};
font-weight: 500;
}

padding: 34px 0;
display: flex;
flex-direction: column;
gap: 60px;
`

const sectionCommonLayout = css`
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -181,23 +147,3 @@ const FourthSection = styled.section`
padding: 16px 20px 146px 20px;
margin-bottom: -98px;
`

const CtaButton = styled.button`
${flexRowCenter}
padding: 6px 18px;
gap: 14px;
width: 212px;
height: 36px;
background-color: ${({ theme }) => theme.COLOR['primary-normal']};
color: ${({ theme }) => theme.COLOR['gray-900']};
border-radius: 86px;
${({ theme }) => theme.FONT.caption1};
`

const CharacterBg = styled.img`
position: absolute;
right: -48px;
width: 285px;
object-fit: contain;
object-position: center;
`
Loading
Loading