Skip to content
Open
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c256ca3
docs: 요구사항 명세서 및 테스트 시나리오 작성
GamjaIsMine02 Mar 31, 2026
d51c3f2
init: 프로젝트 초기 설정
GamjaIsMine02 Mar 31, 2026
0292b66
feat: 영화 불러오기 기능 구현
GamjaIsMine02 Mar 31, 2026
edc0811
feat: 영화 20개 출력
GamjaIsMine02 Mar 31, 2026
1abceb5
feat: 더 보기 기능 구현
GamjaIsMine02 Mar 31, 2026
32daaab
feat: 검색 기능 구현
GamjaIsMine02 Mar 31, 2026
de28417
feat: 검색 결과가 없을 때의 상황 구현
GamjaIsMine02 Mar 31, 2026
ae0d491
refactor: 영화 목록을 추가하는 로직을 메서드로 분리
GamjaIsMine02 Apr 1, 2026
a316471
refactor: 영화 목록을 불러오는 api 로직을 메서드로 분리
GamjaIsMine02 Apr 1, 2026
7f8b604
fix: 불필요한 코드 삭제
GamjaIsMine02 Apr 1, 2026
c8156ec
refactor: querySelector로 dom을 조작하는 코드에서 타입 단언을 제거 및 메서드로 분리
GamjaIsMine02 Apr 1, 2026
ee830e4
docs: 요구사항 명세서 업데이트
GamjaIsMine02 Apr 1, 2026
d21e1be
refactor: 이벤트 리스너 메서드를 bind 메서드로 분리
GamjaIsMine02 Apr 1, 2026
e743ebc
fix: 영화 alt 속성값을 각 영화의 title로 수정, main.ts에서 String.raw 코드 제거
GamjaIsMine02 Apr 1, 2026
8da7434
fix: modal.html에서 index.html을 사용하도록 변경
GamjaIsMine02 Apr 1, 2026
5cd3817
fix: 검색바 css & 페이지 타이틀 css 수정
GamjaIsMine02 Apr 1, 2026
74150b9
fix: index.html 경로 수정 및 이미지 경로 정상화
GamjaIsMine02 Apr 1, 2026
00b3354
fix: footer css 수정
GamjaIsMine02 Apr 1, 2026
b9d4435
feat: skeleton UI 적용 및 사용하지 않는 import 제거
GamjaIsMine02 Apr 1, 2026
1dcfc6a
fix: skeleton UI를 위해서 강제로 딜레이하는 로직 제거
GamjaIsMine02 Apr 2, 2026
9d9c9e3
fix: css가 늦게 올라가는 버그 수정
GamjaIsMine02 Apr 2, 2026
5d5098f
feat: 영화 포스터 클릭 시 백그라운드에 영화 정보를 띄우는 기능 구현
GamjaIsMine02 Apr 2, 2026
df6e2ed
fix: 백그라운드에 영화 평점이 소수점 1자리까지로 수정
GamjaIsMine02 Apr 2, 2026
f281375
docs: 기능 요구사항에 따라 E2E 테스트 시나리오 추가
GamjaIsMine02 Apr 2, 2026
dd13f81
fix: 최초 접속 시 인기순 첫 번째 영화를 백그라운드에 띄워지도록 수정
GamjaIsMine02 Apr 2, 2026
350ace7
test: cypresss E2E 테스트 코드 추가
GamjaIsMine02 Apr 2, 2026
011997c
fix: api 키 변경
yuncic Apr 2, 2026
f8e44f8
fix: logo, star_empty 이미지 경로 하드코딩 제거
yuncic Apr 2, 2026
0ed101c
- feat: api 호출 에러 처리 기능 추가
yuncic Apr 5, 2026
2f54796
fix: main.ts 초기 로딩 시 에러 처리 누락 수정(callMovieList 적용)
yuncic Apr 5, 2026
0504185
refactor: bindMovieEvent.ts 함수 역할 분리
yuncic Apr 5, 2026
b20fbd2
test: cy.intercept()로 API mock 적용 및 실패 케이스 테스트 추가
yuncic Apr 5, 2026
fdad549
fix: cypress test URL local 주소 -> 배포 주소
yuncic Apr 6, 2026
8b4d0c6
refactor: API 설정을 BASE_API 객체로 분리
yuncic Apr 6, 2026
2254261
fix: 오타 수정
yuncic Apr 6, 2026
c564d97
refactor: request 함수로 API 호출 로직 분리 및 경로 상수화
yuncic Apr 6, 2026
51249f1
docs: step2 기능 명세서 작성
yuncic Apr 7, 2026
856e62a
feat: 영화 상세정보 모달 구현
yuncic Apr 7, 2026
6127209
feat: 별점 매기기 기능 추가
yuncic Apr 7, 2026
a56ffcf
fix: 별점 없는 영화 값 예외 처리를 위한 삼항연상자 추가
yuncic Apr 8, 2026
fcbdab6
feat: 영화 포스터 호버 추가
yuncic Apr 8, 2026
e03ba10
feat: 무한 스크롤 기능 추가
yuncic Apr 8, 2026
6d5ac60
feat: 모바일 반응형 웹 구현
yuncic Apr 8, 2026
4cb37f6
feat: 태블릿 반응형 웹 구현
yuncic Apr 8, 2026
40af500
fix: nav 고정 height값 수정 및 썸네일 repeat 우선순위에 따라 재배치
yuncic Apr 8, 2026
1d39687
fix: 이미지 경로 수정
yuncic Apr 8, 2026
1630e6a
fix: 미디어쿼리 범위 수정
yuncic Apr 9, 2026
959a082
feat: 영화 상세정보 모달 테스트 추가 및 관련 데이터 파일 생성
yuncic Apr 9, 2026
dca0674
test: 무한 스크롤 테스트 추가
yuncic Apr 9, 2026
b49c1e1
fix: 테스트에서 사용된 URL을 최신 버전으로 업데이트
yuncic Apr 9, 2026
e796c59
test: E2E 별점 매기기 테스트 추가
yuncic Apr 9, 2026
de26508
fix: .prettierrc에서 semi 옵션을 true로 수정
yuncic Apr 10, 2026
384a13f
Merge upstream/yuncic into step2
yuncic Apr 10, 2026
1d6520e
refactor: IntersectionObserver 콜백에서 forEach 대신 entries[0] 사용
yuncic Apr 10, 2026
517e398
refactor:
yuncic Apr 13, 2026
f1d8509
refactor: localStorage 내부 동작 검증 테스트 제거
yuncic Apr 13, 2026
5ad7506
refactor: localStorage 직접 접근을 StarRatingStorage로 추상화
yuncic Apr 13, 2026
97d46db
test: 무한스크롤 테스트를 fixture 기반 동적 검증으로 개선
yuncic 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
12 changes: 6 additions & 6 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"endOfLine": "auto",
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"semi": false
"endOfLine": "auto",
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"semi": true
}
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
# javascript-movie-review

FE 레벨1 영화 리뷰 미션
FE 레벨1 영화 리뷰 미션 step2

## 요구사항 명세서

### 영화
## 영화 상세정보 조회 모달

- [x] 영화 리스트를 불러온다.
- [x] 영화 섹션 하나에 포스터, 평점, 영화 제목을 띄운다.
- [x] 영화 목록을 띄울 때 20개씩 띄운다.
- [x] 검색어에 따른 필러링된 영화 리스트를 불러온다.
### 이벤트

- [x] 영화를 불러오는 도중에는 스켈레톤 UI를 보여준다.
- [x] 포스터 클릭 시 백그라운드 영역에 해당 영화 포스터를 보여준다.
- [x] 클릭시 모달 창 띄우기
- [x] 우측 상단 x버튼 혹은 모달 밖 화면 클릭 시 모달 창 끄기

### 더 보기 기능
### UI

- [x] '더보기' 버튼을 누르면 영화 20개를 추가로 띄운다. (띄운 후 아래 '더보기' 버튼 또 생김)
- [x] 포스터 클릭시 상세정보 모달 띄우기
- [x] 포스터 사진 (poster_path)
- [x] 제목 (title)
- [x] 평균 별점 (vote_average)
- [x] 내 별점
- [x] 줄거리 (overview)

### 검색 기능
### 내 별점

- [x] 검색란에 영화 제목을 입력해서 검색 버튼, Enter키를 눌러 필터링된 영화 20개를 띄운다.
- [x] 검색 결과가 존재하지 않으면 "검색 결과가 없습니다" 텍스트를 띄운다.
- [x] 검색 결과 화면에서 검색란에 검색어가 존재하지 않은 상태에서 검색버튼을 누르면 원상복귀.
- local storage 사용

## 테스트 시나리오
* [x] 사용자는 영화에 대해 별점을 줄 수 있으며 새로고침하더라도 사용자가 남긴 별점은 유지되어야 한다.
* [x] 별점은 5개로 구성되어 있으며 한 개당 2점이며 1점 단위는 고려하지 않는다.
* [x] 2점: 최악이예요
* [x] 4점: 별로예요
* [x] 6점: 보통이에요
* [x] 8점: 재미있어요
* [x] 10점: 명작이에요

- [x] '더보기' 버튼을 눌러 페이지를 확장시키면 영화 개수가 20개씩 늘어난다.
- [x] 검색란에 검색어를 입력하고 검색 버튼, Enter키를 누르면 필터링된 영화 목록을 보여준다.
- [x] 검색란에 검색어를 입력해도 결과가 존재하지 않다면 "검색 결과가 없습니다" 텍스트를 띄운다.
- [x] 포스터 클릭시 백그라운드에 해당 포스터의 정보가 띄워진다.
## 무한 스크롤

- [x] 더보기 버튼 대신 무한스크롤 적용

## 반응형 웹

- 분기별로 잘라서 3가지 타입으로 진행

- [x] 데스크톱
- [x] 태블릿
- [x] 모바일

## E2E 테스트

- [x] 포스터를 클릭하면 모달창이 뜬다.
- [x] 내 별점을 클릭하면 별점이 적용이 된다.
- [x] 스크롤을 끝까지 내리면 다음 영화 리스트가 나온다.
33 changes: 33 additions & 0 deletions cypress/e2e/modalTest.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
describe('search test', () => {
beforeEach(() => {
cy.intercept('GET', '**/movie/*', { fixture: 'infoModal.json' })
cy.intercept('GET', '**/movie/popular**', { fixture: 'popularMovies.json' })
cy.intercept('GET', '**/search/movie**', { fixture: 'searchMovies.json' })
})

it('인기순 영화 페이지에서 두 번째 포스터를 클릭하면 영화 상세정보 모달이 띄워진다.', () => {
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.get('.thumbnail-list li')
.eq(1)
.find('#title')
.invoke('text')
.then((listTitle) => {
cy.get('.thumbnail-list li').eq(1).click()
cy.get('#modalBackground').should('have.class', 'active')
cy.get('#modalTitle')
.invoke('text')
.should((modalTitle) => {
expect(modalTitle.trim()).to.equal(listTitle.trim())
})
})
})

it('검색 결과에서 포스터를 클릭하면 모달이 열린다.', () => {
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.get('.search-bar').type('스파이더맨')
cy.get('.search-btn').click()
cy.get('.thumbnail-list li').first().click()
cy.get('#modalBackground').should('have.class', 'active')
cy.get('#modalTitle').should('not.be.empty')
})
})
78 changes: 63 additions & 15 deletions cypress/e2e/moreBtnTest.cy.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
describe('more btn test', () => {
describe('infinite scroll test', () => {
beforeEach(() => {
cy.intercept('GET', '**/movie/popular**', { fixture: 'popularMovies.json' })
cy.intercept('GET', '**/search/movie**', { fixture: 'searchMovies.json' })
})
it('페이지 접속 후 더보기 버튼을 1번 누르면 영화 개수가 40개가 된다.', () => {
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')

cy.get('.item').should('have.length', 20)
it('스크롤을 내리면 추가 영화 목록을 불러온다.', () => {
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')

cy.get('.display-more-btn').click()
cy.get('.thumbnail-list li').should('have.length', 20)

cy.get('.item').should('have.length', 40)
})
it('페이지 접속 후 더보기 버튼을 10번 누르면 영화 개수가 220개가 된다.', () => {
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.get('.item').should('have.length', 20)
for (let i = 0; i < 10; i++) {
cy.get('.display-more-btn').click()
}
cy.get('.item').should('have.length', 220)
cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 40)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 60)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 80)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 100)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 120)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 140)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 160)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 180)

cy.get('.thumbnail-list li').last().scrollIntoView()

cy.get('.thumbnail-list li').should('have.length', 200)
})
})

// describe('more btn test', () => {
// beforeEach(() => {
// cy.intercept('GET', '**/movie/popular**', { fixture: 'popularMovies.json' })
// cy.intercept('GET', '**/search/movie**', { fixture: 'searchMovies.json' })
// })
// it('페이지 접속 후 더보기 버튼을 1번 누르면 영화 개수가 40개가 된다.', () => {
// cy.visit('https://javascript-movie-review-dvlk.vercel.app/')

// cy.get('.item').should('have.length', 20)

// cy.get('.display-more-btn').click()

// cy.get('.item').should('have.length', 40)
// })
// it('페이지 접속 후 더보기 버튼을 10번 누르면 영화 개수가 220개가 된다.', () => {
// cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
// cy.get('.item').should('have.length', 20)
// for (let i = 0; i < 10; i++) {
// cy.get('.display-more-btn').click()
// }
// cy.get('.item').should('have.length', 220)
// })
// })
10 changes: 5 additions & 5 deletions cypress/e2e/searchTest.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('search test', () => {
cy.intercept('GET', '**/search/movie**', { fixture: 'searchMovies.json' })
})
it('검색어를 입력한 뒤 검색 버튼을 누르면 필터링된 영화 목록을 보여준다.', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

우선 테스트 시나리오를 유저 행동 기반으로 작성한 점은 정말 좋다고 생각해요.
다만 현재 해당 테스트 시나리오는 한번에 2개를 검증하고 있는데 이렇게 하신 의도가 있을까요?
따로따로 나눠서 해도 되지 않을까 해서요!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

검색을 하지 않았을때 뽑은 타이틀 텍스트와 비교하기 위해 그랬던 것인데, fixture 데이터를 정의했을 시점에 수정했어야하는데 놓친 것 같습니다..!
해당 부분 수정했습니다!

cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')

cy.get('.thumbnail-list li')
.first()
Expand All @@ -25,7 +25,7 @@ describe('search test', () => {
})

it('검색어를 입력한 뒤 엔터키를 누르면 필터링된 영화 목록을 보여준다.', () => {
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')

cy.get('.thumbnail-list li')
.first()
Expand All @@ -45,23 +45,23 @@ describe('search test', () => {
})
it("검색란에 검색어를 입력해도 결과가 존재하지 않다면 '검색 결과가 없습니다' 텍스트를 띄운다", () => {
cy.intercept('GET', '**/search/movie**', { body: { results: [] } })
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.get('.search-bar').type('ㄴㅇ러ㅏㅗㅁ라ㅗ어ㅏ로머ㅏJklhdskldh')
cy.get('.search-btn').click()
cy.get('.search-error-text').should('have.text', '검색 결과가 없습니다.')
})

it('네트워크 오류 시 알림을 띄운다', () => {
cy.intercept('GET', '**/movie/popular**', { forceNetworkError: true })
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.on('window:alert', (text) => {
expect(text).to.equal('네트워크 오류가 발생하였습니다.')
})
})

it('API 오류 시 알림을 띄운다', () => {
cy.intercept('GET', '**/movie/popular**', { statusCode: 401 })
cy.visit('https://javascript-movie-review-dvlk.vercel.app/')
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.on('window:alert', (text) => {
expect(text).to.equal('데이터를 불러오지 못했습니다.')
})
Expand Down
74 changes: 74 additions & 0 deletions cypress/e2e/starRatingTest.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
describe('별점 매기기 테스트', () => {
beforeEach(() => {
cy.intercept('GET', '**/movie/*', { fixture: 'infoModal.json' })
cy.intercept('GET', '**/movie/popular**', { fixture: 'popularMovies.json' })
cy.clearLocalStorage()
cy.visit('https://javascript-movie-review-dvlk-a6xn5spuo-yun-cics-projects.vercel.app/')
cy.get('.thumbnail-list li').first().click()
cy.get('#modalBackground').should('have.class', 'active')
})

it('별점 클릭 시 해당 별점 텍스트가 표시된다.', () => {
cy.get('.star-icon[data-value="2"]').click()
cy.get('.my-rate-text').should('have.text', '최악이에요 (2/10)')

cy.get('.star-icon[data-value="4"]').click()
cy.get('.my-rate-text').should('have.text', '별로예요 (4/10)')

cy.get('.star-icon[data-value="6"]').click()
cy.get('.my-rate-text').should('have.text', '보통이에요 (6/10)')

cy.get('.star-icon[data-value="8"]').click()
cy.get('.my-rate-text').should('have.text', '재미있어요 (8/10)')

cy.get('.star-icon[data-value="10"]').click()
cy.get('.my-rate-text').should('have.text', '명작이에요 (10/10)')
})

it('별점 클릭 시 클릭한 별까지 채워진 별로 변경된다.', () => {
// 클릭 전 빈 별 src 저장
cy.get('.star-icon[data-value="10"]')
.invoke('attr', 'src')
.then((emptySrc) => {
cy.get('.star-icon[data-value="6"]').click()

// 클릭한 별까지는 src가 빈 별과 달라야 함
cy.get('.star-icon[data-value="2"]').invoke('attr', 'src').should('not.equal', emptySrc)
cy.get('.star-icon[data-value="4"]').invoke('attr', 'src').should('not.equal', emptySrc)
cy.get('.star-icon[data-value="6"]').invoke('attr', 'src').should('not.equal', emptySrc)

// 클릭하지 않은 별은 여전히 빈 별이어야 함
cy.get('.star-icon[data-value="8"]').invoke('attr', 'src').should('equal', emptySrc)
cy.get('.star-icon[data-value="10"]').invoke('attr', 'src').should('equal', emptySrc)
})
})

it('별점 클릭 시 localStorage에 별점이 저장된다.', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2;

E2E 테스트에선 유저 행동 기반으로 작성하는게 좋아요.
현재 로컬 스토리지에 저장하는지 여부는 유저 입장에서 굳이 알 필요가 없기도 해요.
유저 관점으로 테스트 시나리오를 작성한다면 어떻게 해볼 수 있을까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

인터렉션 관점에서 생각했어야하는데 구현 세부사항으로 빠져버렸네요 ㅠ
중요한건 "별점을 매기고 모달을 껏다 켜도 남아있는가" 이니까
해당 부분 테스트만 남기고 내부 동작 검증 테스트는 지웠습니다!

cy.get('.star-icon[data-value="8"]').click()

// infoModal.json movie id 83533
cy.window().then((win) => {
expect(win.localStorage.getItem('rating_83533')).to.equal('8')
})
})

it('모달을 닫았다가 다시 열면 이전에 저장한 별점이 복원된다.', () => {
cy.get('.star-icon[data-value="6"]').click()
cy.get('.my-rate-text').should('have.text', '보통이에요 (6/10)')

cy.get('#closeModal').click()
cy.get('#modalBackground').should('not.have.class', 'active')

cy.get('.thumbnail-list li').first().click()
cy.get('#modalBackground').should('have.class', 'active')

cy.get('.my-rate-text').should('have.text', '보통이에요 (6/10)')
cy.get('.star-icon[data-value="8"]')
.invoke('attr', 'src')
.then((emptySrc) => {
cy.get('.star-icon[data-value="2"]').invoke('attr', 'src').should('not.equal', emptySrc)
cy.get('.star-icon[data-value="6"]').invoke('attr', 'src').should('not.equal', emptySrc)
cy.get('.star-icon[data-value="8"]').invoke('attr', 'src').should('equal', emptySrc)
})
})
})
Loading