Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
39dafc9
docs(readme): 기능 요구사항 초안 작성
bigcloud07 Mar 31, 2026
b3296d5
docs(readme): 서비스 전체 시나리오 작성
bigcloud07 Mar 31, 2026
ab1d8e0
docs(readme): 시나리오1 수정
bigcloud07 Mar 31, 2026
50cb4e5
test: 사용 시나리오 의사코드 작성
bigcloud07 Mar 31, 2026
6b8c140
feat: index.html 템플릿 파일 적용
bigcloud07 Mar 31, 2026
ea14686
test: E2E 테스트 작성완료
bigcloud07 Mar 31, 2026
a4d44aa
feat: 영화 목록 조회 기능 추가
bigcloud07 Apr 1, 2026
236c392
refactor: 영화 목록 조회 기능 1차 리팩토링
bel1c10ud Apr 1, 2026
4cee2c7
feat: 영화 검색 기능 추가
bel1c10ud Apr 2, 2026
aa8e270
test: e2e 테스트 문법 에러 수정
bel1c10ud Apr 2, 2026
71d65a9
docs(readme): 구현 현황 업데이트
bel1c10ud Apr 2, 2026
8d21654
feat: 토스트 기능 추가
bel1c10ud Apr 2, 2026
6efc6e7
feat: API 응답 시간이 길어지는 경우 예외를 발생시키는 동작 추가
bel1c10ud Apr 2, 2026
e9f4f06
docs(readme): API 요청에 실패하거나 응답이 길어져서 실패하는 경우에 대한 시나리오 추가
bel1c10ud Apr 2, 2026
80ee884
test: API 응답에 실패하거나 응답이 길어져 실패하는 경우에 대한 모킹 및 테스트 추가
bel1c10ud Apr 2, 2026
071708c
refactor: 하드코딩된 URL 환경 변수로 분리
bel1c10ud Apr 2, 2026
bbb2389
feat: 더보기 버튼을 짧은 주기로 누르는 경우 무시하도록 쓰로틀 추가
bel1c10ud Apr 2, 2026
60aebbe
chore: 파비콘 추가
bel1c10ud Apr 2, 2026
487ba7c
fix: 배포 환경 대응
bel1c10ud Apr 2, 2026
89a4dca
fix: 로컬 환경에서 평점 아이콘이 정상적으로 출력되지 않는 문제 해결
bel1c10ud Apr 2, 2026
8378310
fix: 배포 환경에 search 페이지가 포함되지 않는 문제 해결
bel1c10ud Apr 2, 2026
3cd1d57
fix: 이미지에 base url이 적용되어있지 않았던 부분 수정
bel1c10ud Apr 2, 2026
3aa31f9
fix: 잘못된 mock 반환값 수정
bel1c10ud Apr 5, 2026
379526d
fix: 누락된 img alt 추가
bel1c10ud Apr 5, 2026
259e7bb
fix: 잘못된 마크업 구조 개선
bel1c10ud Apr 5, 2026
110e656
fix: 오타 수정
bel1c10ud Apr 5, 2026
0e05eeb
feat: 커스텀 에러 클래스 도입 및 에러 핸들링 개선
bel1c10ud Apr 5, 2026
4072641
test: 변경된 에러 형식에 대응하도록 테스트 수정
bel1c10ud Apr 5, 2026
a25542c
docs(readme): 구현 현황 업데이트
bel1c10ud Apr 5, 2026
425ace6
fix: API 요청 실패시 skeleton이 사라지지 않는 문제 해결
bel1c10ud Apr 5, 2026
fbca497
test: 더보기 버튼과 Skeleton UI 테스트 추가
bel1c10ud Apr 5, 2026
f519882
refactor: render 관련 유틸 함수 분리
bel1c10ud Apr 5, 2026
391401b
chore: 함수 이름과 일치하지 않는 파일 이름 수정
bel1c10ud Apr 5, 2026
07ced67
refactor: 에러 핸들링 개선
bel1c10ud Apr 10, 2026
99b0ae3
docs(readme): 2단계 기능 요구사항 작성
bel1c10ud Apr 11, 2026
f740937
feat: 영화 상세 정보 모달 구현
bel1c10ud Apr 11, 2026
b23b175
feat: 별점 매기기 기능 구현
bel1c10ud Apr 11, 2026
4b01e75
feat: 최상단 영화 클릭시 모달이 띄워지는 기능 추가
bel1c10ud Apr 11, 2026
ed8013b
feat: 영화 목록 무한 스크롤 적용
bel1c10ud Apr 11, 2026
b4667b5
feat: 반응형 디자인 적용
bel1c10ud Apr 11, 2026
543aa6a
feat: 양방향 무한 스크롤 구현
bel1c10ud Apr 12, 2026
c9f8c24
feat: 영화 상세 정보 모달 비동기 로딩 스피너 추가
bel1c10ud Apr 12, 2026
5294f49
feat: 평점 메시지 추가
bel1c10ud Apr 12, 2026
86d6791
fix: 최상단 영화가 정상적으로 갱신되지않는 문제 해결
bel1c10ud Apr 12, 2026
ab7c31a
docs(readme): 구현 현황 업데이트
bel1c10ud Apr 12, 2026
feb0712
feat: 상단 무한 스크롤 적용
bel1c10ud Apr 12, 2026
2884bd6
refactor: 반응형 디자인 개선
bel1c10ud Apr 12, 2026
e5ed376
fix: 잘못된 에러 핸들링 및 비동기 처리 수정
bel1c10ud Apr 12, 2026
bf31206
docs(readme): 사용 시나리오 업데이트
bel1c10ud Apr 13, 2026
7373d6c
test: 사용 시나리오 기반으로 E2E 테스트 업데이트
bel1c10ud Apr 13, 2026
567c312
feat: 테스트 환경에서 ESC키를 사용해 모달을 닫을 수 있도록 이벤트 핸들러 추가
bel1c10ud Apr 13, 2026
6c50efd
feat: 모달이 열려있는 경우 스크롤할 수 없도록 처리
bel1c10ud Apr 13, 2026
6aaa165
refactor: 에러 핸들링 개선
bel1c10ud Apr 13, 2026
8995f5e
fix: 모달이 열릴때 스크롤이 상단으로 이동하는 문제 해결
bel1c10ud Apr 13, 2026
af04faf
Merge remote-tracking branch 'upstream/bel1c10ud' into bel1c10ud
bel1c10ud 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
59 changes: 52 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ FE 레벨1 영화 리뷰 미션

# 서비스 전체 시나리오 작성

## [시나리오 1] Movie List 탐색 - 더보기
## [시나리오 1] Movie List 탐색 - 무한 스크롤

User -> Main Page -> 영화 목록이 보인다 -> 더보기를 눌러 더 많은 영화 목록을 불러온다
User -> Main Page -> 영화 목록이 보인다 -> 스크롤하여 페이지 끝에 도달하면 다음 영화 목록을 불러온다

## [시나리오 1-1] Movie List 탐색 - 로딩 중 스켈레톤 UI 표시

User -> Main Page -> 영화 목록이 보인다 -> 더보기를 눌러 더 많은 영화 목록을 불러온다 -> 스켈레톤 UI가 표시된다.
User -> Main Page -> 영화 목록이 보인다 -> 스크롤하여 페이지 끝에 도달하면 다음 영화 목록을 불러온다 -> 스켈레톤 UI가 표시된다.

## [시나리오 1-2] Movie List 탐색 - 마지막 페이지 도달시 더보기 버튼 제거
## [시나리오 1-2] Movie List 탐색 - 마지막 페이지 도달시 추가 로드 없음

User -> Main Page -> 영화 목록이 보인다 -> 더보기를 눌러 더 많은 영화 목록을 불러온다 -> 마지막 페이지 도달 -> 더보기 버튼이 제거된다
User -> Main Page -> 영화 목록이 보인다 -> 스크롤하여 페이지 끝에 도달한다 -> 마지막 페이지 도달 -> 더 이상 영화 목록을 불러오지 않는다

## [시나리오 2] 검색 - 결과 있음

Expand All @@ -45,12 +45,47 @@ User -> Main Page -> API 요청 실패 -> 화면 하단에 "API 에러" 타이

User -> Main Page -> API 요청 -> 응답 대기 시간이 10초를 넘어간다 -> 화면 하단에 "API 에러" 타이틀과 "API 응답 시간이 10000ms를 초과했습니다." 메세지를 담은 토스트를 띄운다.

## [시나리오 6] 영화 상세 정보 조회 - 모달 열기

User -> Main Page -> 영화 카드 클릭 -> 영화 상세 정보가 모달로 표시된다

## [시나리오 6-1] 영화 상세 정보 조회 - 닫기 버튼으로 모달 닫기

User -> Main Page -> 영화 카드 클릭 -> 모달이 표시된다 -> 닫기 버튼 클릭 -> 모달이 닫힌다

## [시나리오 6-2] 영화 상세 정보 조회 - ESC 키로 모달 닫기

User -> Main Page -> 영화 카드 클릭 -> 모달이 표시된다 -> ESC 키 입력 -> 모달이 닫힌다

## [시나리오 7] 별점 매기기

User -> Main Page -> 영화 카드 클릭 -> 모달이 표시된다 -> 별점 선택 -> 별점이 저장된다

## [시나리오 7-1] 별점 유지 - 새로고침 후

User -> Main Page -> 영화 카드 클릭 -> 별점 선택 -> 페이지 새로고침 -> 같은 영화 클릭 -> 이전에 선택한 별점이 유지된다

## [시나리오 8] URL 페이지 파라미터로 접속 - 해당 페이지 로드

User -> URL에 page=2를 포함하여 접속 -> 2페이지의 영화 목록이 표시된다

## [시나리오 8-1] URL 페이지 파라미터 접속 후 상단 무한 스크롤

User -> URL에 page=2를 포함하여 접속 -> 2페이지 영화 목록이 표시된다 -> 상단으로 스크롤 -> 1페이지 영화 목록이 추가로 표시된다

## [시나리오 8-2] URL 페이지 파라미터 접속 후 하단 무한 스크롤 - 마지막 페이지

User -> URL에 page=2(마지막 페이지)를 포함하여 접속 -> 2페이지 영화 목록이 표시된다 -> 하단으로 스크롤 -> 더 이상 영화 목록을 불러오지 않는다

## [시나리오 9] 새로고침 후 스크롤 위치 복원

User -> Main Page -> 영화 목록 탐색 -> 특정 영화가 화면에 들어옴 -> URL에 해당 영화의 ID와 페이지가 기록된다 -> 새로고침 -> 이전에 화면에 보였던 영화 위치로 자동 스크롤된다

# 기능 요구 사항

## 🎬 영화 목록 조회

- [x] 영화 목록의 1페이지를 불러오며 더보기 버튼을 누르면 그 다음의 영화 목록을 불러 올 수 있다.
- [x] 페이지 끝에 도달한 경우에는 더보기 버튼을 화면에 출력하지 않는다.
- [x] 영화 목록의 1페이지를 불러오며 페이지끝에 도달하면 그 다음의 영화 목록을 불러 올 수 있다.
- [x] 영화는 한 번의 요청당 20개씩 영화 목록을 보여준다.
- [x] 영화 목록을 불러오는 동안 Skeleton UI 표시

Expand All @@ -60,6 +95,16 @@ User -> Main Page -> API 요청 -> 응답 대기 시간이 10초를 넘어간다
- [x] 검색 버튼을 클릭하여 검색할 수 있다
- [x] 영화 목록 조회와 같이 검색한 결과에 한해 정보를 보여주는 화면의 요구사항은 동일하다

## 📺 영화 상세 정보 조회

- [x] 영화 포스터나 제목 또는 메인 화면의 자세히 보기를 누른경우 영화 상세 정보를 모달로 출력한다.
- [x] 닫기 버튼 또는 ESC를 누르면 모달이 닫힌다.
- [x] 모달창을 반응형 레이아웃으로 구현한다.
- [x] 사용자는 영화에 대해 별점을 남길 수 있다
- [x] 별점은 5개로 구성되어있으며 한개장 2점이다, 1점 단위는 고려하지 않는다.
- [x] 새로고침하더라도 별점은 유지되어야 한다
- [x] localStorag로 구현하돼 서버 API로 쉽고 안전하게 갈아끼울 수 있는 구조로 개발하여야한다.

## 공통

### 예외처리
Expand Down
112 changes: 99 additions & 13 deletions cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const TMDB_POPULAR_URL_PATTERN = "**/movie/popular**";
const TMDB_SEARCH_URL_PATTERN = "**/search/movie**";
const TMDB_MOVIE_DETAIL_URL_PATTERN = /\/movie\/\d+/;

describe("Movie App", () => {
beforeEach(() => {
Expand All @@ -19,29 +20,28 @@ describe("Movie App", () => {
cy.wait("@popularMovies");
});

context("Movie List 탐색", () => {
it("초기 목록 20개 표시 후 더보기 클릭 시 40개 표시", () => {
context("Movie List 탐색 - 무한 스크롤", () => {
it("초기 목록 20개 표시 후 페이지 끝 스크롤 시 40개 표시", () => {
cy.get(".item").should("have.length", 20);
cy.get(".show-more-button").click();
cy.get(".scroll-area").scrollIntoView();
cy.wait("@popularMovies");
cy.get(".item").should("have.length", 40);
});

it("더보기 클릭 시 로딩 중 스켈레톤 UI 표시", () => {
it("페이지 끝 스크롤 시 로딩 중 스켈레톤 UI 표시", () => {
cy.intercept("GET", TMDB_POPULAR_URL_PATTERN, (req) => {
req.reply({ fixture: "popular-movies-p2.json", delay: 1000 });
}).as("delayedPopularMovies");
cy.get(".show-more-button").click();
cy.get(".scroll-area").scrollIntoView();
cy.get(".skeleton-item").should("exist");
cy.wait("@delayedPopularMovies");
cy.get(".skeleton-item").should("not.exist");
});

it("마지막 페이지 도달 시 더보기 버튼 제거", () => {
cy.get(".show-more-button").should("be.visible");
cy.get(".show-more-button").click();
it("마지막 페이지 도달 시 하단 스크롤 트리거 요소가 제거된다", () => {
cy.get(".scroll-area").scrollIntoView();
cy.wait("@popularMovies");
cy.get(".show-more-button").should("not.exist");
cy.get(".scroll-area").should("not.exist");
});
});

Expand All @@ -60,7 +60,6 @@ describe("Movie App", () => {
cy.get(".search-submit").click();
cy.wait("@searchMovies");
cy.get(".item").its("length").should("be.gte", 1);

cy.get(".search-input").clear().type("Inception");
cy.get(".search-submit").click();
cy.wait("@searchMovies");
Expand Down Expand Up @@ -97,7 +96,6 @@ describe("Movie App", () => {
cy.visit("localhost:5173");
cy.wait("@failedPopularMovies");
cy.get(".toast").should("be.visible");
cy.get(".toast-title").should("contain", "API 에러");
});
});

Expand All @@ -109,8 +107,96 @@ describe("Movie App", () => {
cy.visit("localhost:5173");
cy.tick(10001);
cy.get(".toast").should("be.visible");
cy.get(".toast-title").should("contain", "API 에러");
cy.get(".toast-message").should("contain", "응답 시간");
});
});

context("영화 상세 정보 조회 - 모달", () => {
beforeEach(() => {
cy.intercept("GET", TMDB_MOVIE_DETAIL_URL_PATTERN, { fixture: "movie-detail.json" }).as("movieDetail");
});

it("영화 클릭 시 상세 정보 모달이 표시된다", () => {
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".modal[open]").should("exist");
cy.get(".modal-movie-title").should("exist");
});

it("닫기 버튼 클릭 시 모달이 닫힌다", () => {
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".modal[open]").should("exist");
cy.get(".modal-close-button").click();
cy.get(".modal[open]").should("not.exist");
});

it("ESC 키 입력 시 모달이 닫힌다", () => {
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".modal[open]").should("exist");
cy.get(".modal[open]").trigger("keydown", { key: "Escape", keyCode: 27, which: 27 });
cy.get(".modal[open]").should("not.exist");
});
});

context("별점 매기기", () => {
beforeEach(() => {
cy.intercept("GET", TMDB_MOVIE_DETAIL_URL_PATTERN, { fixture: "movie-detail.json" }).as("movieDetail");
});

it("별점 선택 시 localStorage에 별점이 저장된다", () => {
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".star-button[data-rating='8']").click();
cy.window().then((win) => {
const ratings = JSON.parse(win.localStorage.getItem("my-ratings") ?? "{}");
expect(ratings[1523145]).to.equal(8);
});
});

it("새로고침 후에도 선택한 별점이 유지된다", () => {
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".star-button[data-rating='8']").click();
cy.get(".modal-close-button").click();
cy.visit("localhost:5173");
cy.wait("@popularMovies");
cy.get(".item:not(.skeleton-item)").first().click();
cy.wait("@movieDetail");
cy.get(".modal-movie-my-rating-selector").should("have.attr", "data-rating", "8");
});
});

context("URL 페이지 파라미터 기반 페이지 로드", () => {
it("page=2 URL로 접속 시 첫 번째 API 요청이 2페이지 데이터를 요청한다", () => {
cy.visit("localhost:5173?page=2");
cy.wait("@popularMovies").its("request.url").should("include", "page=2");
});

it("page=2(마지막 페이지) 접속 시 하단 무한 스크롤 트리거 요소가 존재하지 않는다", () => {
cy.visit("localhost:5173?page=2");
cy.wait("@popularMovies");
cy.get(".scroll-area").should("not.exist");
});

it("page=2 접속 후 상단 스크롤 시 1페이지 영화 목록이 추가로 불러와진다", () => {
cy.visit("localhost:5173?page=2");
cy.wait("@popularMovies");
cy.wait("@popularMovies");
cy.get(".item").should("have.length", 40);
});
});

context("스크롤 위치 복원", () => {
it("viewed-movie-id URL 파라미터로 접속 시 해당 영화가 뷰포트에 표시된다", () => {
const targetMovieId = "83533";
cy.visit(`localhost:5173?page=1&viewed-movie-id=${targetMovieId}`);
cy.wait("@popularMovies");
cy.get(`[data-movie-id="${targetMovieId}"]`).should(($el) => {
const rect = $el[0].getBoundingClientRect();
expect(rect.top, "영화가 뷰포트 상단 아래에 있어야 함").to.be.lt(Cypress.config("viewportHeight") as number);
expect(rect.bottom, "영화가 뷰포트 상단 위에 있어야 함").to.be.gt(0);
});
});
});
});
31 changes: 31 additions & 0 deletions cypress/fixtures/movie-detail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"id": 1523145,
"title": "Твоё сердце будет разбито",
"original_title": "Твоё сердце будет разбито",
"overview": "테스트용 줄거리입니다. 이 영화에 대한 상세 설명이 여기에 표시됩니다.",
"release_date": "2026-03-26",
"genres": [
{ "id": 10749, "name": "로맨스" },
{ "id": 18, "name": "드라마" }
],
"popularity": 1037.0135,
"vote_average": 6.6,
"vote_count": 27,
"poster_path": "/iGpMm603GUKH2SiXB2S5m4sZ17t.jpg",
"backdrop_path": "/1x9e0qWonw634NhIsRdvnneeqvN.jpg",
"adult": false,
"video": false,
"original_language": "ru",
"belongs_to_collection": null,
"budget": 0,
"homepage": "",
"imdb_id": "",
"origin_country": ["RU"],
"production_companies": [],
"production_countries": [],
"revenue": 0,
"runtime": 90,
"spoken_languages": [],
"status": "Released",
"tagline": ""
}
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<link rel="stylesheet" href="./src/styles/thumbnail.css" />
<link rel="stylesheet" href="./src/styles/skeleton.css" />
<link rel="stylesheet" href="./src/styles/toast.css" />
<link rel="stylesheet" href="./src/styles/modal.css" />
<title>영화 리뷰</title>
</head>

Expand Down Expand Up @@ -48,7 +49,7 @@
</section>
<div class="container">
<main>
<section>
<section class="movie-list-section">
<h2 class="list-title">지금 인기 있는 영화</h2>
<ul class="thumbnail-list"></ul>
</section>
Expand All @@ -60,7 +61,7 @@ <h2 class="list-title">지금 인기 있는 영화</h2>
<p><img src="./images/woowacourse_logo.png" alt="woowacourse" width="180" /></p>
</footer>
</div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/page/popular.ts"></script>
</body>

</html>
Expand Down
Binary file added public/images/star_sprites.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/svg/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions search.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<link rel="stylesheet" href="./src/styles/thumbnail.css" />
<link rel="stylesheet" href="./src/styles/skeleton.css" />
<link rel="stylesheet" href="./src/styles/toast.css" />
<link rel="stylesheet" href="./src/styles/modal.css" />
<title>영화 리뷰</title>
</head>

Expand All @@ -31,7 +32,7 @@
</div>
<div class="container">
<main>
<section>
<section class="movie-list-section">
<h2 class="list-title">
<div class="skeleton skeleton-list-title"></div>
</h2>
Expand All @@ -45,7 +46,7 @@ <h2 class="list-title">
<p><img src="./images/woowacourse_logo.png" alt="woowacourse" width="180" /></p>
</footer>
</div>
<script type="module" src="/src/search.ts"></script>
<script type="module" src="/src/page/search.ts"></script>
</body>

</html>
Expand Down
Loading