Skip to content

Commit

Permalink
Merge pull request #26 from f-lab-edu/feature/24-now-playing-movie-list
Browse files Browse the repository at this point in the history
구현 - 현재 상영중인 영화 목록 추가
  • Loading branch information
ag502 authored Oct 6, 2024
2 parents 0a0b7fe + e7417b3 commit 381105d
Show file tree
Hide file tree
Showing 32 changed files with 264 additions and 9 deletions.
7 changes: 4 additions & 3 deletions apps/web/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<div className={lightThemeClass} id="__app">
<div className={lightThemeClass}>
<Component {...pageProps} />
</div>
</QueryClientProvider>
Expand Down
35 changes: 32 additions & 3 deletions apps/web/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<typeof getStaticProps>) {
return (
<>
<HydrationBoundary state={dehydratedState}>
<Head>
<title>Create Next App</title>
<meta content="Generated by create next app" name="description" />
Expand All @@ -19,6 +24,15 @@ export default function Home() {
</Head>

<Container>
<Row>
<Col lg={12} md={12} sm={4}>
<Text as="h2" className={popularMovieListTitle} size="xl" weight="bold">
현재 상영작
</Text>
<NowPlayingMovieList />
</Col>
</Row>

<Row>
<Col lg={12} md={12} sm={4}>
<Text as="h2" className={popularMovieListTitle} size="xl" weight="bold">
Expand All @@ -37,6 +51,21 @@ export default function Home() {
</Col>
</Row>
</Container>
</>
</HydrationBoundary>
);
}

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 }>;
20 changes: 20 additions & 0 deletions apps/web/src/entities/genres/api/genres-api.ts
Original file line number Diff line number Diff line change
@@ -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<GenresDTO> {
return tmdbHttp
.get(`${genresBaseURL}/movie/list`, { params: { language }, ...axiosConfig })
.then((res) => camelCaseObjMapper(res.data));
}
}
4 changes: 4 additions & 0 deletions apps/web/src/entities/genres/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./genres-api";
export * from "./request-types";
export * from "./response-types";
export * from "./queries";
2 changes: 2 additions & 0 deletions apps/web/src/entities/genres/api/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./query-keys";
export * from "./use-movie-genres-query";
5 changes: 5 additions & 0 deletions apps/web/src/entities/genres/api/queries/query-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { GenresReqParams } from "../request-types";

export const genresQueryKeys = {
movieGenres: ({ language }: GenresReqParams) => ["movie-genres", { language }] as const,
};
16 changes: 16 additions & 0 deletions apps/web/src/entities/genres/api/queries/use-movie-genres-query.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
5 changes: 5 additions & 0 deletions apps/web/src/entities/genres/api/request-types/genres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BaseAxiosReq } from "@/shared/api";

export interface GenresReqParams extends BaseAxiosReq {
language: string;
}
1 change: 1 addition & 0 deletions apps/web/src/entities/genres/api/request-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./genres";
3 changes: 3 additions & 0 deletions apps/web/src/entities/genres/api/response-types/genres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GenresDTO {
genres: { id: number; name: string }[];
}
1 change: 1 addition & 0 deletions apps/web/src/entities/genres/api/response-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./genres";
2 changes: 2 additions & 0 deletions apps/web/src/entities/genres/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./api";
export * from "./models";
15 changes: 15 additions & 0 deletions apps/web/src/entities/genres/models/genres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createBaseModel } from "@/shared/lib";

import { GenresDTO } from "../api";

export class Genres extends createBaseModel<GenresDTO>() {
constructor(private genresList: GenresDTO) {
super(genresList);
}

get GenresList() {
return this.genresList.genres.reduce((prev, { id, name }) => {
return prev.set(id, { id, name });
}, new Map<number, GenresDTO["genres"][0]>());
}
}
1 change: 1 addition & 0 deletions apps/web/src/entities/genres/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./genres";
1 change: 1 addition & 0 deletions apps/web/src/entities/movie-list/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
12 changes: 12 additions & 0 deletions apps/web/src/entities/movie-list/api/movie-list-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<NowPlayingMovieListDTO> {
return tmdbHttp
.get(`${movieBaseURL}/now_playing`, { params: { page, language, region }, ...axiosConfig })
.then((res) => camelCaseObjMapper(res.data));
}
}
1 change: 1 addition & 0 deletions apps/web/src/entities/movie-list/api/movie-list.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/entities/movie-list/api/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./use-now-playing-movie-list-query";
export * from "./query-keys";
6 changes: 6 additions & 0 deletions apps/web/src/entities/movie-list/api/queries/query-keys.ts
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ import { MovieListItemDTO } from "./movie-list-item";
export interface MovieListDTO {
page: number;
results: MovieListItemDTO[];
total_pages: number;
total_results: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MovieListDTO } from "./movie-list";

export interface NowPlayingMovieListDTO extends MovieListDTO {
dates: {
maximum: string;
minimum: string;
};
}
10 changes: 9 additions & 1 deletion apps/web/src/entities/movie-list/model/movie-list-item.ts
Original file line number Diff line number Diff line change
@@ -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<MovieListItemDTO>() {
constructor(private movieListItem: MovieListItemDTO) {
constructor(
private movieListItem: MovieListItemDTO,
private genresModel?: Genres,
) {
super(movieListItem);
}

Expand All @@ -33,4 +37,8 @@ export class MovieListItem extends createBaseModel<MovieListItemDTO>() {
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]! : "";
}
}
10 changes: 8 additions & 2 deletions apps/web/src/entities/movie-list/model/movie-list.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Genres } from "@/entities/genres";
import { createBaseModel } from "@/shared/lib";

import { MovieListDTO } from "../api/response-types/movie-list";

import { MovieListItem } from "./movie-list-item";

export abstract class MovieList extends createBaseModel<MovieListDTO>() {
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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MovieList } from "./movie-list";

export class NowPlayingMovieList extends MovieList {}
1 change: 1 addition & 0 deletions apps/web/src/entities/movie-list/ui/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -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 (
<MovieInfoCard>
<MovieInfoCard.Poster>
<AspectRatio ratio={4 / 3}>
<Image
alt={`${title} poster image`}
height={300}
src={`${process.env.NEXT_PUBLIC_TMDB_RESOURCE_BASE_URL}/t/p/w400${posterPath}`}
style={{ width: "100%", height: "100%" }}
width={200}
/>
</AspectRatio>
</MovieInfoCard.Poster>

<MovieInfoCard.Title>
<Text lineHeight="short" size="md">
{title}
</Text>
</MovieInfoCard.Title>

<MovieInfoCard.Description>
<Text size="xs">{Genres}</Text>
</MovieInfoCard.Description>
</MovieInfoCard>
);
}
1 change: 1 addition & 0 deletions apps/web/src/features/get-now-playing-movie-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ui";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./now-playing-movie-list/now-playing-movie-list";
Original file line number Diff line number Diff line change
@@ -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 <div>Loading...</div>;
}

if (nowPlayingMovieListStatus === "error") {
return <div>Error...</div>;
}

return (
<Swiper breakpoints={swiperBreakPoints} slidesPerView={3} spaceBetween={8}>
{nowPlayingMovieList.GeneralAudienceMovies?.map((movieInfo) => {
return (
<SwiperSlide key={movieInfo.id}>
<NowPlayingMovieCard movieInfo={movieInfo} />
</SwiperSlide>
);
})}
</Swiper>
);
}
Loading

0 comments on commit 381105d

Please sign in to comment.