diff --git a/README.md b/README.md index cd4d664088..d8003c98b8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# 1단계 - 영화 목록 불러오기 +# 영화 리뷰 - 웹 앱 FE 레벨1 영화 리뷰 미션입니다. +## 1단계 - 영화 목록 불러오기 + ## 영화 목록 조회 기능 - [x] 페이지 상단에 평점이 가장 높은 영화를 출력한다. @@ -24,3 +26,27 @@ FE 레벨1 영화 리뷰 미션입니다. - [x] 검색어가 입력되지 않았을 때 검색 기능이 수행되지 않도록 한다. - [x] api 반환 값이 200이 아닐 경우 에러 메시지를 모달로 보여준다. - [ ] api가 정상적으로 동작하지 않을 경우 에러 메시지를 모달로 보여준다. + +## 2단계 - 상세정보 & UI/UX 개선 + +### 영화 상세 정보 조회 + +- [x] 해당 id를 가진 영화의 상세 정보를 불러온다. +- [x] 상세 정보를 모달창으로 렌더링 한다. +- [x] 키보드의 ESC 키를 누르면 모달 창을 닫는다. + +### 별점 매기기 + +- [x] 영화 별점을 매긴다. (별점은 5개이고 한개당 2점) + - 2점: 최악이예요 + - 4점: 별로예요 + - 6점: 보통이에요 + - 8점: 재미있어요 + - 10점: 명작이에요 +- [x] 사용자가 매긴 별점은 로컬스토리지에 저장한다. + +### UI⁄UX 개선하기 + +- [x] 브라우저 화면의 끝에 도달하면 영화 20개를 자동으로 로드한다. (무한 스크롤) +- [x] 태블릿 화면에서 영화 목록과 모달 레이아웃이 반응형으로 동작하도록 한다. +- [x] 모바일 화면에서 영화 목록과 모달 레이아웃이 반응형으로 동작하도록 한다. diff --git a/cypress/e2e/error-handling.cy.ts b/cypress/e2e/error-handling.cy.ts index 6b304a9306..df034e46b2 100644 --- a/cypress/e2e/error-handling.cy.ts +++ b/cypress/e2e/error-handling.cy.ts @@ -1,18 +1,16 @@ import { moviesFixture } from "../../test/fixtures"; +import { mockPopularPage } from "../support/movie"; describe("오류 대응 테스트", () => { beforeEach(() => { - cy.intercept("GET", "**/movie/popular?page=1", { - statusCode: 200, - body: { - page: 1, - results: [...moviesFixture], - total_pages: 2, - total_results: 40, - }, - }).as("getPopularPage1"); + mockPopularPage({ + page: 1, + results: moviesFixture, + totalPages: 2, + totalResults: 40, + }); - cy.intercept("GET", "**/movie/popular?page=2", { + cy.intercept("GET", "**/movie/popular?page=2&language=ko-KR", { statusCode: 400, body: { success: false, @@ -36,7 +34,8 @@ describe("오류 대응 테스트", () => { const alertSpy = cy.stub(); cy.on("window:alert", alertSpy); - cy.get("#more-button").click(); + cy.get(".scroll-sentinel").scrollIntoView(); + cy.wait("@getInvalidPopularPage"); cy.wrap(alertSpy).should("have.been.called"); }); }); diff --git a/cypress/e2e/movie-list-rendering.cy.ts b/cypress/e2e/movie-list-rendering.cy.ts index b8aae4dcef..37a24b657f 100644 --- a/cypress/e2e/movie-list-rendering.cy.ts +++ b/cypress/e2e/movie-list-rendering.cy.ts @@ -1,26 +1,28 @@ import { moviesFixture } from "../../test/fixtures"; +import { mockPopularPage } from "../support/movie"; describe("영화 목록 조회 기능 테스트", () => { beforeEach(() => { - cy.intercept("GET", "**/movie/popular?page=1", { - statusCode: 200, - body: { - page: 1, - results: [...moviesFixture], - total_pages: 2, - total_results: 40, - }, - }).as("getPopularPage1"); - - cy.intercept("GET", "**/movie/popular?page=2", { - statusCode: 200, - body: { - page: 2, - results: [...moviesFixture], - total_pages: 2, - total_results: 40, - }, - }).as("getPopularPage2"); + mockPopularPage({ + page: 1, + results: moviesFixture, + totalPages: 2, + totalResults: 40, + }); + + mockPopularPage({ + page: 2, + results: moviesFixture, + totalPages: 2, + totalResults: 40, + }); + + mockPopularPage({ + page: 3, + results: [], + totalPages: 2, + totalResults: 40, + }); cy.visit("localhost:5173"); cy.wait("@getPopularPage1"); @@ -30,17 +32,18 @@ describe("영화 목록 조회 기능 테스트", () => { cy.get("#movie-list li").should("have.length", 20); }); - it("더보기 버튼을 누르면 영화 목록이 추가로 생성되어 렌더링 된다.", () => { - cy.get("#more-button").click(); + it("리스트 아래 sentinel이 보이면 영화 목록이 추가로 생성되어 렌더링 된다.", () => { + cy.get(".scroll-sentinel").scrollIntoView(); cy.wait("@getPopularPage2"); - cy.get("#movie-list li").should("have.length.greaterThan", 20); + cy.get("#movie-list li").should("have.length", 40); }); - it("마지막 페이지까지 렌더링 됬을 때 더보기 버튼을 출력하지 않는다.", () => { - cy.get("#more-button").click(); + it("마지막 페이지까지 렌더링되면 추가 요청이 발생하지 않는다.", () => { + cy.get(".scroll-sentinel").scrollIntoView(); cy.wait("@getPopularPage2"); - cy.get("#more-button").should("not.be.visible"); + cy.get(".scroll-sentinel").scrollIntoView(); + cy.get("@getPopularPage3.all").should("have.length", 0); }); }); diff --git a/cypress/e2e/movie-modal.cy.ts b/cypress/e2e/movie-modal.cy.ts new file mode 100644 index 0000000000..b151d28d65 --- /dev/null +++ b/cypress/e2e/movie-modal.cy.ts @@ -0,0 +1,49 @@ +import { movieDetailFixture, moviesFixture } from "../../test/fixtures"; +import { + mockMovieDetail, + mockPopularPage, + openMovieModal, +} from "../support/movie"; + +describe("영화 모달 기능 테스트", () => { + beforeEach(() => { + mockPopularPage({ + page: 1, + results: moviesFixture, + totalPages: 2, + totalResults: 40, + }); + + mockMovieDetail(0, movieDetailFixture); + + cy.visit("localhost:5173"); + cy.wait("@getPopularPage1"); + }); + + it("영화 목록을 클릭하면 모달이 뜨고 닫기 버튼으로 닫을 수 있다", () => { + openMovieModal(0); + + cy.get("#close-modal").click(); + cy.get("#modal-background").should("not.have.class", "active"); + }); + + it("ESC 버튼을 누르면 모달이 닫힌다", () => { + openMovieModal(0); + + cy.get("body").type("{esc}"); + cy.get("#modal-background").should("not.have.class", "active"); + }); + + it("모달창이 아닌 부분을 클릭하면 모달이 닫힌다", () => { + openMovieModal(0); + + cy.get("#modal-background").click("topLeft"); + cy.get("#modal-background").should("not.have.class", "active"); + }); + + it("모달창이 뜨면 배경 스크롤이 적용되지 않는다", () => { + openMovieModal(0); + + cy.get("body").should("have.class", "modal-open"); + }); +}); diff --git a/cypress/e2e/movie-rating.cy.ts b/cypress/e2e/movie-rating.cy.ts new file mode 100644 index 0000000000..dd4e24fa82 --- /dev/null +++ b/cypress/e2e/movie-rating.cy.ts @@ -0,0 +1,71 @@ +import { RATING_SCORES, RATING_TEXTS } from "../../src/constants/rating"; +import { movieDetailFixture, moviesFixture } from "../../test/fixtures"; +import { + mockPopularPage, + mockMovieDetail, + openMovieModal, +} from "../support/movie"; + +describe("영화 별점 기능 테스트", () => { + beforeEach(() => { + mockPopularPage({ + page: 1, + results: moviesFixture, + totalPages: 2, + totalResults: 40, + }); + + mockMovieDetail(0, movieDetailFixture); + + cy.visit("localhost:5173"); + cy.wait("@getPopularPage1"); + + cy.clearLocalStorage(); + }); + + it("모달창에서 별점을 클릭하면 해당 별만큼 채워지고 점수와 평가 문구가 바뀐다", () => { + openMovieModal(0); + + cy.get(".movie-rating .stars img").eq(3).click(); + + cy.get(".movie-rating .stars img") + .eq(0) + .should("have.attr", "src") + .and("include", "star_filled.png"); + + cy.get(".movie-rating .stars img") + .eq(3) + .should("have.attr", "src") + .and("include", "star_filled.png"); + + cy.get(".movie-rating .stars img") + .eq(4) + .should("have.attr", "src") + .and("include", "star_empty.png"); + + cy.get(".rating-text").should("have.text", RATING_TEXTS[3]); + cy.get("#rating-value").should("have.text", RATING_SCORES[3]); + }); + + it("별점을 매기고 다시 모달을 열면 이전 별점이 유지된다", () => { + openMovieModal(0); + + cy.get(".movie-rating .stars img").eq(3).click(); + cy.get("#close-modal").click(); + + openMovieModal(0); + + cy.get(".rating-text").should("have.text", RATING_TEXTS[3]); + cy.get("#rating-value").should("have.text", RATING_SCORES[3]); + + cy.get(".movie-rating .stars img") + .eq(3) + .should("have.attr", "src") + .and("include", "star_filled.png"); + + cy.get(".movie-rating .stars img") + .eq(4) + .should("have.attr", "src") + .and("include", "star_empty.png"); + }); +}); diff --git a/cypress/e2e/movie-search.cy.ts b/cypress/e2e/movie-search.cy.ts index c293833f73..4aa52a34a1 100644 --- a/cypress/e2e/movie-search.cy.ts +++ b/cypress/e2e/movie-search.cy.ts @@ -1,44 +1,37 @@ import { searchFixture } from "../../test/fixtures"; +import { mockSearchPage } from "../support/movie"; describe("영화 검색 기능 테스트", () => { beforeEach(() => { - cy.intercept( - "GET", - "**/search/movie?page=1&query=%EC%8A%A4%ED%8C%8C%EC%9D%B4", - { - statusCode: 200, - body: { - page: 1, - results: [...searchFixture], - total_pages: 2, - total_results: 40, - }, - }, - ).as("getSearchPage1"); + mockSearchPage({ + query: "스파이", + page: 1, + results: searchFixture, + totalPages: 2, + totalResults: 40, + }); + + mockSearchPage({ + query: "스파이", + page: 2, + results: searchFixture, + totalPages: 2, + totalResults: 40, + }); cy.intercept( "GET", - "**/search/movie?page=2&query=%EC%8A%A4%ED%8C%8C%EC%9D%B4", + "**/search/movie?page=1&query=%EB%B7%80&language=ko-KR", { statusCode: 200, body: { - page: 2, - results: [...searchFixture], - total_pages: 2, - total_results: 40, + page: 1, + results: [], + total_pages: 1, + total_results: 0, }, }, - ).as("getSearchPage2"); - - cy.intercept("GET", "**/search/movie?page=1&query=%EB%B7%80", { - statusCode: 200, - body: { - page: 1, - results: [], - total_pages: 1, - total_results: 0, - }, - }).as("getSearchNoResult"); + ).as("getSearchNoResult"); cy.visit("localhost:5173"); }); @@ -59,26 +52,27 @@ describe("영화 검색 기능 테스트", () => { cy.get("#movie-list li").should("have.length.greaterThan", 0); }); - it("검색 후 더보기 버튼을 클릭하면 필터링 된 영화 목록이 추가로 출력된다.", () => { + it("검색 후 스크롤을 내려 sentinel 요소가 보이면 필터링 된 영화 목록이 추가로 출력된다.", () => { cy.get("#search-input").type("스파이"); cy.get("#search-button").click(); cy.wait("@getSearchPage1"); - cy.get("#more-button").click(); + cy.get(".scroll-sentinel").scrollIntoView(); cy.wait("@getSearchPage2"); - cy.get("#movie-list li").should("have.length.greaterThan", 20); + cy.get("#movie-list li").should("have.length", 40); }); - it("필터링 된 영화 목록이 마지막 페이지면 더보기 버튼을 출력하지 않는다.", () => { + it("필터링 된 영화 목록이 마지막 페이지면 sentinel 요소가 보여도 추가로 요청하지 않는다.", () => { cy.get("#search-input").type("스파이"); cy.get("#search-button").click(); cy.wait("@getSearchPage1"); - cy.get("#more-button").click(); + cy.get(".scroll-sentinel").scrollIntoView(); cy.wait("@getSearchPage2"); - cy.get("#more-button").should("not.be.visible"); + cy.get(".scroll-sentinel").scrollIntoView(); + cy.get("#movie-list li").should("have.length", 40); }); it("검색 결과가 없을 때는 안내메시지를 출력한다.", () => { diff --git a/cypress/support/movie.ts b/cypress/support/movie.ts new file mode 100644 index 0000000000..b4ea309222 --- /dev/null +++ b/cypress/support/movie.ts @@ -0,0 +1,77 @@ +import { + movieDetailFixture, + moviesFixture, + searchFixture, +} from "../../test/fixtures"; + +interface MockPopularPageParams { + page: number; + results: typeof moviesFixture; + totalPages: number; + totalResults: number; +} + +interface MockSearchPageParams { + query: string; + page: number; + results: typeof searchFixture; + totalPages: number; + totalResults: number; +} + +export const mockPopularPage = ({ + page, + results, + totalPages, + totalResults, +}: MockPopularPageParams) => { + cy.intercept("GET", `**/movie/popular?page=${page}&language=ko-KR`, { + statusCode: 200, + body: { + page, + results: [...results], + total_pages: totalPages, + total_results: totalResults, + }, + }).as(`getPopularPage${page}`); +}; + +export const mockSearchPage = ({ + query, + page, + results, + totalPages, + totalResults, +}: MockSearchPageParams) => { + cy.intercept( + "GET", + `**/search/movie?page=${page}&query=${encodeURIComponent(query)}&language=ko-KR`, + { + statusCode: 200, + body: { + page, + results: [...results], + total_pages: totalPages, + total_results: totalResults, + }, + }, + ).as(`getSearchPage${page}`); +}; + +export const mockMovieDetail = ( + movieIndex: number, + body: typeof movieDetailFixture, +) => { + const movieId = moviesFixture[movieIndex].id; + + cy.intercept("GET", `**/movie/${movieId}?language=ko-KR`, { + statusCode: 200, + body, + }).as("getMovieDetail"); +}; + +export const openMovieModal = (movieIndex: number) => { + cy.get("#movie-list li").eq(movieIndex).click(); + cy.wait("@getMovieDetail"); + cy.get("#modal-background").should("have.class", "active"); +}; diff --git a/index.html b/index.html index 62f4ff3533..2eb91c407b 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,7 @@ + 영화 리뷰 @@ -61,7 +62,7 @@

지금 인기 있는 영화

-