diff --git a/apps/web/package.json b/apps/web/package.json index f464f94..421900f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ "@repo/ui": "workspace:*", "@tanstack/react-query": "^5.52.0", "axios": "^1.7.4", + "dayjs": "^1.11.13", "next": "14.2.5", "react": "^18", "react-dom": "^18", diff --git a/apps/web/pages/index.tsx b/apps/web/pages/index.tsx index 48613d1..97dd5ed 100644 --- a/apps/web/pages/index.tsx +++ b/apps/web/pages/index.tsx @@ -1,6 +1,7 @@ import Head from "next/head"; import { PopularMovieList } from "@/features/get-popular-movie-list"; +import { UpcomingMovieList } from "@/features/get-upcoming-movie-list"; import { popularMovieListTitle } from "@/pages/home"; import { Col } from "@/shared/ui/col"; import { Container } from "@/shared/ui/container"; @@ -15,6 +16,7 @@ export default function Home() { + @@ -22,6 +24,13 @@ export default function Home() { + + + +

공개 예정작

+ + +
); 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 e57e310..fd4204d 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 @@ -1,20 +1,24 @@ import { tmdbHttp } from "@/shared/api"; -import { camelCaseObjMapper, Nullable } from "@/shared/lib"; +import { camelCaseObjMapper } from "@/shared/lib"; -import { PopularMovieListReqParams } from "./request-types"; +import { MovieListReqParams } from "./request-types"; import { MovieListDTO } from "./response-types"; +import { UpcomingMovieListDTO } from "./response-types/upcoming-movie-list"; -export default class MovieListApi { - static baseURL = "movie"; +const movieBaseURL = "movie"; +export default class MovieListApi { + /** + * 트렌드 영화 목록 + */ static async getPopularMovieList({ page = 1, language = "ko-KR", region = 410, ...axiosConfig - }: PopularMovieListReqParams): Promise> { + }: MovieListReqParams): Promise { return tmdbHttp - .get(`${this.baseURL}/popular`, { + .get(`${movieBaseURL}/popular`, { params: { page, language, @@ -24,4 +28,15 @@ export default class MovieListApi { }) .then((res) => camelCaseObjMapper(res.data)); } + + /** + * 개봉 예정 영화 목록 + */ + static async getUpcomingMovieList( + { page, language, region, ...axiosConfig }: MovieListReqParams = { page: 1, language: "ko-KR", region: "KR" }, + ): Promise { + return tmdbHttp + .get(`${movieBaseURL}/upcoming`, { 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 9b59550..eace419 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 @@ -1,21 +1,27 @@ import { queryOptions } from "@tanstack/react-query"; import { MovieList } from "../model"; +import { UpcomingMovieList } from "../model/upcoming-movie-list"; import MovieListApi from "./movie-list-api"; -import { PopularMovieListReqParams } from "./request-types"; +import { MovieListReqParams } from "./request-types"; export const movieListQueryKeys = { popularMovieList: () => ["popular-movie-list"] as const, + upcomingMovieList: () => ["upcoming-movie-list"] as const, }; export const movieListQueries = { - popularMovieList: ( - { page, language, region }: PopularMovieListReqParams = { page: 1, language: "ko-KR", region: 410 }, - ) => + popularMovieList: ({ page, language, region }: MovieListReqParams = { page: 1, language: "ko-KR", region: 410 }) => queryOptions({ queryKey: [...movieListQueryKeys.popularMovieList(), { page, language, region }], queryFn: () => MovieListApi.getPopularMovieList({ page, language, region }), select: (data) => new MovieList(data), }), + upcomingMovieList: ({ page, language, region }: MovieListReqParams = { page: 1, language: "ko-KR", region: "KR" }) => + queryOptions({ + queryKey: [...movieListQueryKeys.upcomingMovieList(), { page, language, region }], + queryFn: () => MovieListApi.getUpcomingMovieList({ page, language, region }), + select: (data) => new UpcomingMovieList(data), + }), }; diff --git a/apps/web/src/entities/movie-list/api/request-types/index.ts b/apps/web/src/entities/movie-list/api/request-types/index.ts index 0afd581..d4b55f8 100644 --- a/apps/web/src/entities/movie-list/api/request-types/index.ts +++ b/apps/web/src/entities/movie-list/api/request-types/index.ts @@ -1 +1 @@ -export * from "./popular-movie-list"; +export * from "./movie-list"; diff --git a/apps/web/src/entities/movie-list/api/request-types/popular-movie-list.ts b/apps/web/src/entities/movie-list/api/request-types/movie-list.ts similarity index 60% rename from apps/web/src/entities/movie-list/api/request-types/popular-movie-list.ts rename to apps/web/src/entities/movie-list/api/request-types/movie-list.ts index 005facf..40f0294 100644 --- a/apps/web/src/entities/movie-list/api/request-types/popular-movie-list.ts +++ b/apps/web/src/entities/movie-list/api/request-types/movie-list.ts @@ -1,6 +1,6 @@ import { BaseAxiosReq } from "@/shared/api"; -export interface PopularMovieListReqParams extends BaseAxiosReq { +export interface MovieListReqParams extends BaseAxiosReq { page: number; language: string; region: number; diff --git a/apps/web/src/entities/movie-list/api/response-types/upcoming-movie-list.ts b/apps/web/src/entities/movie-list/api/response-types/upcoming-movie-list.ts new file mode 100644 index 0000000..d155104 --- /dev/null +++ b/apps/web/src/entities/movie-list/api/response-types/upcoming-movie-list.ts @@ -0,0 +1,8 @@ +import { MovieListDTO } from "./movie-list"; + +export interface UpcomingMovieListDTO 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 b3fa858..653ebb5 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,3 +1,5 @@ +import dayjs from "dayjs"; + import { languageRegions } from "@/shared/config"; import { createBaseModel } from "@/shared/lib"; @@ -23,4 +25,12 @@ export class MovieListItem extends createBaseModel() { get Region() { return languageRegions[this.originalLanguage] ?? this.originalLanguage; } + + get Platform() { + return this.video ? "OTT" : "극장"; + } + + get ReleasedDateDotParsed() { + return dayjs(this.ReleasedDate).format("YYYY.MM.DD"); + } } diff --git a/apps/web/src/entities/movie-list/model/upcoming-movie-list.ts b/apps/web/src/entities/movie-list/model/upcoming-movie-list.ts new file mode 100644 index 0000000..b49ea71 --- /dev/null +++ b/apps/web/src/entities/movie-list/model/upcoming-movie-list.ts @@ -0,0 +1,15 @@ +import { UpcomingMovieListDTO } from "../api/response-types/upcoming-movie-list"; + +import { MovieList } from "./movie-list"; + +export class UpcomingMovieList extends MovieList { + public rangeStartDate: string; + public rangeEndData: string; + + constructor(private upcomingMovieList: UpcomingMovieListDTO) { + super(upcomingMovieList); + + this.rangeStartDate = upcomingMovieList.dates.minimum; + this.rangeEndData = upcomingMovieList.dates.maximum; + } +} diff --git a/apps/web/src/entities/movie-list/ui/index.ts b/apps/web/src/entities/movie-list/ui/index.ts index 1344c77..861bb23 100644 --- a/apps/web/src/entities/movie-list/ui/index.ts +++ b/apps/web/src/entities/movie-list/ui/index.ts @@ -1 +1,2 @@ export * from "./popular-movie-card/popular-movie-card"; +export * from "./upcoming-movie-card/upcoming-movie-card"; diff --git a/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.css.ts b/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.css.ts new file mode 100644 index 0000000..30c642f --- /dev/null +++ b/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.css.ts @@ -0,0 +1,12 @@ +import { style } from "@vanilla-extract/css"; + +import { themeColorContract } from "@/shared/styles"; + +export const descriptionWrapper = style({ + display: "flex", + gap: "5px", +}); + +export const releasedDate = style({ + color: themeColorContract.color.primary10, +}); diff --git a/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.tsx b/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.tsx new file mode 100644 index 0000000..dcf1ecf --- /dev/null +++ b/apps/web/src/entities/movie-list/ui/upcoming-movie-card/upcoming-movie-card.tsx @@ -0,0 +1,39 @@ +import { AspectRatio } from "@repo/ui/aspect-ratio"; +import Image from "next/image"; + +import { MovieInfoCard } from "@/shared/ui/movie-info-card"; + +import { MovieListItem } from "../../model"; + +import { descriptionWrapper, releasedDate } from "./upcoming-movie-card.css"; + +export function UpcomingMovieCard({ movieInfo }: { movieInfo: MovieListItem }) { + const { posterPath, title, ReleasedDateDotParsed, Platform } = movieInfo; + + return ( + + + + {`${title} + + + + + {title} + + + +
+ {Platform} + {ReleasedDateDotParsed} +
+
+
+ ); +} diff --git a/apps/web/src/features/get-popular-movie-list/ui/popular-movie-list/popular-movie-list.tsx b/apps/web/src/features/get-popular-movie-list/ui/popular-movie-list/popular-movie-list.tsx index 09009b9..d0555b9 100644 --- a/apps/web/src/features/get-popular-movie-list/ui/popular-movie-list/popular-movie-list.tsx +++ b/apps/web/src/features/get-popular-movie-list/ui/popular-movie-list/popular-movie-list.tsx @@ -1,22 +1,18 @@ import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; import { Swiper, SwiperSlide } from "swiper/react"; import { SwiperOptions } from "swiper/types"; import { PopularMovieCard, movieListQueries } 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 PopularMovieList() { const { data: popularMovieList, status: popularMovieListStatus } = useQuery(movieListQueries.popularMovieList()); - const swiperBreakPoints: SwiperOptions["breakpoints"] = useMemo( - () => ({ - [parseInt(screenBreakPoints.sm)]: { slidesPerView: 4, spaceBetween: 12 }, - [parseInt(screenBreakPoints.md)]: { slidesPerView: 5, spaceBetween: 16 }, - }), - [], - ); - if (popularMovieListStatus === "pending") { return
Loading...
; } diff --git a/apps/web/src/features/get-upcoming-movie-list/index.ts b/apps/web/src/features/get-upcoming-movie-list/index.ts new file mode 100644 index 0000000..4aedf59 --- /dev/null +++ b/apps/web/src/features/get-upcoming-movie-list/index.ts @@ -0,0 +1 @@ +export * from "./ui"; diff --git a/apps/web/src/features/get-upcoming-movie-list/ui/index.ts b/apps/web/src/features/get-upcoming-movie-list/ui/index.ts new file mode 100644 index 0000000..b8b11eb --- /dev/null +++ b/apps/web/src/features/get-upcoming-movie-list/ui/index.ts @@ -0,0 +1 @@ +export * from "./upcoming-movie-list/upcoming-movie-list"; diff --git a/apps/web/src/features/get-upcoming-movie-list/ui/upcoming-movie-list/upcoming-movie-list.tsx b/apps/web/src/features/get-upcoming-movie-list/ui/upcoming-movie-list/upcoming-movie-list.tsx new file mode 100644 index 0000000..6fee6f8 --- /dev/null +++ b/apps/web/src/features/get-upcoming-movie-list/ui/upcoming-movie-list/upcoming-movie-list.tsx @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { SwiperOptions } from "swiper/types"; + +import { UpcomingMovieCard, movieListQueries } 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 UpcomingMovieList() { + const { data: upcomingMovieList, status: upcomingMovieListStatus } = useQuery(movieListQueries.upcomingMovieList()); + + if (upcomingMovieListStatus === "pending") { + return
Loading...
; + } + + if (upcomingMovieListStatus === "error") { + return
Error...
; + } + + return ( + + {upcomingMovieList.GeneralAudienceMovies?.map((movieInfo) => { + return ( + + + + ); + })} + + ); +} diff --git a/apps/web/src/shared/config/language-regions.ts b/apps/web/src/shared/config/language-regions.ts index 26cc303..6ccc07b 100644 --- a/apps/web/src/shared/config/language-regions.ts +++ b/apps/web/src/shared/config/language-regions.ts @@ -4,4 +4,6 @@ export const languageRegions: Record = { ja: "일본", hi: "인도", fr: "프랑스", + pt: "포르투갈", + cn: "중국", }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c40406d..809b3d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: axios: specifier: ^1.7.4 version: 1.7.4 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 next: specifier: 14.2.5 version: 14.2.5(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.2.0) @@ -2987,6 +2990,10 @@ packages: engines: {node: '>= 14'} dev: true + /dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dev: false + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: