Skip to content

Commit

Permalink
feat: 영화 북마크 기능을 추가한다.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zero-1016 committed Jun 3, 2024
1 parent ca118a1 commit 544ef2c
Show file tree
Hide file tree
Showing 27 changed files with 237 additions and 34 deletions.
3 changes: 3 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const path = require('path')
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
async rewrites() {
return [
{
Expand Down
17 changes: 17 additions & 0 deletions src/entities/local_movie/api/getMovieBookMark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { QueryFunction } from '@tanstack/react-query'

import { Bookmark } from '@/entities/local_movie/model'

export const getMovieBookMark: QueryFunction<Bookmark, [string, string, string]> = async ({ queryKey }) => {
const [_1, _2, movieId] = queryKey
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/bookmark/${movieId}`, {
credentials: 'include',
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
1 change: 1 addition & 0 deletions src/entities/local_movie/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getMovieBookMark } from './getMovieBookMark'
3 changes: 3 additions & 0 deletions src/entities/local_movie/model/bookmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type Bookmark = {
isLike: boolean
}
1 change: 1 addition & 0 deletions src/entities/local_movie/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { Bookmark } from './bookmark'
45 changes: 45 additions & 0 deletions src/entities/local_movie/ui/MovieBookMarkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import StarRoundedIcon from '@mui/icons-material/StarRounded'
import { IconButton } from '@mui/material'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useMemo } from 'react'

import { getMovieBookMark } from '@/entities/local_movie/api'
import { DeleteMovieDisLikeContainer, PostMovieLikeContainer } from '@/features/bookmark/ui'
import { LOCAL_QUERY_KEY } from '@/shared/constants'

type Props = {
movieId: string
size: 'small' | 'medium'
}

export function MovieBookMarkButton({ movieId, size }: Props) {
const { data } = useSuspenseQuery({
queryKey: LOCAL_QUERY_KEY.movieBookMark(movieId),
queryFn: getMovieBookMark,
})

const [width, height, right, top] = useMemo(() => {
switch (size) {
case 'medium':
return [100, 100, 10, 10]
case 'small':
return [40, 40, 0, 20]
}
}, [size])

return (
<IconButton sx={{ position: 'absolute', zIndex: 10, right, top }}>
{data?.isLike ? (
<DeleteMovieDisLikeContainer movieId={movieId}>
<StarRoundedIcon sx={{ color: '#ffff00', width, height }} />
</DeleteMovieDisLikeContainer>
) : (
<PostMovieLikeContainer movieId={movieId}>
<StarRoundedIcon sx={{ color: '#5F6368', width, height }} />
</PostMovieLikeContainer>
)}
</IconButton>
)
}
1 change: 1 addition & 0 deletions src/entities/local_movie/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MovieBookMarkButton } from './MovieBookMarkButton'
16 changes: 16 additions & 0 deletions src/entities/local_movie/ui/movie-book-mark-button.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.button {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}

.smallButton {
width: 40px;
aspect-ratio: 1/1;
}

.mediumButton{
width: 80px;
aspect-ratio: 1/1;
}
17 changes: 7 additions & 10 deletions src/entities/mock/api/handler/movie/bookmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,33 @@ export const bookmarkHandlers = [
const { movieId } = params

if (!movieId) {
return HttpResponse.json(null, { status: 400, statusText: '영화 아이디가 없습니다.' })
return HttpResponse.json(null, { status: 401, statusText: 'Bad Request' })
}

return HttpResponse.json(
{
isLike: faker.datatype.boolean(),
},
{ status: 200, statusText: `${movieId} 데이터를 불러오는 것을 성공하였습니다.` },
)
return HttpResponse.json({
isLike: faker.datatype.boolean(),
})
}),
http.post('/bookmark', () => {
return HttpResponse.json(
{
message: 'success',
},
{ status: 200, statusText: '전송에 성공하였습니다.' },
{ status: 200 },
)
}),
http.delete('/bookmark/:movieId', ({ params }) => {
const { movieId } = params

if (!movieId) {
return HttpResponse.json(null, { status: 400, statusText: '영화 아이디가 없습니다.' })
return HttpResponse.json(null, { status: 401 })
}

return HttpResponse.json(
{
message: 'success',
},
{ status: 200, statusText: `${movieId} 삭제에 성공하였습니다.` },
{ status: 200 },
)
}),
]
12 changes: 10 additions & 2 deletions src/entities/movie/ui/DetailMovieBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import classNames from 'classnames'
import { motion } from 'framer-motion'
import Image from 'next/image'
import { useSession } from 'next-auth/react'
import { Suspense } from 'react'

import { MovieBookMarkButton } from '@/entities/local_movie/ui'
import { getDetail, getImageUrl } from '@/entities/movie/api'
import { IMAGE_SIZE } from '@/shared/constants'
import { MOVIE_QUERY_KEY } from '@/shared/constants/QUERY_KEY'
Expand All @@ -17,9 +20,9 @@ type Props = {
}

export function DetailMovieBanner({ movieId }: Readonly<Props>) {
const queryKey = MOVIE_QUERY_KEY.detail(movieId) as [string, string, string]
const session = useSession()
const { data: result } = useSuspenseQuery({
queryKey: queryKey,
queryKey: MOVIE_QUERY_KEY.detail(movieId),
queryFn: getDetail,
})

Expand All @@ -32,6 +35,11 @@ export function DetailMovieBanner({ movieId }: Readonly<Props>) {
return (
<section className={styles.container}>
<div className={styles.imageContainer}>
{session.data && (
<Suspense fallback={null}>
<MovieBookMarkButton movieId={movieId} size="medium" />
</Suspense>
)}
<div className={styles.blueBlur} />
<Image src={imagePath} alt={title + '포스터'} fill={true} style={{ objectFit: 'cover' }} />
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/entities/movie/ui/DetailMovieImageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ type Props = {
}

export function DetailMovieImageContainer({ movieId }: Readonly<Props>) {
const queryKey = MOVIE_QUERY_KEY.images(movieId) as [string, string, string]
const { data } = useSuspenseQuery({
queryKey: queryKey,
queryKey: MOVIE_QUERY_KEY.images(movieId),
queryFn: getImages,
})

Expand Down
3 changes: 1 addition & 2 deletions src/entities/movie/ui/DetailMovieIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ type Props = {
}

export function DetailMovieIntro({ movieId }: Readonly<Props>) {
const queryKey = MOVIE_QUERY_KEY.detail(movieId) as [string, string, string]
const { data: result } = useSuspenseQuery({
queryKey,
queryKey: MOVIE_QUERY_KEY.detail(movieId),
queryFn: getDetail,
})

Expand Down
5 changes: 2 additions & 3 deletions src/entities/movie/ui/MainMovieBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import styles from './main-movie-banner.module.scss'

export function MainMovieBanner() {
const [movieIndex, setMovieIndex] = useState(0)
const queryKey = MOVIE_QUERY_KEY.nowPlay('1') as [string, string, string]
const { data: playListData } = useSuspenseQuery<MoviesResponse>({
queryKey: queryKey,
queryFn: () => getNowPlay({ pageParam: 1, queryKey }),
queryKey: MOVIE_QUERY_KEY.nowPlay('1'),
queryFn: () => getNowPlay({ pageParam: 1, queryKey: MOVIE_QUERY_KEY.nowPlay('1') }),
})

const movieList = useSuspenseQueries({
Expand Down
5 changes: 3 additions & 2 deletions src/entities/movie/ui/MovieDetailContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import Image from 'next/image'
import { useRouter } from 'next/navigation'

import { MovieBookMarkButton } from '@/entities/local_movie/ui'
import { getDetail, getImageUrl } from '@/entities/movie/api'
import { IMAGE_SIZE, SITE_PATH } from '@/shared/constants'
import { MOVIE_QUERY_KEY } from '@/shared/constants/QUERY_KEY'
Expand All @@ -18,10 +19,9 @@ type Props = {

export function MovieDetailContent({ movieId }: Readonly<Props>) {
const router = useRouter()
const queryKey = MOVIE_QUERY_KEY.detail(movieId) as [string, string, string]

const { data: result } = useSuspenseQuery({
queryKey,
queryKey: MOVIE_QUERY_KEY.detail(movieId),
queryFn: getDetail,
})

Expand All @@ -40,6 +40,7 @@ export function MovieDetailContent({ movieId }: Readonly<Props>) {
return (
<div className={styles.container}>
<div className={styles.leftSection}>
<MovieBookMarkButton movieId={movieId} size="small" />
<Image
onClick={showImage}
placeholder={'blur'}
Expand Down
2 changes: 2 additions & 0 deletions src/features/bookmark/hook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { UseDeleteMovieBookMark } from './useDeleteMovieBookMark'
export { UsePostMovieBookMark } from './usePostMovieBookMark'
23 changes: 23 additions & 0 deletions src/features/bookmark/hook/useDeleteMovieBookMark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import { deleteMovieBookMark } from '@/features/bookmark/lib'
import { LOCAL_QUERY_KEY } from '@/shared/constants'

export function UseDeleteMovieBookMark() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (movieId: string) => deleteMovieBookMark(movieId),
onMutate: movieId => {
queryClient.setQueryData(LOCAL_QUERY_KEY.movieBookMark(movieId), (prevData: { isLike: boolean }) => {
return { isLike: !prevData.isLike }
})
return movieId
},
onError: async (_, movieId) => {
await queryClient.invalidateQueries({
queryKey: LOCAL_QUERY_KEY.movieBookMark(movieId),
})
},
})
}
22 changes: 22 additions & 0 deletions src/features/bookmark/hook/usePostMovieBookMark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import { postMovieBookMark } from '@/features/bookmark/lib'
import { LOCAL_QUERY_KEY } from '@/shared/constants'

export function UsePostMovieBookMark(movieId: string) {
const queryClient = useQueryClient()

return useMutation({
mutationFn: () => postMovieBookMark(movieId),
onMutate: () => {
queryClient.setQueryData(LOCAL_QUERY_KEY.movieBookMark(movieId), (prevData: { isLike: boolean }) => {
return { isLike: !prevData.isLike }
})
},
onError: () => {
queryClient.invalidateQueries({

Check notice on line 17 in src/features/bookmark/hook/usePostMovieBookMark.ts

View workflow job for this annotation

GitHub Actions / qodana

Result of method call returning a promise is ignored

Promise returned from invalidateQueries is ignored
queryKey: LOCAL_QUERY_KEY.movieBookMark(movieId),
})
},
})
}
13 changes: 13 additions & 0 deletions src/features/bookmark/lib/deleteMovieBookMark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const deleteMovieBookMark = async (movieId: string) => {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/bookmark/${movieId}`, {
method: 'DELETE',
credentials: 'include',
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
2 changes: 2 additions & 0 deletions src/features/bookmark/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { deleteMovieBookMark } from './deleteMovieBookMark'
export { postMovieBookMark } from './postMovieBookMark'
14 changes: 14 additions & 0 deletions src/features/bookmark/lib/postMovieBookMark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const postMovieBookMark = async (movieId: string) => {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/bookmark`, {
method: 'POST',
credentials: 'include',
cache: 'no-store',
body: JSON.parse(movieId),
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
19 changes: 19 additions & 0 deletions src/features/bookmark/ui/deleteMovieDisLikeContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'

import { PropsWithChildren } from 'react'

import { UseDeleteMovieBookMark } from '@/features/bookmark/hook'

type Props = {
movieId: string
}

export function DeleteMovieDisLikeContainer({ movieId, children }: PropsWithChildren<Props>) {
const { mutateAsync } = UseDeleteMovieBookMark()

const onClick = async () => {
await mutateAsync(movieId)
}

return <div onClick={onClick}>{children}</div>
}
2 changes: 2 additions & 0 deletions src/features/bookmark/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DeleteMovieDisLikeContainer } from './deleteMovieDisLikeContainer'
export { PostMovieLikeContainer } from './postMovieLikeContainer'
18 changes: 18 additions & 0 deletions src/features/bookmark/ui/postMovieLikeContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import { PropsWithChildren } from 'react'

import { UsePostMovieBookMark } from '@/features/bookmark/hook'

type Props = {
movieId: string
}

export function PostMovieLikeContainer({ movieId, children }: PropsWithChildren<Props>) {
const { mutateAsync } = UsePostMovieBookMark(movieId)

const onClick = async () => {
await mutateAsync()
}
return <div onClick={onClick}>{children}</div>
}
11 changes: 10 additions & 1 deletion src/shared/constants/QUERY_KEY.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type QUERY_KEY = 'search' | 'nowPlay' | 'upComing' | 'popular' | 'topRated' | 'detail' | 'images' | 'video'

// eslint-disable-next-line unused-imports/no-unused-vars
type QueryFunction = (key: string) => string[]
type QueryFunction = (key: string) => [string, string, string]

export const MOVIE_QUERY_KEY: Record<QUERY_KEY, QueryFunction> = {
search: key => ['movies', 'search', key],
Expand All @@ -13,3 +13,12 @@ export const MOVIE_QUERY_KEY: Record<QUERY_KEY, QueryFunction> = {
images: key => ['movie', 'images', key],
video: key => ['movies', 'videos', key],
} as const

type LOCAL_QUERY_KEY = 'movieBookMark'

// eslint-disable-next-line unused-imports/no-unused-vars
type LocalQueryFunction = (key: string) => [string, string, string]

export const LOCAL_QUERY_KEY: Record<LOCAL_QUERY_KEY, LocalQueryFunction> = {
movieBookMark: key => ['movie', 'bookmark', key],
} as const
2 changes: 1 addition & 1 deletion src/shared/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { GENRES } from './GENRES'
export { IMAGE_SIZE } from './IMAGE_SIZE'
export { LANDING_TITLE } from './LANDING_TITLE'
export { MOVIE_QUERY_KEY } from './QUERY_KEY'
export { LOCAL_QUERY_KEY, MOVIE_QUERY_KEY } from './QUERY_KEY'
export { SITE_PATH } from './SITE_PATH'
Loading

0 comments on commit 544ef2c

Please sign in to comment.