-
Notifications
You must be signed in to change notification settings - Fork 155
[2단계 - 영화 목록 불러오기] 포도 미션 제출합니다. #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
goodsmell
wants to merge
60
commits into
woowacourse:goodsmell
Choose a base branch
from
goodsmell:step-2
base: goodsmell
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
72aff64
docs: 커밋 테스트
goodsmell 62261f0
docs: 요구사항 및 테스트 명세 작성
goodsmell c9a1238
feat: 인기 영화 20개를 가져온다.
goodsmell 4be2b05
chore: env파일 무시
goodsmell 06fccf8
feat: 인기 영화 목록 렌더링
goodsmell b9ff268
feat: 검색창 UI 구현
goodsmell 4cee69e
feat: 배너 설정
goodsmell 8214f4f
feat: 더보기 기능 구현
goodsmell 864a72c
feat: 검색 기능 구현
goodsmell 3b71dee
feat: 더보기 숨기기 기능 구현
goodsmell ac83534
feat: 스켈레톤UI 구현
goodsmell 7683de0
fix: 검색 결과 없을 때 UI 중복되는 오류 해결
goodsmell 2fa44b0
docs: E2E 시나리오 작성
goodsmell 7090a41
chore: 스타일 파일 이동
goodsmell bc6fb20
test: 검색 시나리오 테스트 구현
goodsmell d8d57ec
test: 메인 시나리오 테스트 구현
goodsmell fba0e4d
feat: api 요청 시 더 보기 비활성 구현
goodsmell 90c441e
test: api 요청 예외 테스트
goodsmell d6fe4d0
refector: 파일구조 분리
goodsmell 3a177a6
chore: 배포 파이프라인 설정
goodsmell 5824970
fix: 빌드 오류 해결
goodsmell 130ab9c
refactor: main.ts - try-catch-finally 패턴 적용
goodsmell e4bf5e4
refactor: moreButton.ts - try-catch-finally 패턴 적용 및 중복 로직 제거
goodsmell d58f4d1
refatcore: searchForm.ts - try-catch-finally 패턴 적용
goodsmell cbb8693
fix: 검색 시 페이지 상태 초기화
goodsmell c1aada5
fix: 검색 결과에 따라 더보기 버튼 표시 처리
goodsmell a26637b
fix: response.json() 파싱 실패 처리를 위한 try/catch 추가
goodsmell 692c2c2
refactor: non-null assertion(!) 제거 및 null 체크 추가
goodsmell beffa5a
fix: 영화 이미지 없을 경우 fallback image 사용
goodsmell d764db5
refactor: bindEvent 역할 분리
goodsmell ca9f91a
refactor: 상태 관리를 DOM 기준에서 store 기준으로 개선
goodsmell b45c77f
refactor: 중복 랜더링 제거
goodsmell 689ac3b
refactor: main.ts 구조 개선 및 역할 분리
goodsmell 443f5b1
refactor: searchForm DOM 요소를 역할별 객체로 묶어 관리하도록 개선
goodsmell 2ca9882
refactor: SearchForm의 검색처리 로직을 메서드로 분리
goodsmell b772bb4
refactor: MoreButton에 더보기 버튼 표시 로직을 위임
goodsmell cbeb870
chore: 파일 구조 분리
goodsmell a614dc4
fix: 리스트 초기화 없이 append되어 영화 목록이 누적되는 문제 수정
goodsmell 12affa2
refactor: 영화 API 요청 및 응답 처리 로직 공통화
goodsmell 0eec055
fix: api 링크 지역 및 언어 설정
goodsmell 8dd1f01
docs: step2 요구사항 목록 작성
goodsmell ce95c96
feat: 영화 상세 정보 모달 구현
goodsmell 11041d0
feat: 별점 입력 기능 구현
goodsmell 25e3908
feat:무한 스크롤 구현 및 더보기 버튼 삭제
goodsmell bb64502
fix: 모달 열리면 스크롤 방어
goodsmell 7eef935
feat: 모달 즉시 열기 및 로딩 상태 추가
goodsmell 7a378a9
style: 반응형 적용
goodsmell 7752223
style: section title 워딩 변경
goodsmell cf9c908
test: 반응형, 모달, 무한스크롤을 e2e에 적용
goodsmell 6c2eacb
fix: RATING_LABELS 워딩 변경
goodsmell b06cdfb
fix: 모달 이미지 렌더링 문제 수정 및 fallback 처리 추가
goodsmell 492cb85
Merge remote-tracking branch 'upstream/goodsmell' into step-2
goodsmell 4076b17
refactor: 클래스 인스턴스 변수 3개 이하로 제한
goodsmell b26003f
docs: 요구사항 목록 정리
goodsmell 1f0e248
refactor: 오타 수정
goodsmell b45f46c
refactor: 사용자 친화적 에러메시지로 변경
goodsmell 98deb00
refactor: 빈 main 블록 제거
goodsmell 90f3faa
refactor: 상수화
goodsmell 62c5f46
refactor: requireElement 유틸로 DOM 조회 방식 통일
goodsmell 4694bc2
refactor: 이미지 로딩 로직을 loadImageWithFallback 유틸로 추출
goodsmell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # javascript-movie-review | ||
|
|
||
| FE 레벨1 영화 리뷰 미션 step2 | ||
|
|
||
| # 요구사항 목록 | ||
|
|
||
| ## 1. 영화 상세정보 조회 | ||
|
|
||
| - 영화를 클릭하면 모달이 뜬다 | ||
| - ESC를 누르면 모달이 사라진다 | ||
| - x를 누르면 모달이 사라진다 | ||
|
|
||
| ## 2. 별점 매기기 | ||
|
|
||
| - 영화 상세정보 모달에서 별을 눌러 별점을 설정할 수 있다. | ||
| - 별점은 5개로 구성되어 있으며 한 개당 2점이다. | ||
| - 2점: 최악이예요 | ||
| - 4점: 별로예요 | ||
| - 6점: 보통이에요 | ||
| - 8점: 재미있어요 | ||
| - 10점: 명작이에요 | ||
| - 새로고침을 하더라도 별점이 유지된다. | ||
| - local storage를 사용하되, 추후에 API 요청으로 갈아끼울 수 있는 형태로 만든다. | ||
|
|
||
| ## 3. 반응형 UI | ||
|
|
||
| ## 4. 무한스크롤 | ||
|
|
||
| - 기존 더보기 버튼이 사라지고 무한 스크롤 형태로 변경 | ||
| - 사용자가 브라우저 화면의 끝에 도달하면 그 다음 20개의 목록을 서버에 요청하여 추가한다. | ||
|
|
||
| </br> | ||
| </br> | ||
| </br> | ||
|
|
||
| # 테스트 명세 | ||
|
|
||
| ## E2E 시나리오 | ||
|
|
||
| ### 최초 진입 시 | ||
|
|
||
| 1. 배너에 첫 번째 인기 영화의 정보가 표시된다. | ||
| 2. 최초 진입 시 인기 영화 20개가 표시된다. | ||
| 3. 스크롤을 내리면 다음 20개가 자동으로 추가된다. | ||
| 4. 연속 스크롤 시 다음 페이지가 순차적으로 추가된다. | ||
| 5. 영화를 클릭하면 모달이 열린다. | ||
| - 클릭 즉시 모달이 열리고, API 응답 전까지 콘텐츠가 비어있다. | ||
| - API 응답 후 모달에 제목, 카테고리, 평점, 줄거리가 채워진다. | ||
| - 모달이 열리면 body 스크롤이 방지된다. | ||
| - X 버튼을 누르면 모달이 닫힌다. | ||
| - ESC 키를 누르면 모달이 닫힌다. | ||
| - 배경(딤 영역)을 클릭하면 모달이 닫힌다. | ||
| 6. 반응형 레이아웃 | ||
| - 모바일(375px)에서 영화 목록이 1열로 표시된다. | ||
| - 태블릿(900px)에서 영화 목록이 3열로 표시된다. | ||
| - 데스크톱(1200px)에서 영화 목록이 4열로 표시된다. | ||
| - 와이드(1600px)에서 영화 목록이 5열로 표시된다. | ||
| - 모바일(375px)에서 모달의 포스터 이미지가 숨겨진다. | ||
| - 태블릿(900px)에서 모달이 하단 시트 형태로 표시된다. | ||
|
|
||
| ### 검색 시 | ||
|
|
||
| 1. "인사이드"를 검색하면 관련 영화가 최대 20개 표시된다. | ||
| 2. 스크롤을 내리면 다음 검색 결과가 자동으로 추가된다. | ||
| 3. 마지막 페이지까지 불러오면 더 이상 요청이 발생하지 않는다. | ||
| 4. 검색 결과가 없으면 안내 메시지를 아이콘과 함께 표시한다. | ||
| 5. 로고를 누르면 메인 화면으로 돌아온다. | ||
| 6. 검색 결과에서 영화를 클릭하면 모달이 열린다. | ||
| - 모달에 영화 정보가 올바르게 표시된다. | ||
| - 모달을 닫고 검색 결과 화면으로 돌아올 수 있다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,79 +1,228 @@ | ||
| describe("메인 화면 최초 진입 시나리오 테스트", () => { | ||
| // TMDB API 응답 형식을 생성하는 헬퍼 함수 | ||
| const mockMovies = ( | ||
| count: number, | ||
| titlePrefix: string, | ||
| page: number, | ||
| totalPages: number, | ||
| ) => ({ | ||
| results: Array.from({ length: count }, (_, i) => ({ | ||
| id: i + (page - 1) * 20, | ||
| title: `${titlePrefix} ${i + 1}`, | ||
| poster_path: "/63In39uCc7769Y0667vCInth6Uv.jpg", | ||
| vote_average: 8.5, | ||
| })), | ||
| page: page, | ||
| total_pages: totalPages, | ||
| }); | ||
| const mockMovies = (count: number, page: number, totalPages: number) => ({ | ||
| results: Array.from({ length: count }, (_, i) => ({ | ||
| id: i + (page - 1) * 20 + 1, | ||
| title: `인기 영화 ${i + (page - 1) * 20 + 1}`, | ||
| poster_path: "/63In39uCc7769Y0667vCInth6Uv.jpg", | ||
| vote_average: 8.5, | ||
| })), | ||
| page, | ||
| total_pages: totalPages, | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| // 1페이지 호출 모킹 (20개 응답, 총 2페이지가 있다고 가정) | ||
| cy.intercept( | ||
| "GET", | ||
| "**/movie/popular?*page=1*", | ||
| mockMovies(20, "인기 영화", 1, 2), | ||
| ).as("getPopularP1"); | ||
|
|
||
| // 2페이지 호출 모킹 (마지막 페이지) | ||
| cy.intercept( | ||
| "GET", | ||
| "**/movie/popular?*page=2*", | ||
| mockMovies(20, "인기 영화", 2, 2), | ||
| ).as("getPopularP2"); | ||
| const mockMovieDetail = { | ||
| id: 1, | ||
| title: "인기 영화 1", | ||
| overview: "줄거리", | ||
| vote_average: 8.5, | ||
| poster_path: "/63In39uCc7769Y0667vCInth6Uv.jpg", | ||
| release_date: "2024-06-14", | ||
| genres: [{ name: "애니메이션" }, { name: "가족" }], | ||
| }; | ||
|
|
||
| describe("최초 진입 시나리오 테스트", () => { | ||
| beforeEach(() => { | ||
| cy.intercept("GET", "**/movie/popular?*page=1*", mockMovies(20, 1, 3)).as( | ||
| "getPage1", | ||
| ); | ||
| cy.intercept("GET", "**/movie/popular?*page=2*", mockMovies(20, 2, 3)).as( | ||
| "getPage2", | ||
| ); | ||
| cy.intercept("GET", "**/movie/popular?*page=3*", mockMovies(10, 3, 3)).as( | ||
| "getPage3", | ||
| ); | ||
| cy.visit("http://localhost:5173/"); | ||
| }); | ||
|
|
||
| it("1. 배너에 첫 번째 인기 영화의 정보가 표시된다.", () => { | ||
| cy.wait("@getPopularP1"); | ||
| cy.wait("@getPage1"); | ||
|
|
||
| // 리스트의 첫 번째 영화 제목을 가져와서 배너(.title)와 비교 | ||
| cy.get(".thumbnail-list li:first-child", { timeout: 10000 }) | ||
| cy.get(".thumbnail-list li:first-child") | ||
| .find("strong") | ||
| .invoke("text") | ||
| .then((firstMovieTitle) => { | ||
| cy.get(".title").invoke("text").should("eq", firstMovieTitle); | ||
| }); | ||
| }); | ||
|
|
||
| it("2. 최초 진입 시 인기 영화 최대 20개가 표시된다.", () => { | ||
| cy.wait("@getPopularP1"); | ||
| it("2. 최초 진입 시 인기 영화 20개가 표시된다.", () => { | ||
| cy.wait("@getPage1"); | ||
|
|
||
| // 리스트 아이템 개수 확인 | ||
| cy.get(".thumbnail-list li").should("have.length", 20); | ||
| }); | ||
|
|
||
| it("3. 더 보기 버튼을 누르면 최대 20개가 추가된다.", () => { | ||
| cy.wait("@getPopularP1"); | ||
|
|
||
| // 초기 20개 확인 후 버튼 클릭 | ||
| it("3. 스크롤을 내리면 다음 20개가 자동으로 추가된다.", () => { | ||
| cy.wait("@getPage1"); | ||
| cy.get(".thumbnail-list li").should("have.length", 20); | ||
| cy.get(".more-button").click(); | ||
|
|
||
| cy.wait("@getPopularP2"); | ||
| cy.scrollTo("bottom"); | ||
| cy.wait("@getPage2"); | ||
|
|
||
| // 추가되어 총 40개가 되었는지 확인 | ||
| cy.get(".thumbnail-list li").should("have.length", 40); | ||
| }); | ||
|
|
||
| it("4. 더 이상 보여줄 영화가 없으면 더 보기 버튼이 사라진다.", () => { | ||
| cy.wait("@getPopularP1"); | ||
| it("4. 연속 스크롤 시 다음 페이지가 순차적으로 추가된다.", () => { | ||
| cy.wait("@getPage1"); | ||
|
|
||
| cy.scrollTo("bottom"); | ||
| cy.wait("@getPage2"); | ||
|
|
||
| cy.scrollTo("bottom"); | ||
| cy.wait("@getPage3"); | ||
|
|
||
| cy.get(".thumbnail-list li").should("have.length", 50); | ||
| }); | ||
|
|
||
| describe("5. 영화를 클릭하면 모달이 열린다.", () => { | ||
| beforeEach(() => { | ||
| cy.wait("@getPage1"); | ||
| cy.intercept("GET", "**/movie/*?language=ko-KR", { | ||
| delay: 500, | ||
| body: mockMovieDetail, | ||
| }).as("getDetail"); | ||
| cy.get(".thumbnail-list .item").first().click(); | ||
| }); | ||
|
|
||
| it("클릭 즉시 모달이 열리고, API 응답 전까지 콘텐츠가 비어있다.", () => { | ||
| cy.get(".modal-background").should("have.class", "active"); | ||
| cy.get(".modal-header h2").should("have.text", ""); | ||
| }); | ||
|
|
||
| it("API 응답 후 모달에 제목, 카테고리, 평점, 줄거리가 채워진다.", () => { | ||
| cy.wait("@getDetail"); | ||
|
|
||
| cy.get(".modal-header h2").should( | ||
| "have.text", | ||
| mockMovieDetail.title, | ||
| ); | ||
| cy.get(".modal-description .category") | ||
| .should("contain.text", "2024") | ||
| .and("contain.text", "애니메이션"); | ||
| cy.get(".modal-description .rate span").should( | ||
| "have.text", | ||
| String(mockMovieDetail.vote_average), | ||
| ); | ||
| cy.get(".modal-description .detail").should( | ||
| "have.text", | ||
| mockMovieDetail.overview, | ||
| ); | ||
| }); | ||
|
|
||
| it("모달이 열리면 body 스크롤이 방지된다.", () => { | ||
| cy.get("body").should("have.css", "overflow", "hidden"); | ||
| }); | ||
|
|
||
| it("X 버튼을 누르면 모달이 닫힌다.", () => { | ||
| cy.wait("@getDetail"); | ||
| cy.get(".close-modal").click(); | ||
|
|
||
| cy.get(".modal-background").should("not.have.class", "active"); | ||
| cy.get("body").should("not.have.css", "overflow", "hidden"); | ||
| }); | ||
|
|
||
| it("ESC 키를 누르면 모달이 닫힌다.", () => { | ||
| cy.wait("@getDetail"); | ||
| cy.get("body").type("{esc}"); | ||
|
|
||
| cy.get(".modal-background").should("not.have.class", "active"); | ||
| }); | ||
|
|
||
| it("배경(딤 영역)을 클릭하면 모달이 닫힌다.", () => { | ||
| cy.wait("@getDetail"); | ||
| cy.get(".modal-background").click({ force: true }); | ||
|
|
||
| cy.get(".modal-background").should("not.have.class", "active"); | ||
| }); | ||
| }); | ||
|
|
||
| // 마지막 페이지(2페이지)를 불러오도록 버튼 클릭 | ||
| cy.get(".more-button").click(); | ||
| cy.wait("@getPopularP2"); | ||
| describe("6. 반응형 레이아웃", () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반응형 레이아웃 테스트까지 ㄷㄷ |
||
| beforeEach(() => { | ||
| cy.wait("@getPage1"); | ||
| }); | ||
|
|
||
| // 소스 코드 로직(nowPage === totalPages)에 따라 버튼이 숨겨져야 함 | ||
| cy.get(".more-button").should("not.be.visible"); | ||
| it("모바일(375px)에서 영화 목록이 1열로 표시된다.", () => { | ||
| cy.viewport(375, 812); | ||
|
|
||
| cy.get(".thumbnail-list li") | ||
| .eq(0) | ||
| .then(($first) => { | ||
| cy.get(".thumbnail-list li") | ||
| .eq(1) | ||
| .then(($second) => { | ||
| expect($second[0].getBoundingClientRect().top).to.be.greaterThan( | ||
| $first[0].getBoundingClientRect().top, | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it("태블릿(900px)에서 영화 목록이 3열로 표시된다.", () => { | ||
| cy.viewport(900, 900); | ||
|
|
||
| cy.get(".thumbnail-list li").then(($items) => { | ||
| const firstTop = $items[0].getBoundingClientRect().top; | ||
| const thirdTop = $items[2].getBoundingClientRect().top; | ||
| const fourthTop = $items[3].getBoundingClientRect().top; | ||
|
|
||
| expect(thirdTop).to.be.closeTo(firstTop, 5); | ||
| expect(fourthTop).to.be.greaterThan(firstTop); | ||
| }); | ||
| }); | ||
|
|
||
| it("데스크톱(1200px)에서 영화 목록이 4열로 표시된다.", () => { | ||
| cy.viewport(1200, 900); | ||
|
|
||
| cy.get(".thumbnail-list li").then(($items) => { | ||
| const firstTop = $items[0].getBoundingClientRect().top; | ||
| const fourthTop = $items[3].getBoundingClientRect().top; | ||
| const fifthTop = $items[4].getBoundingClientRect().top; | ||
|
|
||
| expect(fourthTop).to.be.closeTo(firstTop, 5); | ||
| expect(fifthTop).to.be.greaterThan(firstTop); | ||
| }); | ||
| }); | ||
|
|
||
| it("와이드(1600px)에서 영화 목록이 5열로 표시된다.", () => { | ||
| cy.viewport(1600, 900); | ||
|
|
||
| cy.get(".thumbnail-list li").then(($items) => { | ||
| const firstTop = $items[0].getBoundingClientRect().top; | ||
| const fifthTop = $items[4].getBoundingClientRect().top; | ||
| const sixthTop = $items[5].getBoundingClientRect().top; | ||
|
|
||
| expect(fifthTop).to.be.closeTo(firstTop, 5); | ||
| expect(sixthTop).to.be.greaterThan(firstTop); | ||
| }); | ||
| }); | ||
|
|
||
| it("모바일(375px)에서 모달의 포스터 이미지가 숨겨진다.", () => { | ||
| cy.viewport(375, 812); | ||
|
|
||
| cy.intercept("GET", "**/movie/*?language=ko-KR", mockMovieDetail).as( | ||
| "getDetail", | ||
| ); | ||
| cy.get(".thumbnail-list .item").first().click(); | ||
| cy.wait("@getDetail"); | ||
|
|
||
| cy.get(".modal-image").should("not.be.visible"); | ||
| }); | ||
|
|
||
| it("태블릿(900px)에서 모달이 하단 시트 형태로 표시된다.", () => { | ||
| cy.viewport(900, 900); | ||
|
|
||
| cy.intercept("GET", "**/movie/*?language=ko-KR", mockMovieDetail).as( | ||
| "getDetail", | ||
| ); | ||
| cy.get(".thumbnail-list .item").first().click(); | ||
| cy.wait("@getDetail"); | ||
|
|
||
| cy.get(".modal").then(($modal) => { | ||
| cy.window().then((win) => { | ||
| expect($modal[0].getBoundingClientRect().bottom).to.be.closeTo( | ||
| win.innerHeight, | ||
| 5, | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
초기 로딩, 무한스크롤, 모달, 반응형 레이아웃까지 매우 잘 커버하고 있네요👍