Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
72aff64
docs: 커밋 테스트
goodsmell Mar 31, 2026
62261f0
docs: 요구사항 및 테스트 명세 작성
goodsmell Mar 31, 2026
c9a1238
feat: 인기 영화 20개를 가져온다.
goodsmell Mar 31, 2026
4be2b05
chore: env파일 무시
goodsmell Mar 31, 2026
06fccf8
feat: 인기 영화 목록 렌더링
goodsmell Mar 31, 2026
b9ff268
feat: 검색창 UI 구현
goodsmell Mar 31, 2026
4cee69e
feat: 배너 설정
goodsmell Mar 31, 2026
8214f4f
feat: 더보기 기능 구현
goodsmell Mar 31, 2026
864a72c
feat: 검색 기능 구현
goodsmell Apr 1, 2026
3b71dee
feat: 더보기 숨기기 기능 구현
goodsmell Apr 1, 2026
ac83534
feat: 스켈레톤UI 구현
goodsmell Apr 1, 2026
7683de0
fix: 검색 결과 없을 때 UI 중복되는 오류 해결
goodsmell Apr 2, 2026
2fa44b0
docs: E2E 시나리오 작성
goodsmell Apr 2, 2026
7090a41
chore: 스타일 파일 이동
goodsmell Apr 2, 2026
bc6fb20
test: 검색 시나리오 테스트 구현
goodsmell Apr 2, 2026
d8d57ec
test: 메인 시나리오 테스트 구현
goodsmell Apr 2, 2026
fba0e4d
feat: api 요청 시 더 보기 비활성 구현
goodsmell Apr 2, 2026
90c441e
test: api 요청 예외 테스트
goodsmell Apr 2, 2026
d6fe4d0
refector: 파일구조 분리
goodsmell Apr 2, 2026
3a177a6
chore: 배포 파이프라인 설정
goodsmell Apr 2, 2026
5824970
fix: 빌드 오류 해결
goodsmell Apr 2, 2026
130ab9c
refactor: main.ts - try-catch-finally 패턴 적용
goodsmell Apr 6, 2026
e4bf5e4
refactor: moreButton.ts - try-catch-finally 패턴 적용 및 중복 로직 제거
goodsmell Apr 6, 2026
d58f4d1
refatcore: searchForm.ts - try-catch-finally 패턴 적용
goodsmell Apr 6, 2026
cbb8693
fix: 검색 시 페이지 상태 초기화
goodsmell Apr 6, 2026
c1aada5
fix: 검색 결과에 따라 더보기 버튼 표시 처리
goodsmell Apr 6, 2026
a26637b
fix: response.json() 파싱 실패 처리를 위한 try/catch 추가
goodsmell Apr 7, 2026
692c2c2
refactor: non-null assertion(!) 제거 및 null 체크 추가
goodsmell Apr 7, 2026
beffa5a
fix: 영화 이미지 없을 경우 fallback image 사용
goodsmell Apr 7, 2026
d764db5
refactor: bindEvent 역할 분리
goodsmell Apr 7, 2026
ca9f91a
refactor: 상태 관리를 DOM 기준에서 store 기준으로 개선
goodsmell Apr 7, 2026
b45c77f
refactor: 중복 랜더링 제거
goodsmell Apr 7, 2026
689ac3b
refactor: main.ts 구조 개선 및 역할 분리
goodsmell Apr 7, 2026
443f5b1
refactor: searchForm DOM 요소를 역할별 객체로 묶어 관리하도록 개선
goodsmell Apr 7, 2026
2ca9882
refactor: SearchForm의 검색처리 로직을 메서드로 분리
goodsmell Apr 7, 2026
b772bb4
refactor: MoreButton에 더보기 버튼 표시 로직을 위임
goodsmell Apr 7, 2026
cbeb870
chore: 파일 구조 분리
goodsmell Apr 7, 2026
a614dc4
fix: 리스트 초기화 없이 append되어 영화 목록이 누적되는 문제 수정
goodsmell Apr 7, 2026
12affa2
refactor: 영화 API 요청 및 응답 처리 로직 공통화
goodsmell Apr 7, 2026
0eec055
fix: api 링크 지역 및 언어 설정
goodsmell Apr 8, 2026
8dd1f01
docs: step2 요구사항 목록 작성
goodsmell Apr 9, 2026
ce95c96
feat: 영화 상세 정보 모달 구현
goodsmell Apr 10, 2026
11041d0
feat: 별점 입력 기능 구현
goodsmell Apr 12, 2026
25e3908
feat:무한 스크롤 구현 및 더보기 버튼 삭제
goodsmell Apr 12, 2026
bb64502
fix: 모달 열리면 스크롤 방어
goodsmell Apr 12, 2026
7eef935
feat: 모달 즉시 열기 및 로딩 상태 추가
goodsmell Apr 12, 2026
7a378a9
style: 반응형 적용
goodsmell Apr 13, 2026
7752223
style: section title 워딩 변경
goodsmell Apr 13, 2026
cf9c908
test: 반응형, 모달, 무한스크롤을 e2e에 적용
goodsmell Apr 13, 2026
6c2eacb
fix: RATING_LABELS 워딩 변경
goodsmell Apr 13, 2026
b06cdfb
fix: 모달 이미지 렌더링 문제 수정 및 fallback 처리 추가
goodsmell Apr 13, 2026
492cb85
Merge remote-tracking branch 'upstream/goodsmell' into step-2
goodsmell Apr 13, 2026
4076b17
refactor: 클래스 인스턴스 변수 3개 이하로 제한
goodsmell Apr 13, 2026
b26003f
docs: 요구사항 목록 정리
goodsmell Apr 13, 2026
1f0e248
refactor: 오타 수정
goodsmell Apr 13, 2026
b45f46c
refactor: 사용자 친화적 에러메시지로 변경
goodsmell Apr 13, 2026
98deb00
refactor: 빈 main 블록 제거
goodsmell Apr 13, 2026
90f3faa
refactor: 상수화
goodsmell Apr 13, 2026
62c5f46
refactor: requireElement 유틸로 DOM 조회 방식 통일
goodsmell Apr 13, 2026
4694bc2
refactor: 이미지 로딩 로직을 loadImageWithFallback 유틸로 추출
goodsmell 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
4 changes: 3 additions & 1 deletion README.md → STEP1-README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ FE 레벨1 영화 리뷰 미션
- 모든 영화를 가져왔을 때 더 보기 버튼을 숨긴다.
- 검색 시 스켈레톤 UI를 표시한다.



## 배너

- 가장 인기 있는 영화의 정보를 띄운다.
Expand All @@ -38,7 +40,7 @@ FE 레벨1 영화 리뷰 미션
1. 배너에 첫 번째 인기 영화의 정보가 표시된다.
2. 최초 진입 시 인기 영화 최대 20개가 표시된다.
3. 더 보기 버튼을 누르면 최대 20개가 추가된다.
4. 더 이상 보여 줄 영화가 없으면 더 보기 버튼이 사라진다.
4. 더 이상 보여줄 영화가 없으면 더 보기 버튼이 사라진다.

### 검색 시

Expand Down
70 changes: 70 additions & 0 deletions STEP2-README.md
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. 검색 결과에서 영화를 클릭하면 모달이 열린다.
- 모달에 영화 정보가 올바르게 표시된다.
- 모달을 닫고 검색 결과 화면으로 돌아올 수 있다.
251 changes: 200 additions & 51 deletions cypress/e2e/main.cy.ts
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("최초 진입 시나리오 테스트", () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

초기 로딩, 무한스크롤, 모달, 반응형 레이아웃까지 매우 잘 커버하고 있네요👍

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. 반응형 레이아웃", () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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,
);
});
});
});
});
});
Loading