Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
55077e5
docs: 리드미 기능 목록 작성
yundol777 Mar 31, 2026
3047053
chore: cypress 초기 설정
yundol777 Mar 31, 2026
035c2bd
docs: 리드미 기능 목록 추가
yundol777 Mar 31, 2026
8e8ca7f
test: 영화 목록 데이터 가져오기 테스트
yundol777 Mar 31, 2026
0485407
feat: 인기있는 영화 목록 api 호출 함수 구현
yundol777 Mar 31, 2026
77ac51b
feat: api 호출 타입 추가
yundol777 Mar 31, 2026
d8c538b
feat: 메인 UI 리스트 렌더링 및 베너 생성
yundol777 Mar 31, 2026
4a7aa14
test: 검색한 영화 목록 가져오는 테스트 추가
yundol777 Mar 31, 2026
6b0ea86
feat: 입력 값으로 검색 된 영화 목록 렌더링
yundol777 Mar 31, 2026
71bf185
style: 검색창 스타일 및 레이아웃 변경
yundol777 Apr 1, 2026
fff280c
refactor: 파일 구조 분리
yundol777 Apr 1, 2026
baf93ab
feat: 빈 리스트 출력 뷰 추가
yundol777 Apr 1, 2026
2b23868
feat: api 에러 처리 추가
yundol777 Apr 1, 2026
8a5277e
feat: 이미지 에러 처리 추가
yundol777 Apr 1, 2026
4fd2a35
feat: 빈 리스트 및 에러 리스트 출력 시 더보기 버튼 제거
yundol777 Apr 1, 2026
14e0dd6
refacotr: 파일 구조 변경
yundol777 Apr 1, 2026
9bbe7de
fix: 사라진 버튼이 재검색 시 안생기는 오류 수정
yundol777 Apr 1, 2026
fffdec3
feat: 스켈레톤 ui 구현 및 연동
yundol777 Apr 1, 2026
5735ebc
refactor: 파일 구조 변경
yundol777 Apr 2, 2026
27c9c42
style: 에러 출력 UI 스타일 변경
yundol777 Apr 2, 2026
be68938
test: cypress E2E 통합 테스트 추가
yundol777 Apr 2, 2026
fc4607f
docs: 리드미 수정
yundol777 Apr 2, 2026
b8835d9
chore: cypress 타입 설정 추가
yundol777 Apr 2, 2026
1df96e8
refactor: 요소 속성 수정
yundol777 Apr 2, 2026
07ada58
refactor: 이미지 및 css 파일 이동
yundol777 Apr 2, 2026
7e3c3d3
refactor: 상태 객체 생성
yundol777 Apr 2, 2026
e0b74fb
refactor: 숫자 상수 처리
yundol777 Apr 2, 2026
1da63c5
refactor: 파일 구조 분리
yundol777 Apr 2, 2026
3c7ce0a
fix: 깃허브 액션을 이용해 토큰 연결
yundol777 Apr 2, 2026
f971c97
fix: 이미지 경로 수정
yundol777 Apr 2, 2026
a139cbe
fix: 이미지 경로 수정
yundol777 Apr 2, 2026
6a686c1
fix: 에러 처리 로직 수정
yundol777 Apr 6, 2026
8f68000
refactor: main, view 파일 구조 분리
yundol777 Apr 7, 2026
f78362d
refactor: state 상태 구조 분리
yundol777 Apr 7, 2026
8facdcd
refactor: 더보기 버튼 구조 개선
yundol777 Apr 7, 2026
588b4ef
refactor: 에러 메세지 구조 개선
yundol777 Apr 7, 2026
4b4ab53
fix: 에러 처리 시 더보기 버튼 제거
yundol777 Apr 7, 2026
107536b
fix: 배포 시 이미지 깨지는 오류 해결
yundol777 Apr 7, 2026
c355424
fix: 중복 클릭 방지 수정
yundol777 Apr 8, 2026
6b6b4e0
feat: 오류 시 더 보기 버튼 제거
yundol777 Apr 8, 2026
a7976c1
docs: 추가 구현 사항 리드미 추가
yundol777 Apr 8, 2026
1577564
feat: 영화 항목 클릭 시 모달 UI 출력
yundol777 Apr 9, 2026
5ba2a00
feat: esc 입력 시 모달 UI 숨김
yundol777 Apr 9, 2026
42b64cc
feat: api를 호출하고 상세 정보를 출력한다.
yundol777 Apr 9, 2026
73854ae
feat: 별점을 눌렀을 때 해당 별점을 저장 및 출력
yundol777 Apr 12, 2026
7e31518
docs: 기능 목록 추가
yundol777 Apr 12, 2026
2abb244
styel: 모달 스타일 적용
yundol777 Apr 12, 2026
7cb4a74
feat: 더보기 버튼 무한 스크롤로 대체
yundol777 Apr 12, 2026
9e2d6b0
feat: 모달 상세 정보 스켈레톤 UI 구현
yundol777 Apr 12, 2026
68ed04d
feat: 상세 정보 fetch 실패 시, 에러 UI 모달 출력
yundol777 Apr 12, 2026
1d281a9
feat: 모달 바깥 영역 클릭 시 모달 닫힘 기능 추가
yundol777 Apr 12, 2026
d1d8679
style: 반응형 UI 추가 및 모달 로딩 처리 변경
yundol777 Apr 13, 2026
4a220f2
feat: 배너 버튼 기능 연결
yundol777 Apr 13, 2026
bd610e9
test: e2e 테스트 수정 및 추가
yundol777 Apr 13, 2026
f5c7589
refactor: 컨트롤러 에러 처리 중복 분리
yundol777 Apr 13, 2026
8885cc8
refactor: 오타 수정
yundol777 Apr 13, 2026
c6d7e20
feat: 로고 클릭 시 새로고침 기능 추가
yundol777 Apr 13, 2026
d1fb715
merge: keep local changes over upstream
yundol777 Apr 13, 2026
23b153e
fix: 배포 시 이미지 깨지는 오류 해결
yundol777 Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
- [x] 지금 인기 있는 영화 목록 20개를 보여준다.
- [x] 제일 먼저 있는 영화를 배너로 띄운다.
- [x] 더보기 버튼을 누르면 20개의 영화 목록을 추가로 불러온다.
- [x] 더보기 버튼을 무한 스크롤로 대체한다.
- [x] 불러올 영화 목록이 없으면 더보기 버튼이 사라진다.
- [x] 영화 데이터를 받아오기 전에 스켈레톤 UI를 먼저 띄운다.

## 검색 시 UI

Expand All @@ -17,18 +19,32 @@
- [x] 해당 검색어에 대한 영화 목록 20개를 보여준다.
- [x] 검색어를 검색 제목에 띄운다.
- [x] 더보기 버튼을 누르면 20개의 검색과 관련된 영화 목록을 추가로 불러온다.
- [x] 더보기 버튼을 무한 스크롤로 대체한다.
- [x] 불러올 영화 목록이 없으면 더보기 버튼이 사라진다.
- [x] 검색 결과가 없으면 `검색 결과가 없습니다.` 라는 문구를 띄운다.
- [x] 영화 데이터를 받아오기 전에 스켈레톤 UI를 먼저 띄운다.

## 모달 UI

- [x] 영화 항목을 선택하면 모달 UI를 띄운다.
- [x] 선택한 영화의 상세 정보 api를 호출한다.
- [x] 해당 영화에 대한 상세 정보를 보여준다.
- [x] 별점을 누르면 해당 별점에 맞게 나의 점수가 저장된다.
- [x] x 를 누르면 모달이 닫힌다.
- [x] esc 를 누르면 모달이 닫힌다.
- [x] 모달 바깥을 누르면 모달이 닫힌다.
- [x] 영화 데이터를 받아오기 전에 스켈레톤 UI를 먼저 띄운다.

## 예외 처리

- [x] 기본 UI에서 영화 정보를 불러오지 못했다면 `영화 정보를 불러오지 못했습니다. 다시 시도해주세요.` 라는 문구를 띄운다.
- [x] 영화 이미지를 불러오지 못했다면 `No Image` 이미지를 띄운다.
- [x] 검색 시, 영화 정보를 불러오지 못했다면 `영화 검색 결과를 불러오지 못했습니다. 다시 시도해주세요.` 라는 문구를 띄운다.
- [x] 영화 상세 정보를 불러올 시, 정보를 불러오지 못했다면 에러 UI를 출력한다.

## 공통

- [x] 영화 데이터를 받아오기 전에 스켈레톤 UI를 먼저 띄운다.
- [ ] pc, 태블릿, 휴대폰에서 사용가능하도록 반응형 UI를 만든다.

## API 명세

Expand Down
149 changes: 70 additions & 79 deletions cypress/e2e/home.cy.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,95 @@
describe("홈 화면 테스트", () => {
beforeEach(() => {
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=1").as(
"getMovies",
);
cy.visit("http://localhost:5173");
});
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=1", {
statusCode: 200,
body: {
page: 1,
results: Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: `영화 ${index + 1}`,
poster_path: `/poster-${index + 1}.jpg`,
vote_average: 7.5,
})),
total_pages: 2,
total_results: 40,
},
}).as("page1");

it("API 호출 확인", () => {
cy.wait("@getMovies").its("response.body.results").should("be.an", "array");
cy.visit("http://localhost:5173");
});

it("배너 안의 요소를 확인", () => {
cy.wait("@getMovies")
.its("response.body.results")
.then((results) => {
cy.get(".title").should("contain", results[0].title);
cy.get(".rate-value").should("contain", results[0].vote_average);
cy.get(".background-container")
.invoke("css", "background-image")
.should("include", results[0].poster_path);
});
it("초기 진입 시 영화 목록이 렌더된다.", () => {
cy.wait("@page1");
cy.get(".thumbnail-item").should("have.length", 20);
});

it("리스트 안의 요소를 확인", () => {
cy.wait("@getMovies")
.its("response.body.results")
.then((results) => {
cy.get(".thumbnail").each(($el, index) => {
cy.wrap($el)
.should("have.attr", "src")
.and("include", results[index].poster_path);
});
it("초기 진입 시 베너가 렌더된다.", () => {
cy.wait("@page1");

cy.get(".item-rate").each(($el, index) => {
cy.wrap($el).should("contain", results[index].vote_average);
});

cy.get(".item-title").each(($el, index) => {
cy.wrap($el).should("contain", results[index].title);
});
});
cy.get(".top-rated-movie").should("be.visible");
cy.get(".title").should("contain", "영화 1");
cy.get(".rate-value").should("contain", "7.5");
});

it("더보기 버튼이 있는지 확인", () => {
cy.get(".thumbnail-add-button").should("exist");
it("배너의 자세히 보기 버튼 클릭 시 모달이 열린다.", () => {
cy.wait("@page1");

cy.get(".top-rated-movie .detail").click();
cy.get(".modal-background").should("have.class", "active");
cy.get(".modal").should("be.visible");
Comment on lines +34 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

printf '\n== cypress/e2e/home.cy.ts intercept 목록 ==\n'
rg -n --type=ts 'cy\.intercept\(' cypress/e2e/home.cy.ts

printf '\n== 모달/상세 fetch 관련 코드 ==\n'
rg -n --type=ts 'getDetail|movieDetail|modalController|/movie/' src cypress/e2e/home.cy.ts cypress/e2e/modal.cy.ts

Repository: woowacourse/javascript-movie-review

Length of output: 1900


모달 오픈 테스트에서 상세 정보 API를 stub하지 않고 있습니다.

이 테스트는 ".detail" 버튼 클릭 시 모달을 여는 것을 검증하지만, 모달 열기는 modalControllergetDetail 함수를 통해 /movie/{id} 엔드포인트로 실제 API 요청을 보냅니다. 인기 목록(/movie/popular)만 stub하고 상세 정보 요청을 stub하지 않으면, 테스트가 네트워크 지연이나 응답 시간에 따라 불안정해질 수 있습니다.

cypress/e2e/modal.cy.ts를 보면 동일한 상세 정보 요청을 line 18에서 cy.intercept("GET", "**/movie/1?language=ko-KR&region=KR", { ... }) 방식으로 stub하고 있습니다. 현재 코드 (lines 34-39, 78-83)에서도 같은 방식으로 상세 정보 엔드포인트를 stub하도록 추가해야 테스트의 안정성을 높일 수 있습니다. modal.cy.ts의 stub 패턴을 참고하여 어떤 요청을 어떻게 고정해야 할지 검토해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypress/e2e/home.cy.ts` around lines 34 - 39, The test opens the modal by
clicking ".detail" which triggers modalController -> getDetail to call the
/movie/{id} API but you only stubbed the popular list; add a cy.intercept for
the detail endpoint (same pattern used in modal.cy.ts: cy.intercept("GET",
"**/movie/1?language=ko-KR&region=KR", {...})) before clicking the ".detail"
button so the detail request is stubbed and the assertions on
".modal-background" and ".modal" become deterministic.

});
});

describe("더보기 버튼 테스트", () => {
describe("영화 리스트 기능 테스트", () => {
beforeEach(() => {
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=1").as(
"page1",
);
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=2").as(
"page2",
);
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=1", {
statusCode: 200,
body: {
page: 1,
results: Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: `영화 ${index + 1}`,
poster_path: `/poster-${index + 1}.jpg`,
vote_average: 7.5,
})),
total_pages: 2,
total_results: 40,
},
}).as("page1");

cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=2", {
statusCode: 200,
body: {
page: 2,
results: Array.from({ length: 20 }, (_, index) => ({
id: index + 21,
title: `영화 ${index + 21}`,
poster_path: `/poster-${index + 21}.jpg`,
vote_average: 8.1,
})),
total_pages: 2,
total_results: 40,
},
}).as("page2");

cy.visit("http://localhost:5173");
});

it("버튼 클릭 시 API 호출 확인", () => {
cy.get(".thumbnail-add-button").click();
it("영화 카드 클릭 시 모달이 열린다.", () => {
cy.wait("@page1");

cy.wait("@page2").its("response.body.results").should("be.an", "array");
cy.get(".item").first().click();
cy.get(".modal-background").should("have.class", "active");
cy.get(".modal").should("be.visible");
});

it("리스트 안의 요소를 확인", () => {
let page1Results: Movies[];

cy.wait("@page1")
.its("response.body.results")
.then((results1) => {
page1Results = results1;
});

cy.get(".thumbnail-add-button").click();

cy.wait("@page2")
.its("response.body.results")
.then((results2) => {
const allResults = [...page1Results, ...results2];

cy.get(".thumbnail").should("have.length", allResults.length);

cy.get(".thumbnail").each(($el, index) => {
cy.wrap($el)
.should("have.attr", "src")
.and("include", allResults[index].poster_path);
});
it("무한 스크롤 시 다음 페이지가 붙는다.", () => {
cy.wait("@page1");
cy.get(".thumbnail-item").should("have.length", 20);

cy.get(".item-title").each(($el, index) => {
cy.wrap($el).should("contain", allResults[index].title);
});
cy.get("#sentinel").scrollIntoView();
cy.wait("@page2");

cy.get(".item-rate").each(($el, index) => {
cy.wrap($el).should(
"contain",
String(allResults[index].vote_average),
);
});
});
cy.get(".thumbnail-item").should("have.length", 40);
});
});
75 changes: 75 additions & 0 deletions cypress/e2e/modal.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
describe("모달 테스트", () => {
beforeEach(() => {
cy.intercept("GET", "**/movie/popular?language=ko-KR&region=KR&page=1", {
statusCode: 200,
body: {
page: 1,
results: Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: `영화 ${index + 1}`,
poster_path: `/poster-${index + 1}.jpg`,
vote_average: 7.5,
})),
total_pages: 2,
total_results: 40,
},
}).as("page1");

cy.intercept("GET", "**/movie/1?language=ko-KR&region=KR", {
statusCode: 200,
body: {
id: 1,
title: "영화 1",
poster_path: "/poster-1.jpg",
vote_average: 7.5,
release_date: "2024-01-01",
genres: [
{ id: 1, name: "액션" },
{ id: 2, name: "모험" },
],
overview: "영화 1의 줄거리입니다.",
},
}).as("movieDetail");

cy.visit("http://localhost:5173");
cy.wait("@page1");
cy.get(".top-rated-movie .detail").click();
cy.wait("@movieDetail");
});

it("모달이 열리면 상세정보가 렌더된다.", () => {
cy.get(".modal-background").should("have.class", "active");
cy.get(".modal").should("be.visible");
cy.get(".modal .modal-title-section h2").should("contain", "영화 1");
cy.get(".modal .detail").should("contain", "영화 1의 줄거리입니다.");
});

it("x 버튼을 누르면 모달이 닫힌다.", () => {
cy.get(".close-modal").click();
cy.get(".modal-background").should("not.have.class", "active");
cy.get(".modal").should("not.be.visible");
});

it("esc 버튼을 누르면 모달이 닫힌다.", () => {
cy.get("body").type("{esc}");
cy.get(".modal-background").should("not.have.class", "active");
cy.get(".modal").should("not.be.visible");
});

it("모달 바깥을 누르면 모달이 닫힌다.", () => {
cy.get(".modal-background").click("topLeft");
cy.get(".modal-background").should("not.have.class", "active");
cy.get(".modal").should("not.be.visible");
});

it("처음 모달이 열리면 별점 초기값이 보인다.", () => {
cy.get(".myrate-comment").should("contain", "별점을 입력해주세요");
cy.get(".myrate-score").should("contain", "(0/10)");
});

it("3번째 별을 클릭하면 별점이 반영된다.", () => {
cy.get(".myrate-stars img").eq(2).click();
cy.get(".myrate-comment").should("contain", "보통이에요");
cy.get(".myrate-score").should("contain", "(6/10)");
});
});
Loading