Skip to content
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d0df79d
refactor: renderResultSectionContent 함수 분해
gustn99 Apr 8, 2026
7b9a467
refactor: error container를 컴포넌트로 분리
gustn99 Apr 9, 2026
737589f
refactor: empty container를 컴포넌트로 분리
gustn99 Apr 9, 2026
7c080df
refactor: 더 보기 버튼을 컴포넌트로 분리
gustn99 Apr 9, 2026
b03c2b1
refactor: ThumbnailList를 컴포넌트로 분리
gustn99 Apr 9, 2026
48e48b5
refactor: Main/Search ThumbnailList를 컴포넌트로 분리
gustn99 Apr 9, 2026
e04df3d
refactor: thumbnailList를 movieItems로 변경
gustn99 Apr 9, 2026
c1e8b44
refactor: banner를 컴포넌트로 분리
gustn99 Apr 9, 2026
23bef43
refactor: 썸네일 스켈레톤 로직을 MovieItem 책임으로 분리
gustn99 Apr 10, 2026
bafbcee
refactor: hideAll 함수를 main/search로 분리
gustn99 Apr 10, 2026
9a12d53
refactor: render-ThumbnailListLoading 함수 플로우를 다른 함수들과 통일
gustn99 Apr 10, 2026
efe58af
refactor: renderResultSectionContent 파일을 페이지 단위로 분리
gustn99 Apr 10, 2026
f0824a9
refactor: renderLoadingUI 대신 페이지별 loading 렌더 함수 사용
gustn99 Apr 10, 2026
2faea7a
refactor: 불필요한 렌더 함수 파일 제거
gustn99 Apr 10, 2026
c1f2920
refactor: 페이지 렌더링 진입점을 render-UI 함수로 통일
gustn99 Apr 10, 2026
7a8ee22
refactor: renderSearchUI 호출 시 keyword를 인자 기반에서
gustn99 Apr 10, 2026
5549759
fix: 메인 로딩 시에도 배너 렌더링되도록 수정
gustn99 Apr 10, 2026
0b0ab5d
refactor: compositions와 pages 분리
gustn99 Apr 11, 2026
eeced09
refactor: 불필요한 hideBanner 호출 제거
gustn99 Apr 11, 2026
e5c1da2
refactor: 메인 함수의 이벤트 리스너 등록 로직을 함수로 분리
gustn99 Apr 11, 2026
ebac474
feat: 영화 상세 조회 api 함수 구현
gustn99 Apr 11, 2026
b4a8b8d
refactor: api 타입 분리
gustn99 Apr 11, 2026
754d8a0
docs: 기능 요구 사항 정리
gustn99 Apr 11, 2026
d117301
fix: 메인 더 보기 버튼 클릭 시 기존 리스트 제거되는 문제 수정
gustn99 Apr 11, 2026
68f0629
fix: 검색 더 보기 버튼 클릭 시 기존 리스트 제거되는 문제 수정
gustn99 Apr 11, 2026
d6efec6
feat: 스크롤 기반 페이징 구현
gustn99 Apr 11, 2026
6fb7f4e
style: compositions 파일 내 함수 선언 순서 조정
gustn99 Apr 11, 2026
4192096
feat: page 값을 세션 기반으로 관리
gustn99 Apr 11, 2026
ffed08c
fix: 뒤로가기 시 경로-페이지 동기화 문제 해결
gustn99 Apr 11, 2026
42f042c
feat: observer root margin 조정
gustn99 Apr 11, 2026
de3bea0
docs: 요구 사항 목록 업데이트
gustn99 Apr 11, 2026
6d043fa
feat: 모달 템플릿 구현
gustn99 Apr 11, 2026
bd8c2fb
feat: 모달 렌더링 이벤트 등록
gustn99 Apr 11, 2026
7bbcb18
feat: 모달을 dialog 태그로 관리
gustn99 Apr 11, 2026
3484338
refactor: modal -> movie modal 네이밍 변경
gustn99 Apr 11, 2026
e420fcb
refactor: my-rate 블럭을 컴포넌트로 분리
gustn99 Apr 11, 2026
66a430a
feat: 내 별점 업데이트 기능 추가
gustn99 Apr 11, 2026
03ec253
feat: 별점 조회/등록 기능을 로컬스토리지 기반으로 저장
gustn99 Apr 11, 2026
4ed37d0
feat: 로컬스토리지 기반 로직을 api 함수로 래핑
gustn99 Apr 12, 2026
331a681
fix: 버튼 사이 클릭 시 별점 삭제되는 문제 수정
gustn99 Apr 12, 2026
72d897d
docs: 요구 사항 목록 업데이트
gustn99 Apr 12, 2026
9541fe6
feat: 영화 리스트 반응형 추가
gustn99 Apr 12, 2026
cc5df42
feat: 헤더 반응형 추가
gustn99 Apr 12, 2026
303f786
feat: 모달 반응형 추가
gustn99 Apr 12, 2026
073197b
feat: 모달 렌더링 시 스크롤 제한
gustn99 Apr 12, 2026
7d9ed77
fix: movie item 외 영역 클릭 시에도 alert하는 문제 수정
gustn99 Apr 12, 2026
775f413
docs: 요구 사항 목록 업데이트
gustn99 Apr 12, 2026
06b9665
fix: 페이징 에러 핸들링 로직을 이벤트 핸들러 -> 페이지 내부로 이동
gustn99 Apr 12, 2026
886157e
test: 무한 스크롤 테스트 추가
gustn99 Apr 12, 2026
e00098a
test: 더 보기 버튼 테스트 제거
gustn99 Apr 12, 2026
a05475b
test: 영화 상세 모달 테스트 추가
gustn99 Apr 12, 2026
89fd069
test: 무한 스크롤 엣지 케이스 추가
gustn99 Apr 12, 2026
ce210ac
test: 모달 렌더링 시 내 별점 확인 테스트 추가
gustn99 Apr 12, 2026
20964e8
test: 내 별점 기능 테스트 추가
gustn99 Apr 12, 2026
bc70531
docs: 요구 사항 목록 업데이트
gustn99 Apr 12, 2026
552debf
feat: 배너 자세히 보기 버튼 이벤트 핸들러 등록
gustn99 Apr 12, 2026
1f167b5
refactor: main 컴포지션을 home으로 리네이밍
gustn99 Apr 12, 2026
6da8763
refactor: 남용되는 main 키워드를 popular, home으로 대체
gustn99 Apr 12, 2026
c1bce49
refactor: images 폴더를 public 내부로 이동
gustn99 Apr 12, 2026
2d83c21
refactor: 별점 관련 접근성 추가
gustn99 Apr 12, 2026
2c1ed1f
fix: MyRate 누락된 마크업 추가
gustn99 Apr 12, 2026
0924d6d
refactor: 검색 핸들러 내부 page 의존성 제거
gustn99 Apr 14, 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
134 changes: 25 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,130 +1,46 @@
# 요구 사항 목록

## 영상 목록 조회
## 무한 스크롤

- [x] 영화 목록의 1페이지를 불러오며 더보기 버튼을 누르면 그 다음의 영화 목록을 불러 올 수 있다.
- [x] 페이지 끝에 도달한 경우에는 더보기 버튼을 화면에 출력하지 않는다.
- [x] 영화는 한 번의 요청당 20개씩 영화 목록을 보여준다.
- [x] 영화 목록을 불러오는 동안 Skeleton UI 를 보여준다
- [x] Skeleton UI는 템플릿으로 제공되는 파일 이외로 자유롭게 구현할 수 있다.
- [x] 더 보기 버튼 제거
- [x] page search params 제거

## 검색
## 영화 상세정보 조회

- [x] 영화 검색 API를 이용하여 내가 보고 싶은 영화를 검색할 수 있다.
- [x] 엔터키를 눌러 검색할 수 있다
- [x] 검색 버튼을 클릭하여 검색할 수 있다
- [x] 영화 목록 조회와 같이 검색한 결과에 한해 정보를 보여주는 화면의 요구사항은 동일하다
- [x] 영화 포스터나 제목을 클릭하면 예고편, 줄거리 모달 렌더링
- [x] x 버튼, esc로 모달 닫기
- [x] 이벤트 위임 -> renderMovieItems에서 처리

## 오류
## 별점 매기기

- [x] 오류가 발생하는 경우에는 사용자를 위한 오류 메시지를 띄워 준다.
- [x] 어떤 오류를 대응해야 하고, 어떤 UI로 보여줄 것인지는 자율적으로 결정한다.
- [x] 상세 모달 안에서 등록한 별점이 없는 경우 등록 가능
- [x] 이미 등록한 별점이 있는 경우 별점 렌더링
- [x] 별점은 로컬스토리지에 저장, api 서버 연결 가능성을 고려하여 확장성 있게 설계

---

# 기능 목록

## 퍼블리싱

### 기본 UI

- [x] 영화 목록 검색 input
- [x] 정렬 버튼 삭제
- [x] 더보기 버튼
- [x] 다음 페이지 없으면 제거
- [x] 영화 리스트 background 변경

### 검색 시 UI

- [x] 검색 시(엔터, 검색 버튼 클릭) 인기 영화 배너 hidden
- [x] 검색 결과 없을 때 영화 배열 및 더 보기 버튼 hidden
- [x] 검색 결과 없음 UI

### 조회 에러 UI

- [x] 검색 결과가 없음 UI에서 텍스트 변경 + 재시도 버튼 추가

## 기능

### 메인 페이지

- [x] 최초 접근 시 인기순 영화 20개 렌더링
## UI/UX 개선

### 더보기 버튼

- [x] api 함수 받아서 호출

### 검색

- [x] 검색 버튼 클릭 or 엔터 입력 시 검색 api 호출
- [x] 검색 버튼 클릭 시 keyword 파라미터 추가
- [x] 이전 검색 기록 남아있는 버그 수정

### 로고

- [x] 클릭 시 기본 UI로
- [x] width에 따라 column 수 조절
- [x] 헤더 및 배너 UI 레이아웃도 변경
- [x] 데스크톱: 중앙 모달
- [x] 태블릿: 하단 모달
- [x] 모바일: 하단 모달 + 중앙 정렬 + 썸네일 제거
- [x] 모달 렌더링 시 백그라운드 스크롤 제한

---

## 미룬이

- [ ] getElementById로 가져온 html 요소 타입 좁혀주는 유틸
- [x] TMDBError class 사용에 대해 논의
- [x] 빈 keyword 검색 시 기본 UI로 변경
- [ ] parameter로 관리하는 page가 1일 때도 보여지게 하는게 맞을까? 안 보이게 할까?
- [x] E2E 테스트
- [ ] 돔 접근을 어디서 할 것인지 기준 세우기 (인자로 받기 vs 함수 안에서 호출하기)
- [ ] 동일 검색어 api 요청 막기

## E2E 테스트 목록

- [x] 처음 앱에 도달했을 때 메인 구성 요소가 렌더링 되는지 테스트
- given: 없음
- when: 페이지 진입
- then
- main-thumbnail-list 렌더링
- banner 렌더링
- logo 렌더링
- search input 렌더링

- [x] 더 보기 기능 (main)
- given: 페이지 진입, 더 보기 버튼
- when: 더 보기 버튼 클릭
- then: 기존 영화 목록에 추가 영화 목록이 append됨

- [x] 더 보기 기능 (search)
- given: 검색어 입력 후 검색, 더 보기 버튼
- when: 더 보기 버튼 클릭
- then: 기존 검색 결과 목록에 추가 결과가 append됨

- [x] 검색 기능 - 버튼 클릭
- given: 영화 검색 input에 검색어 입력
- when: 검색 버튼 클릭
- then
- search-thumbnail-list 렌더링
- URL에 keyword parameter 추가
- subtitle이 "${검색어} 검색 결과"로 변경

- [x] 검색 기능 - 엔터 입력
- given: 영화 검색 input에 검색어 입력
- when: 엔터 키 입력
- then
- search-thumbnail-list 렌더링
- URL에 keyword parameter 추가
- subtitle이 "${검색어} 검색 결과"로 변경

- [x] 검색 후 메인으로 복귀(logo 클릭)
- given: 검색 결과 화면
- when: 로고 클릭
- then
- main-thumbnail-list 렌더
- banner 렌더링
- keyword parameter 제거

- [x] 검색 후 메인으로 복귀(빈 문자열 검색)
- given: 검색 결과 화면
- when: 빈 문자열 검색
- then
- main-thumbnail-list 렌더링
- banner 렌더링
- keyword parameter 제거
- [x] 무한스크롤
- [x] 상세 모달 렌더링
- [x] x 버튼으로 모달 닫기
- [x] esc 입력으로 모달 닫기
- [x] 별점 입력
- [x] 별점 업데이트
- [x] 새로고침 시에도 별점 유지
108 changes: 108 additions & 0 deletions cypress/e2e/InfiniteScroll.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { interceptPopularError, interceptPopularLastPage, interceptPopularPage1, interceptPopularPage2, interceptSearchError, interceptSearchLastPage, interceptSearchPage1, interceptSearchPage2 } from "./spec";

describe("메인 무한 스크롤 동작 테스트", () => {
it("스크롤이 observer target에 도달하면 추가 영화가 append된다", () => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");

cy.get("#popular-thumbnail-list li").then(($initialItems) => {
const initialCount = $initialItems.length;

interceptPopularPage2();
cy.get("#home-observer-target").scrollIntoView();
cy.wait("@getPopularPage2");

cy.get("#popular-thumbnail-list li").should(
"have.length.greaterThan",
initialCount,
);
});
});

it("마지막 페이지인 경우 observer target이 렌더링되지 않는다", () => {
interceptPopularLastPage();
cy.visit("/");
cy.wait("@getPopularLastPage");

cy.get("#home-observer-target").should("not.exist");
});
});

describe("검색 무한 스크롤 동작 테스트", () => {
it("검색 후 스크롤이 observer target에 도달하면 추가 결과가 append된다", () => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");

interceptSearchPage1();
cy.get("#search-input").type("인터스텔라");
cy.get("#search-button").click();
cy.wait("@getSearchPage1");

cy.get("#search-thumbnail-list li").then(($initialItems) => {
const initialCount = $initialItems.length;

interceptSearchPage2();
cy.get("#search-observer-target").scrollIntoView();
cy.wait("@getSearchPage2");

cy.get("#search-thumbnail-list li").should(
"have.length.greaterThan",
initialCount,
);
});
});

it("검색 결과가 마지막 페이지인 경우 observer target이 렌더링되지 않는다", () => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");

interceptSearchLastPage();
cy.get("#search-input").type("인터스텔라");
cy.get("#search-button").click();
cy.wait("@getSearchLastPage");

cy.get("#search-observer-target").should("not.exist");
});
});

describe("홈 무한 스크롤 에러 테스트", () => {
it("스크롤로 추가 로딩 중 에러가 발생하면 에러 메시지가 alert된다", () => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");

interceptPopularError();
const alertStub = cy.stub();
cy.on("window:alert", alertStub);

cy.get("#home-observer-target").scrollIntoView();
cy.wait("@getPopularError").then(() => {
expect(alertStub).to.have.been.called;
});
});
});

describe("검색 무한 스크롤 에러 테스트", () => {
it("검색 후 스크롤로 추가 로딩 중 에러가 발생하면 에러 메시지가 alert된다", () => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");

interceptSearchPage1();
cy.get("#search-input").type("인터스텔라");
cy.get("#search-button").click();
cy.wait("@getSearchPage1");

interceptSearchError();
const alertStub = cy.stub();
cy.on("window:alert", alertStub);

cy.get("#search-observer-target").scrollIntoView();
cy.wait("@getSearchError").then(() => {
expect(alertStub).to.have.been.called;
});
});
});
6 changes: 3 additions & 3 deletions cypress/e2e/MainRender.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { interceptPopularPage1, interceptPopularError } from "./spec";
import { interceptPopularError, interceptPopularPage1 } from "./spec";

describe("처음 앱에 도달했을 때 메인 구성 요소가 렌더링 되는지 테스트", () => {
beforeEach(() => {
Expand All @@ -7,8 +7,8 @@ describe("처음 앱에 도달했을 때 메인 구성 요소가 렌더링 되
cy.wait("@getPopularPage1");
});

it("main-thumbnail-list가 렌더링된다", () => {
cy.get("#main-thumbnail-list").should("be.visible");
it("popular-thumbnail-list가 렌더링된다", () => {
cy.get("#popular-thumbnail-list").should("be.visible");
});

it("banner가 렌더링된다", () => {
Expand Down
107 changes: 107 additions & 0 deletions cypress/e2e/MovieModal.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
interceptMovieDetail,
interceptMovieDetailError,
interceptPopularPage1,
} from "./spec";

const openModal = () => {
interceptMovieDetail();
cy.get("#popular-thumbnail-list li").first().click();
cy.wait("@getMovieDetail");
};

describe("영화 상세 모달 렌더링 테스트", () => {
beforeEach(() => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");
});

it("영화 아이템을 클릭하면 상세 모달이 렌더링된다", () => {
openModal();
cy.get("#modal-dialog").should("be.visible");
});

it("모달에 영화 제목이 표시된다", () => {
openModal();
cy.get(".modal-description h2").should("contain.text", "영화1");
});

it("모달에 장르가 표시된다", () => {
openModal();
cy.get(".modal-description .category").should("contain.text", "액션");
cy.get(".modal-description .category").should("contain.text", "모험");
});

it("모달에 평균 별점이 표시된다", () => {
openModal();
cy.get(".modal-rate .rate span").should("contain.text", "8");
});

it("모달에 내 별점이 표시된다", () => {
openModal();
cy.get("#my-rate").should("be.visible");
});

it("모달에 줄거리가 표시된다", () => {
openModal();
cy.get(".detail p").should("contain.text", "영화1의 줄거리입니다.");
});
});

describe("영화 상세 모달 닫기 테스트", () => {
beforeEach(() => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");
});

it("닫기 버튼을 클릭하면 모달이 닫힌다", () => {
openModal();
cy.get("#closeModal").click({ force: true });
cy.get("#modal-dialog").should("not.exist");
});

it("ESC 키를 누르면 모달이 닫힌다", () => {
openModal();
cy.get("#modal-dialog").then(($dialog) => {
$dialog[0].dispatchEvent(new Event("cancel"));
});
cy.get("#modal-dialog").should("not.exist");
});

it("모달 내부를 클릭하면 모달이 닫히지 않는다", () => {
openModal();
cy.get(".modal-description h2").click();
cy.get("#modal-dialog").should("be.visible");
});
});

describe("영화 상세 모달 에러 테스트", () => {
beforeEach(() => {
interceptPopularPage1();
cy.visit("/");
cy.wait("@getPopularPage1");
});

it("영화 상세 조회에 실패하면 에러 메시지가 alert된다", () => {
interceptMovieDetailError();
const alertStub = cy.stub();
cy.on("window:alert", alertStub);

cy.get("#popular-thumbnail-list li").first().click();
cy.wait("@getMovieDetailError").then(() => {
expect(alertStub).to.have.been.called;
});
});

it("영화 상세 조회에 실패하면 모달이 렌더링되지 않는다", () => {
interceptMovieDetailError();
cy.on("window:alert", () => {});

cy.get("#popular-thumbnail-list li").first().click();
cy.wait("@getMovieDetailError");

cy.get("#modal-dialog").should("not.exist");
});
});
Loading