diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 3c17bad..803daa0 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,10 +1,11 @@ import "@/shared/styles/normalize.css"; import "@/shared/styles/globals.css"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useState } from "react"; +import { queryClient as queryClientInstance } from "@/shared/api"; import { lightThemeClass } from "@/shared/styles"; import type { AppProps } from "next/app"; @@ -13,12 +14,12 @@ import "swiper/css"; export default function App({ Component, pageProps }: AppProps) { // eslint-disable-next-line react/hook-use-state - const [queryClient] = useState(() => new QueryClient()); + const [queryClient] = useState(() => queryClientInstance); return ( -
+
diff --git a/apps/web/pages/index.tsx b/apps/web/pages/index.tsx index 87c2362..60d2b8f 100644 --- a/apps/web/pages/index.tsx +++ b/apps/web/pages/index.tsx @@ -1,5 +1,10 @@ +import { dehydrate, DehydratedState, HydrationBoundary, QueryClient } from "@tanstack/react-query"; +import { GetStaticProps, InferGetStaticPropsType } from "next"; import Head from "next/head"; +import { genresQueryKeys } from "@/entities/genres"; +import GenresApi from "@/entities/genres/api/genres-api"; +import { NowPlayingMovieList } from "@/features/get-now-playing-movie-list"; import { PopularMovieList } from "@/features/get-popular-movie-list"; import { UpcomingMovieList } from "@/features/get-upcoming-movie-list"; import { popularMovieListTitle } from "@/pages/home"; @@ -8,9 +13,9 @@ import { Container } from "@/shared/ui/container"; import { Row } from "@/shared/ui/row"; import { Text } from "@/shared/ui/text"; -export default function Home() { +export default function Home({ dehydratedState }: InferGetStaticPropsType) { return ( - <> + Create Next App @@ -19,6 +24,15 @@ export default function Home() { + + + + 현재 상영작 + + + + + @@ -37,6 +51,21 @@ export default function Home() { - + ); } + +export const getStaticProps = (async () => { + const queryClient = new QueryClient(); + await queryClient.prefetchQuery({ + queryKey: [...genresQueryKeys.movieGenres({ language: "ko-KR" })], + queryFn: () => GenresApi.getMovieGenres(), + }); + + return { + props: { + dehydratedState: dehydrate(queryClient), + }, + revalidate: 10 * 1000 * 60 * 60 * 24, + }; +}) satisfies GetStaticProps<{ dehydratedState: DehydratedState }>; diff --git a/apps/web/src/entities/genres/api/genres-api.ts b/apps/web/src/entities/genres/api/genres-api.ts new file mode 100644 index 0000000..fb363f3 --- /dev/null +++ b/apps/web/src/entities/genres/api/genres-api.ts @@ -0,0 +1,20 @@ +import { tmdbHttp } from "@/shared/api"; +import { camelCaseObjMapper } from "@/shared/lib"; + +import { GenresReqParams } from "./request-types"; +import { GenresDTO } from "./response-types"; + +const genresBaseURL = "genre"; + +export default class GenresApi { + /** + * 영화 장르 목록 + */ + static async getMovieGenres( + { language, ...axiosConfig }: GenresReqParams = { language: "ko-KR" }, + ): Promise { + return tmdbHttp + .get(`${genresBaseURL}/movie/list`, { params: { language }, ...axiosConfig }) + .then((res) => camelCaseObjMapper(res.data)); + } +} diff --git a/apps/web/src/entities/genres/api/index.ts b/apps/web/src/entities/genres/api/index.ts new file mode 100644 index 0000000..fbbb484 --- /dev/null +++ b/apps/web/src/entities/genres/api/index.ts @@ -0,0 +1,4 @@ +export * from "./genres-api"; +export * from "./request-types"; +export * from "./response-types"; +export * from "./queries"; diff --git a/apps/web/src/entities/genres/api/queries/index.ts b/apps/web/src/entities/genres/api/queries/index.ts new file mode 100644 index 0000000..eec51ec --- /dev/null +++ b/apps/web/src/entities/genres/api/queries/index.ts @@ -0,0 +1,2 @@ +export * from "./query-keys"; +export * from "./use-movie-genres-query"; diff --git a/apps/web/src/entities/genres/api/queries/query-keys.ts b/apps/web/src/entities/genres/api/queries/query-keys.ts new file mode 100644 index 0000000..54fc22a --- /dev/null +++ b/apps/web/src/entities/genres/api/queries/query-keys.ts @@ -0,0 +1,5 @@ +import { GenresReqParams } from "../request-types"; + +export const genresQueryKeys = { + movieGenres: ({ language }: GenresReqParams) => ["movie-genres", { language }] as const, +}; diff --git a/apps/web/src/entities/genres/api/queries/use-movie-genres-query.ts b/apps/web/src/entities/genres/api/queries/use-movie-genres-query.ts new file mode 100644 index 0000000..025081d --- /dev/null +++ b/apps/web/src/entities/genres/api/queries/use-movie-genres-query.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; + +import { Genres } from "../../models"; +import GenresApi from "../genres-api"; +import { GenresReqParams } from "../request-types"; + +import { genresQueryKeys } from "./query-keys"; + +export const useMovieGenresQuery = ({ language }: GenresReqParams = { language: "ko-KR" }) => { + return useQuery({ + queryKey: [...genresQueryKeys.movieGenres({ language })], + queryFn: () => GenresApi.getMovieGenres({ language }), + select: (data) => new Genres(data), + staleTime: 10 * 1000 * 60, + }); +}; diff --git a/apps/web/src/entities/genres/api/request-types/genres.ts b/apps/web/src/entities/genres/api/request-types/genres.ts new file mode 100644 index 0000000..dcd156c --- /dev/null +++ b/apps/web/src/entities/genres/api/request-types/genres.ts @@ -0,0 +1,5 @@ +import { BaseAxiosReq } from "@/shared/api"; + +export interface GenresReqParams extends BaseAxiosReq { + language: string; +} diff --git a/apps/web/src/entities/genres/api/request-types/index.ts b/apps/web/src/entities/genres/api/request-types/index.ts new file mode 100644 index 0000000..e3ced9b --- /dev/null +++ b/apps/web/src/entities/genres/api/request-types/index.ts @@ -0,0 +1 @@ +export * from "./genres"; diff --git a/apps/web/src/entities/genres/api/response-types/genres.ts b/apps/web/src/entities/genres/api/response-types/genres.ts new file mode 100644 index 0000000..14f5269 --- /dev/null +++ b/apps/web/src/entities/genres/api/response-types/genres.ts @@ -0,0 +1,3 @@ +export interface GenresDTO { + genres: { id: number; name: string }[]; +} diff --git a/apps/web/src/entities/genres/api/response-types/index.ts b/apps/web/src/entities/genres/api/response-types/index.ts new file mode 100644 index 0000000..e3ced9b --- /dev/null +++ b/apps/web/src/entities/genres/api/response-types/index.ts @@ -0,0 +1 @@ +export * from "./genres"; diff --git a/apps/web/src/entities/genres/index.ts b/apps/web/src/entities/genres/index.ts new file mode 100644 index 0000000..0475d35 --- /dev/null +++ b/apps/web/src/entities/genres/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./models"; diff --git a/apps/web/src/entities/genres/models/genres.ts b/apps/web/src/entities/genres/models/genres.ts new file mode 100644 index 0000000..ba74a14 --- /dev/null +++ b/apps/web/src/entities/genres/models/genres.ts @@ -0,0 +1,15 @@ +import { createBaseModel } from "@/shared/lib"; + +import { GenresDTO } from "../api"; + +export class Genres extends createBaseModel() { + constructor(private genresList: GenresDTO) { + super(genresList); + } + + get GenresList() { + return this.genresList.genres.reduce((prev, { id, name }) => { + return prev.set(id, { id, name }); + }, new Map()); + } +} diff --git a/apps/web/src/entities/genres/models/index.ts b/apps/web/src/entities/genres/models/index.ts new file mode 100644 index 0000000..e3ced9b --- /dev/null +++ b/apps/web/src/entities/genres/models/index.ts @@ -0,0 +1 @@ +export * from "./genres"; diff --git a/apps/web/src/entities/movie-list/api/index.ts b/apps/web/src/entities/movie-list/api/index.ts index 08710a8..60d79ea 100644 --- a/apps/web/src/entities/movie-list/api/index.ts +++ b/apps/web/src/entities/movie-list/api/index.ts @@ -2,3 +2,4 @@ export * from "./movie-list-api"; export * from "./request-types"; export * from "./response-types"; export * from "./movie-list.queries"; +export * from "./queries"; diff --git a/apps/web/src/entities/movie-list/api/movie-list-api.ts b/apps/web/src/entities/movie-list/api/movie-list-api.ts index c08f347..558a5f5 100644 --- a/apps/web/src/entities/movie-list/api/movie-list-api.ts +++ b/apps/web/src/entities/movie-list/api/movie-list-api.ts @@ -2,6 +2,7 @@ import { tmdbHttp } from "@/shared/api"; import { camelCaseObjMapper } from "@/shared/lib"; import { MovieListReqParams } from "./request-types"; +import { NowPlayingMovieListDTO } from "./response-types/now-playing-movie-list"; import { PopularMovieListDTO } from "./response-types/popular-movie-list"; import { UpcomingMovieListDTO } from "./response-types/upcoming-movie-list"; @@ -39,4 +40,15 @@ export default class MovieListApi { .get(`${movieBaseURL}/upcoming`, { params: { page, language, region }, ...axiosConfig }) .then((res) => camelCaseObjMapper(res.data)); } + + /** + * 현재 상영중인 영화 목록 + */ + static async getNowPlayingMovieList( + { page, language, region, ...axiosConfig }: MovieListReqParams = { page: 1, language: "ko-KR", region: "KR" }, + ): Promise { + return tmdbHttp + .get(`${movieBaseURL}/now_playing`, { params: { page, language, region }, ...axiosConfig }) + .then((res) => camelCaseObjMapper(res.data)); + } } diff --git a/apps/web/src/entities/movie-list/api/movie-list.queries.ts b/apps/web/src/entities/movie-list/api/movie-list.queries.ts index 77eee9c..2bb5050 100644 --- a/apps/web/src/entities/movie-list/api/movie-list.queries.ts +++ b/apps/web/src/entities/movie-list/api/movie-list.queries.ts @@ -9,6 +9,7 @@ import { MovieListReqParams } from "./request-types"; export const movieListQueryKeys = { popularMovieList: () => ["popular-movie-list"] as const, upcomingMovieList: () => ["upcoming-movie-list"] as const, + nowPlayingMovieList: () => ["now-playing-movie-list"] as const, }; export const movieListQueries = { diff --git a/apps/web/src/entities/movie-list/api/queries/index.ts b/apps/web/src/entities/movie-list/api/queries/index.ts new file mode 100644 index 0000000..5f79f39 --- /dev/null +++ b/apps/web/src/entities/movie-list/api/queries/index.ts @@ -0,0 +1,2 @@ +export * from "./use-now-playing-movie-list-query"; +export * from "./query-keys"; diff --git a/apps/web/src/entities/movie-list/api/queries/query-keys.ts b/apps/web/src/entities/movie-list/api/queries/query-keys.ts new file mode 100644 index 0000000..37259a6 --- /dev/null +++ b/apps/web/src/entities/movie-list/api/queries/query-keys.ts @@ -0,0 +1,6 @@ +import { MovieListReqParams } from "../request-types"; + +export const movieListQueryKeys = { + nowPlayingMovieList: ({ page, language, region }: MovieListReqParams) => + ["now-playing-movie-list", { page, language, region }] as const, +}; diff --git a/apps/web/src/entities/movie-list/api/queries/use-now-playing-movie-list-query.ts b/apps/web/src/entities/movie-list/api/queries/use-now-playing-movie-list-query.ts new file mode 100644 index 0000000..46fb09f --- /dev/null +++ b/apps/web/src/entities/movie-list/api/queries/use-now-playing-movie-list-query.ts @@ -0,0 +1,21 @@ +import { useQuery } from "@tanstack/react-query"; + +import { useMovieGenresQuery } from "@/entities/genres"; + +import { NowPlayingMovieList } from "../../model/now-playing-movie-list"; +import MovieListApi from "../movie-list-api"; +import { MovieListReqParams } from "../request-types"; + +import { movieListQueryKeys } from "./query-keys"; + +export const useNowPlayingMovieListQuery = ( + { page, language, region }: MovieListReqParams = { page: 1, language: "ko-KR", region: "KR" }, +) => { + const { data: movieGenres } = useMovieGenresQuery(); + + return useQuery({ + queryKey: [...movieListQueryKeys.nowPlayingMovieList({ page, language, region })], + queryFn: () => MovieListApi.getNowPlayingMovieList({ page, language, region }), + select: (data) => new NowPlayingMovieList(data, movieGenres), + }); +}; diff --git a/apps/web/src/entities/movie-list/api/response-types/movie-list.ts b/apps/web/src/entities/movie-list/api/response-types/movie-list.ts index 5695778..f695201 100644 --- a/apps/web/src/entities/movie-list/api/response-types/movie-list.ts +++ b/apps/web/src/entities/movie-list/api/response-types/movie-list.ts @@ -3,4 +3,6 @@ import { MovieListItemDTO } from "./movie-list-item"; export interface MovieListDTO { page: number; results: MovieListItemDTO[]; + total_pages: number; + total_results: number; } diff --git a/apps/web/src/entities/movie-list/api/response-types/now-playing-movie-list.ts b/apps/web/src/entities/movie-list/api/response-types/now-playing-movie-list.ts new file mode 100644 index 0000000..0718dea --- /dev/null +++ b/apps/web/src/entities/movie-list/api/response-types/now-playing-movie-list.ts @@ -0,0 +1,8 @@ +import { MovieListDTO } from "./movie-list"; + +export interface NowPlayingMovieListDTO extends MovieListDTO { + dates: { + maximum: string; + minimum: string; + }; +} diff --git a/apps/web/src/entities/movie-list/model/movie-list-item.ts b/apps/web/src/entities/movie-list/model/movie-list-item.ts index 653ebb5..df7a24c 100644 --- a/apps/web/src/entities/movie-list/model/movie-list-item.ts +++ b/apps/web/src/entities/movie-list/model/movie-list-item.ts @@ -1,12 +1,16 @@ import dayjs from "dayjs"; +import { Genres } from "@/entities/genres"; import { languageRegions } from "@/shared/config"; import { createBaseModel } from "@/shared/lib"; import { MovieListItemDTO } from "../api/response-types/movie-list-item"; export class MovieListItem extends createBaseModel() { - constructor(private movieListItem: MovieListItemDTO) { + constructor( + private movieListItem: MovieListItemDTO, + private genresModel?: Genres, + ) { super(movieListItem); } @@ -33,4 +37,8 @@ export class MovieListItem extends createBaseModel() { get ReleasedDateDotParsed() { return dayjs(this.ReleasedDate).format("YYYY.MM.DD"); } + + get Genres() { + return this.genresModel ? this.genreIds.map((id) => this.genresModel!.GenresList.get(id)!.name)[0]! : ""; + } } diff --git a/apps/web/src/entities/movie-list/model/movie-list.ts b/apps/web/src/entities/movie-list/model/movie-list.ts index e48005f..e0744d1 100644 --- a/apps/web/src/entities/movie-list/model/movie-list.ts +++ b/apps/web/src/entities/movie-list/model/movie-list.ts @@ -1,3 +1,4 @@ +import { Genres } from "@/entities/genres"; import { createBaseModel } from "@/shared/lib"; import { MovieListDTO } from "../api/response-types/movie-list"; @@ -5,11 +6,16 @@ import { MovieListDTO } from "../api/response-types/movie-list"; import { MovieListItem } from "./movie-list-item"; export abstract class MovieList extends createBaseModel() { - constructor(private movieList: MovieListDTO) { + constructor( + private movieList: MovieListDTO, + private genresModel?: Genres, + ) { super(movieList); } get GeneralAudienceMovies() { - return this.movieList.results.filter(({ adult }) => !adult).map((item) => new MovieListItem(item)); + return this.movieList.results + .filter(({ adult }) => !adult) + .map((item) => new MovieListItem(item, this.genresModel)); } } diff --git a/apps/web/src/entities/movie-list/model/now-playing-movie-list.ts b/apps/web/src/entities/movie-list/model/now-playing-movie-list.ts new file mode 100644 index 0000000..6812631 --- /dev/null +++ b/apps/web/src/entities/movie-list/model/now-playing-movie-list.ts @@ -0,0 +1,3 @@ +import { MovieList } from "./movie-list"; + +export class NowPlayingMovieList extends MovieList {} diff --git a/apps/web/src/entities/movie-list/ui/index.ts b/apps/web/src/entities/movie-list/ui/index.ts index 861bb23..b4e2a8c 100644 --- a/apps/web/src/entities/movie-list/ui/index.ts +++ b/apps/web/src/entities/movie-list/ui/index.ts @@ -1,2 +1,3 @@ export * from "./popular-movie-card/popular-movie-card"; export * from "./upcoming-movie-card/upcoming-movie-card"; +export * from "./now-playing-movie-card/now-playing-movie-card"; diff --git a/apps/web/src/entities/movie-list/ui/now-playing-movie-card/now-playing-movie-card.tsx b/apps/web/src/entities/movie-list/ui/now-playing-movie-card/now-playing-movie-card.tsx new file mode 100644 index 0000000..ccf0a66 --- /dev/null +++ b/apps/web/src/entities/movie-list/ui/now-playing-movie-card/now-playing-movie-card.tsx @@ -0,0 +1,37 @@ +import { AspectRatio } from "@repo/ui/aspect-ratio"; +import Image from "next/image"; + +import { MovieInfoCard } from "@/shared/ui/movie-info-card"; +import { Text } from "@/shared/ui/text"; + +import { MovieListItem } from "../../model"; + +export function NowPlayingMovieCard({ movieInfo }: { movieInfo: MovieListItem }) { + const { posterPath, title, Genres } = movieInfo; + + return ( + + + + {`${title} + + + + + + {title} + + + + + {Genres} + + + ); +} diff --git a/apps/web/src/features/get-now-playing-movie-list/index.ts b/apps/web/src/features/get-now-playing-movie-list/index.ts new file mode 100644 index 0000000..4aedf59 --- /dev/null +++ b/apps/web/src/features/get-now-playing-movie-list/index.ts @@ -0,0 +1 @@ +export * from "./ui"; diff --git a/apps/web/src/features/get-now-playing-movie-list/ui/index.ts b/apps/web/src/features/get-now-playing-movie-list/ui/index.ts new file mode 100644 index 0000000..b6ebe13 --- /dev/null +++ b/apps/web/src/features/get-now-playing-movie-list/ui/index.ts @@ -0,0 +1 @@ +export * from "./now-playing-movie-list/now-playing-movie-list"; diff --git a/apps/web/src/features/get-now-playing-movie-list/ui/now-playing-movie-list/now-playing-movie-list.tsx b/apps/web/src/features/get-now-playing-movie-list/ui/now-playing-movie-list/now-playing-movie-list.tsx new file mode 100644 index 0000000..d0fee5c --- /dev/null +++ b/apps/web/src/features/get-now-playing-movie-list/ui/now-playing-movie-list/now-playing-movie-list.tsx @@ -0,0 +1,36 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import { SwiperOptions } from "swiper/types"; + +import { NowPlayingMovieCard, useNowPlayingMovieListQuery } from "@/entities/movie-list"; +import { screenBreakPoints } from "@/shared/styles"; + +const swiperBreakPoints: SwiperOptions["breakpoints"] = { + [parseInt(screenBreakPoints.sm)]: { slidesPerView: 4, spaceBetween: 12 }, + [parseInt(screenBreakPoints.md)]: { slidesPerView: 5, spaceBetween: 16 }, +}; + +export function NowPlayingMovieList() { + const { data: nowPlayingMovieList, status: nowPlayingMovieListStatus } = useNowPlayingMovieListQuery(); + + // useQueries(movieListQueries.nowPlayingMovieList()); + + if (nowPlayingMovieListStatus === "pending") { + return
Loading...
; + } + + if (nowPlayingMovieListStatus === "error") { + return
Error...
; + } + + return ( + + {nowPlayingMovieList.GeneralAudienceMovies?.map((movieInfo) => { + return ( + + + + ); + })} + + ); +} diff --git a/apps/web/src/shared/api/index.ts b/apps/web/src/shared/api/index.ts index e551d32..55ce2ff 100644 --- a/apps/web/src/shared/api/index.ts +++ b/apps/web/src/shared/api/index.ts @@ -1,2 +1,3 @@ export * from "./tmdb-instance"; export * from "./base-type"; +export * from "./query-client"; diff --git a/apps/web/src/shared/api/query-client.ts b/apps/web/src/shared/api/query-client.ts new file mode 100644 index 0000000..6c7b9de --- /dev/null +++ b/apps/web/src/shared/api/query-client.ts @@ -0,0 +1,3 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient();