Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3ff14f5
feat: 기본 기능구현 목록 작성
lee-eojin Apr 10, 2026
548df30
delete: 더보기 관련 기능 주석처리 및 관련 인자 제거
lee-eojin Apr 10, 2026
b6df0ab
feat: IntersectionObserver 기반 ScrollObserver 구현
lee-eojin Apr 11, 2026
fcd70db
chore: .prettierrc에서 printWidth 120으로 설정
lee-eojin Apr 11, 2026
2ea61fb
feat: stars_sprite 이미지 생성
lee-eojin Apr 11, 2026
7d4bcea
feat: 주석 제거 및 더보기 버튼 기능 제거
lee-eojin Apr 11, 2026
eefad93
feat: IntersectionObserver 기반 ScrollObserver 구현
lee-eojin Apr 11, 2026
5da001b
refactor: dom.ts 로 분리
lee-eojin Apr 11, 2026
ca6e882
refactor: 이벤트 라우팅 router.ts로 분리 및 TITLE_CHANGED 책임 이동
lee-eojin Apr 11, 2026
fb1d599
refactor: Header가 logo 클릭 이벤트 직접 소유
lee-eojin Apr 11, 2026
1af32c7
chore: init.ts에서 subscriptions로 이름 변경
lee-eojin Apr 11, 2026
56052e4
refactor: 중복 로직 제거
lee-eojin Apr 11, 2026
d492562
refactor: searchInput을 동적 DOM으로 처리
lee-eojin Apr 11, 2026
a77d683
refactor: observer 흐름 주석 추가 및 변수명 개선
lee-eojin Apr 11, 2026
16e4068
refactor: fetchMoviesApi를 fetchApi로 통합 및 fetchMovieDetailApi 추가
lee-eojin Apr 11, 2026
556f560
feat: 영화 카드 클릭 이벤트 위임 및 MOVIE_SELECTED 이벤트 추가
lee-eojin Apr 11, 2026
1a6880c
feat: 영화 상세 정보 API 핸들러 추가
lee-eojin Apr 11, 2026
7a384fa
feat: dialog 기반 모달 구현 및 무한스크롤 충돌 수정
lee-eojin Apr 12, 2026
0949590
style: 헤더 타이틀 폰트 Figma 기준으로 수정
lee-eojin Apr 12, 2026
ee129d6
feat: 별점 구현 - LocalStorageRatingRepository 및 StarRating 컴포넌트
lee-eojin Apr 12, 2026
9d8b641
style: 모달 Figma 스펙 UI 적용
lee-eojin Apr 12, 2026
72008a0
refactor: 계층별 네이밍 기준 적용 (get/load/handle)
lee-eojin Apr 12, 2026
9ae7212
refactor: Genre 인터페이스 추출
lee-eojin Apr 12, 2026
347f9cc
refactor: PaginatedResponse<T> 제네릭 추출
lee-eojin Apr 12, 2026
cb22129
refactor: BaseMovie 인터페이스 추출 및 Movie, MovieDetail 상속 구조 적용
lee-eojin Apr 12, 2026
bb13291
chore: 코드 가독성 개선 - 주석 정리 및 이벤트 순서 정렬
lee-eojin Apr 12, 2026
2c1c847
refactor: 별점 hover를 CSS :has()로 개선 및 주석 보완
lee-eojin Apr 12, 2026
f8acf72
feat: 로딩 스피너 추가 및 모달 openWithLoading/fill 분리
lee-eojin Apr 13, 2026
141285f
style: 반응형 레이아웃 및 모달 반응형 적용
lee-eojin Apr 13, 2026
0c9c4b8
feat: 더보기 버튼 클릭시 모달이 오픈되도록 구현
lee-eojin Apr 13, 2026
8c71fc9
docs: README 기능 목록 수정 및 설계 결정 작성
lee-eojin Apr 13, 2026
3989ae9
docs: README.md 내용 수정
lee-eojin Apr 13, 2026
4c0c3c5
refactor: 별점 매직 넘버 상수 추출
lee-eojin Apr 13, 2026
8cdac5a
refactor: localStorage 키 prefix 상수 추출
lee-eojin Apr 13, 2026
2433275
refactor: MOVIE_DETAIL_PATH 상수 적용
lee-eojin Apr 13, 2026
596b8bf
feat: TooManyRequestsError(429), ServiceUnavailableError(503) 추가
lee-eojin Apr 13, 2026
54c6b7b
chore: ignoreDeprecations 6.0 추가
lee-eojin Apr 13, 2026
7732130
test: fetchApi 단위 테스트 추가 및 에러 케이스 커버
lee-eojin Apr 13, 2026
a793723
test: E2E 테스트 step-2 시나리오 추가
lee-eojin Apr 13, 2026
8726578
chore: 배포 브랜치 step1 → step2로 변경
lee-eojin 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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy to GitHub Pages
on:
push:
branches:
- step1
- step2

jobs:
deploy:
Expand Down
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"endOfLine": "auto"
"endOfLine": "lf",
"printWidth": 120
}
135 changes: 66 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,98 @@
# javascript-movie-review

FE 레벨1 영화 리뷰 미션
FE 레벨1 영화 리뷰 미션 (step-2)

## 공통 요구사항
## step-2 기능 목록

1. 영화 목록 조회 (인기순)
### 영화 상세 정보 모달

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

2. 검색
- [x] 영화 카드를 클릭하면 상세 정보 모달이 뜬다
- [x] 상세 정보 모달에는 포스터, 제목, 장르, 평점, 줄거리가 표시된다
- [x] 닫기 버튼을 클릭하면 모달이 닫힌다
- [x] ESC 키를 눌러도 모달이 닫힌다

- [x] 영화 검색 API를 이용하여 내가 보고 싶은 영화를 검색할 수 있다.
- [x] 엔터키를 눌러 검색할 수 있다
- [x] 검색 버튼을 클릭하여 검색할 수 있다
- [x] 영화 목록 조회와 같이 검색한 결과에 한해 정보를 보여주는 화면의 요구사항은 동일하다
[UX]

3. 오류
- [x] 헤더의 "자세히 보기" 버튼을 클릭해도 모달이 뜬다
- [x] 모달이 열리는 동안 로딩 스피너를 표시한다
- [x] 모달이 열리면 닫기 버튼으로 포커스가 이동한다

- [x] 오류가 발생하는 경우에는 사용자를 위한 오류 메시지를 띄워 준다.
- [x] 어떤 오류를 대응해야 하고, 어떤 UI로 보여줄 것인지는 자율적으로 결정한다.
### 별점 매기기

4. UI
[기본 기능]

- [x] 다음의 Figma 시안을 기준으로 구현한다.
- [x] 상세 정보 모달에서 영화에 별점을 줄 수 있다
- [x] 별은 5개이며 한 개당 2점이다 (최소 2점, 최대 10점)
- [x] 별점에 따라 텍스트가 달라진다
- 2점: 최악이에요
- 4점: 별로예요
- 6점: 보통이에요
- 8점: 재미있어요
- 10점: 명작이에요
- [x] 새로고침 후에도 별점이 유지된다

5. 배포
[UX]

- [x] 실행 가능한 페이지에 접근할 수 있도록 github page 기능을 이용하고, 해당 링크를 PR과 README에 작성한다.
- [x] 별점을 매기기 전에는 "평가하기" 텍스트가 표시된다
- [x] 별에 hover를 하면 해당 별까지 채워진 상태를 미리 보여준다
- [x] hover 중에는 옆 텍스트(평가하기 / 최악이에요 등)가 변경되지 않는다
- [x] 별을 클릭하면 별점이 확정되고 그때 텍스트가 변경된다
- [x] 별에서 hover를 벗어나면 기존 별점 상태로 돌아온다

## 기능 목록
### 무한스크롤

### 1. 기본 UI
[기본 기능]

- 불러온 영화 목록은 한 행에 5개씩 보여준다.
- 영화 카드에 포스터, 별점, 제목 순으로 표시한다.
- 더보기 버튼 클릭시 다음 페이지 영화 20개를 기존 목록 아래에 추가한다.
- 마지막 페이지면, "더보기" 버튼을 숨긴다.
- [x] 더보기 버튼을 제거하고 무한스크롤로 변경한다
- [x] 목록 맨 아래에 도달하면 다음 페이지를 자동으로 불러온다
- [x] 검색 결과에서도 무한스크롤이 동작한다
- [x] 마지막 페이지에 도달하면 추가 요청을 하지 않는다

#### 검색 시 UI
### 반응형 레이아웃

##### 검색 결과가 있을 시
[기본 기능]

- 검색 결과를 영화 목록에 표시하고, 섹션 제목을 "검색어" 검색 결과로 표시한다.

##### 검색 결과가 없을 시

- 검색 결과가 없으면 "검색 결과가 없습니다" 를 표시한다.

#### 로딩 UI

- API 호출 중 스켈레톤 UI를 표시한다.
- 응답이 오면 스켈레톤을 실제 데이터로 교체한다.

#### 예외 처리

- API 호출 오류 시 오류 메시지를 화면에 출력한다.

### 2. 이벤트 처리

- TMDB API에서 인기 영화 목록을 가져온다.
- 엔터키와 검색 버튼을 이용하여 검색할 수 있다.
- 검색어를 지우면 인기 영화 목록으로 복귀한다.
- [x] 디바이스 너비에 따라 영화 목록 카드 열 수가 달라진다
- [x] 디바이스 너비에 따라 모달 레이아웃이 달라진다

---

## 아키텍처
## step-2 핵심 설계 결정

![아키텍처](src/images/flow.png)
### 별점 hover — CSS only

### 레이어 구조
`star_empty.png`와 `star_filled.png`를 하나의 이미지(`stars_sprite.png`)로 합쳐서,
`background-position`만으로 빈 별 / 채운 별 상태를 전환하도록 설계했다.

```
main.ts DOM 이벤트 바인딩만
├─ init.ts EventBus 구독 등록 + UI 업데이트
└─ controllerHandlers state 조작 + 이벤트 publish
└─ dataHandlers API 호출 (read*)
└─ fetchMoviesApi fetch + status 검증 + 커스텀 에러 throw
└─ errors.ts ApiError / UnauthorizedError / NotFoundError

pubsub/
├─ EventBus.ts subscribe / publish 메커니즘
└─ AppEvents.ts 이벤트 상수 + payload 타입 매핑

features/ui/ 컴포넌트 (Header, MovieList, MovieCard, MovieSkeleton)
state.ts 매 호출마다 바뀌는 값 (page, searchQuery)
stars_sprite.png: [ 빈 별 | 채운 별 ]
background-size: 200% 100% → 한 번에 절반만 보이게
background-position: 0% → 빈 별
background-position: 100% → 채운 별
```
Comment on lines 69 to +74
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 | 🟡 Minor

코드 펜스 언어를 지정해 주세요.

Line 69-74의 fenced code block은 언어가 빠져 있어서 markdownlint(MD040)에 걸립니다. 설명용이면 text, 스타일 예시면 css처럼 명시해 두는 편이 좋습니다.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 69-69: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@README.md` around lines 69 - 74, The fenced code block showing sprite notes
lacks a language tag and triggers markdownlint MD040; update that block in
README.md (the triple-backtick block containing "stars_sprite.png: [ 빈 별 | 채운 별
]" and the background-size/position lines) by adding an appropriate language
identifier (e.g., use "text" for plain explanation or "css" if you want to
indicate styling snippets) after the opening ``` so the linter recognizes the
block type.


hover 방향 처리는 CSS `:has()` + `flex-direction: row-reverse` + `~` 형제 선택자 조합으로 구현했다.
DOM 순서는 별 5→1 (역순)이고 `flex-direction: row-reverse`로 화면에 1→5로 표시한다.
`~` 선택자가 "이후 형제"만 선택하는 특성을 역순 배치와 조합해 hover 시 왼쪽 별을 채운다.

```css
.star-list:has(label:hover) label:hover,
.star-list label:hover ~ label {
background-position: 100% 0%;
}
```

### 전체 플로우
### 별점 저장소 — 의존성 주입

진입점은 가장 먼저 EventBus 구독 설정 함수를 호출해 모든 구독자를 등록한다. 이 단계를 거쳐야 이후 발행되는 이벤트가 구독자에게 전달될 수 있다. 이후 진입점은 DOM 이벤트(load / submit / click)만 바인딩하고, 각 이벤트는 컨트롤러의 핸들러를 호출한다.
`IRatingRepository` 인터페이스를 정의하고 `Modal`이 구현체를 주입받는 구조로 설계했다.
현재는 `LocalStorageRatingRepository`를 사용하지만, 서버 API 구현체로 교체해도 `StarRating`과 `Modal` 코드는 변경이 없도록 설계했다.

컨트롤러는 상태를 조작하고 로드 함수를 호출한다. 로드 함수는 항상 같은 순서로 동작한다 — 타이틀 변경 이벤트를 발행하고, 로딩 시작 이벤트를 발행한 뒤, 데이터 레이어를 통해 API를 호출하고, 응답이 오면 데이터 로드 완료 이벤트를 발행한다. 더보기 흐름만 예외적으로 화면을 새로 그리지 않으므로 타이틀 변경과 로딩 시작 이벤트를 생략한다.
`localStorage`는 동기 API라 `async`일 이유가 없지만, 나중에 서버 API로 교체할 때 호출부를 바꾸지 않아도 되도록
인터페이스의 `save` / `load`를 `Promise`를 반환하는 형태로 미리 맞춰두었다.

구독 레이어는 각 이벤트를 받아 헤더와 영화 목록을 렌더링하고, 더보기 버튼의 표시 여부를 결정합니다. 검색 결과가 비어 있으면 "검색 결과 없음" UI를, API 에러가 발생하면 에러 메시지를 화면에 표시한다.
### 무한스크롤 — observer 생명주기

API 레이어는 `response.ok`가 false일 때 status에 따라 적절한 커스텀 에러를 던진다. 데이터 레이어는 에러를 가로채지 않고 그대로 위로 전파하며, 컨트롤러가 비로소 try/catch로 잡아 에러 이벤트를 발행한다. 에러의 원본 타입이 컨트롤러까지 살아 있어 향후 `instanceof`로 분기 확장이 가능하다.
로딩 중 또는 모달이 열린 동안 sentinel이 뷰포트에 진입해도 중복 요청이 발생하지 않도록,
`LOAD_START`와 `MOVIE_SELECTED` 이벤트에서 observer를 disconnect하고 렌더 완료 후 재등록한다.
50 changes: 33 additions & 17 deletions __tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vi, expect, test, describe, afterEach } from "vitest";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1.

코드를 관심사별로 분류한 것 처럼 E2E 테스트 시나리오도 유저 시나리오(행동) 기반으로 작성해보시면 좋겠어요.

예를들어 인기 목록을 조회하고 행동하는 과정에서 잘 동작하는지, 에러케이스는 잘 대응되고 있는지, 검색과정, 별점 매기는 과정 등을 각각 유저 행동 시나리오로 분류해볼 수 있을 것 같아요.

테스트 코드는 기능이 잘 동작하는지 확인하기 위한 용도도 있지만, 이 서비스가 어떤 기능과 스펙으로 구성되어있는지, 어떻게 동작하는지 등등을 이해하는데에도 도움이 돼요.

import { Movie } from "../types/types";
import { fetchMoviesApi } from "../src/features/api/fetchMoviesApi";
import { Movie, MovieResponse } from "../types/types";
import { fetchApi } from "../src/features/api/fetchApi";

const MOCK_MOVIE: Movie = {
id: 1,
Expand All @@ -15,34 +15,50 @@ const MOCK_RESPONSE = {
total_pages: 11,
};

describe("TMDB API에서 인기 영화 목록을 가져온다.", () => {
describe("TMDB API", () => {
afterEach(() => {
vi.restoreAllMocks();
Comment on lines 19 to 20
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expectation:
# - Either a Vitest config with `unstubGlobals: true`
# - Or an explicit `vi.unstubAllGlobals()` in test cleanup

rg -n 'stubGlobal\("fetch"|unstubAllGlobals\(|unstubGlobals' .

Repository: woowacourse/javascript-movie-review

Length of output: 728


🏁 Script executed:

# Check the full afterEach block and surrounding context
cat -n __tests__/api.test.ts | sed -n '15,30p'

Repository: woowacourse/javascript-movie-review

Length of output: 594


🏁 Script executed:

# Search for Vitest configuration files
fd -e ts -e js -e json '(vitest|vite)\.config' . -type f

Repository: woowacourse/javascript-movie-review

Length of output: 249


🏁 Script executed:

# Check package.json for Vitest version and any config there
cat package.json | grep -A 20 -B 5 'vitest\|vite'

Repository: woowacourse/javascript-movie-review

Length of output: 1205


🏁 Script executed:

# Correct fd command to find config files
find . -maxdepth 2 -type f \( -name 'vitest.config.*' -o -name 'vite.config.*' \)

Repository: woowacourse/javascript-movie-review

Length of output: 61


🏁 Script executed:

# Also check if config is inline in package.json
cat package.json | grep -A 50 '"vitest"'

Repository: woowacourse/javascript-movie-review

Length of output: 982


🏁 Script executed:

# Final check: ensure no unstubAllGlobals calls anywhere in the test file
rg 'unstubAllGlobals' __tests__/api.test.ts

Repository: woowacourse/javascript-movie-review

Length of output: 61


🏁 Script executed:

# Check the entire afterEach and surrounding test structure for any cleanup patterns
cat -n __tests__/api.test.ts | head -80

Repository: woowacourse/javascript-movie-review

Length of output: 2520


vi.stubGlobal()의 정리 방법을 재검토해 주세요.

vi.restoreAllMocks()는 mock과 spy를 복원하지만, vi.stubGlobal()로 생성한 전역 스텁은 별도로 정리해야 합니다.

다음 점들을 확인해 보세요:

  • Vitest 문서에서 전역 스텁(global stubs) 정리 방법을 찾아보기
  • vi.unstubAllGlobals()가 필요한지, 아니면 vitest 설정에서 지원하는 옵션이 있는지 확인

현재 코드에서는 각 테스트마다 fetch를 새로 stubGlobal하고 있는데, 이전 테스트의 stub이 남아있으면 테스트 실행 순서에 따라 예상치 못한 동작이 발생할 수 있습니다.

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

In `@__tests__/api.test.ts` around lines 19 - 20, The tests currently call
vi.restoreAllMocks() in afterEach but do not undo globals created with
vi.stubGlobal(), which can leave stale global stubs like fetch between tests;
update the afterEach cleanup to also call vi.unstubAllGlobals() (or the
appropriate vitest API/setting for global teardown) so any
vi.stubGlobal('fetch', ...) created in tests is removed after each test; locate
the afterEach block in __tests__/api.test.ts and add the unstub call alongside
vi.restoreAllMocks() or configure vitest to automatically unstub globals if
preferred.

});

test("API 성공 시 데이터 반환", async () => {
global.fetch = vi.fn(async () => ({
ok: true,
json: () => MOCK_RESPONSE,
})) as any;
test("인기 영화 목록을 가져온다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: () => MOCK_RESPONSE })));

const data: { results: Movie[]; total_pages: number } =
await fetchMoviesApi("movie/popular", 1);
const data : MovieResponse = await fetchApi("movie/popular", 1);

expect(data.results[0].title).toBe("Test Movie");
expect(data.total_pages).toBe(11);
});

test("검색 API 성공 시 데이터 반환", async () => {
global.fetch = vi.fn(async () => ({
ok: true,
json: () => MOCK_RESPONSE,
})) as any;
test("검색어로 영화 목록을 가져온다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: () => MOCK_RESPONSE })));

const data: { results: Movie[]; total_pages: number } =
await fetchMoviesApi("search/movie", 1, "아바타");
const data : MovieResponse = await fetchApi("search/movie", 1, "아바타");

expect(data.results[0].title).toBe("Test Movie");
expect(data.total_pages).toBe(11);
});

test("401 응답 시 UnauthorizedError를 던진다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 401 })));

await expect(fetchApi("movie/popular", 1)).rejects.toThrow("인증에 실패했습니다.");
});

test("404 응답 시 NotFoundError를 던진다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 404 })));

await expect(fetchApi("movie/popular", 1)).rejects.toThrow("요청한 리소스를 찾을 수 없습니다.");
});

test("429 응답 시 TooManyRequestsError를 던진다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 429 })));

await expect(fetchApi("movie/popular", 1)).rejects.toThrow("요청이 너무 많습니다.");
});

test("503 응답 시 ServiceUnavailableError를 던진다", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 503 })));

await expect(fetchApi("movie/popular", 1)).rejects.toThrow("서버가 일시적으로 사용 불가 상태입니다.");
});
});
68 changes: 53 additions & 15 deletions cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,6 @@ 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.

페이지네이션(무한스크롤) 시나리오도 잘 동작하는지 테스트 시나리오를 넣으면 좋을 것 같은데 어떤가요!?

});

describe("더보기", () => {
beforeEach(() => {
cy.visit("localhost:5173");
cy.get(".thumbnail-list li").should("have.length.greaterThan", 0);
});

it("더보기 클릭 시 영화 카드가 추가된다", () => {
cy.get(".thumbnail-list li")
.its("length")
.then((before) => {
cy.get(".btn-more", { timeout: 8000 }).click();
cy.get(".thumbnail-list li").should("have.length.greaterThan", before);
});
});
});

describe("검색", () => {
beforeEach(() => {
Expand All @@ -49,6 +34,59 @@ describe("검색", () => {
});
});

describe("에러", () => {
it("API 실패 시 에러 메시지가 표시된다", () => {
cy.intercept("GET", "**/movie/popular**", { statusCode: 401 }).as("failedRequest");
cy.visit("localhost:5173");
cy.wait("@failedRequest");
cy.get(".result-none-text").should("be.visible");
});
});

describe("모달", () => {
beforeEach(() => {
cy.visit("localhost:5173");
cy.get(".thumbnail-list li").should("have.length.greaterThan", 0);
});

it("카드 클릭 시 스피너가 표시되다가 상세 정보로 전환된다", () => {
cy.get(".thumbnail-list li").first().click();
cy.get(".spinner").should("be.visible");
cy.get(".modal-description").should("be.visible");
Comment on lines +52 to +55
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

스피너 검증이 비결정적이라 플래키해질 수 있습니다.

상세 API 응답이 빠르면 .spinner가 보였다가 사라지는 구간을 Cypress가 놓쳐서 이 테스트가 간헐적으로 실패할 수 있습니다. 이 케이스는 상세 요청을 intercept해서 의도적으로 지연시키고, 응답 전/후 상태를 나눠서 검증하는 쪽이 더 안정적입니다.

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

In `@cypress/e2e/spec.cy.ts` around lines 52 - 55, The spinner assertion is flaky
because the real detail API can return before Cypress sees the spinner; update
the test "카드 클릭 시 스피너가 표시되다가 상세 정보로 전환된다" to intercept the detail API call
(using cy.intercept, alias it, and inject a deliberate network delay or use a
stubbed delayed response), then click the first ".thumbnail-list li", assert
".spinner" is visible while the intercepted request is pending, call
cy.wait('@yourAlias') to let the response complete, and finally assert
".modal-description" is visible and ".spinner" is gone.

});
});

describe("별점", () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit("localhost:5173");
cy.get(".thumbnail-list li").should("have.length.greaterThan", 0);
cy.get(".thumbnail-list li").first().click();
cy.get(".modal-description").should("be.visible");
});

it("별점을 매기기 전에는 평가하기 텍스트가 표시된다", () => {
cy.get(".rating-label").should("contain.text", "평가하기");
});

it("별을 클릭하면 별점 텍스트가 반영된다", () => {
cy.get(".star-list label").last().click();
cy.get(".rating-label").should("contain.text", "최악이에요");
});

it("새로고침 후에도 별점이 유지된다", () => {
cy.get(".star-list label").last().click();
cy.get(".rating-label").should("contain.text", "최악이에요");

cy.get(".close-modal").click();
cy.reload();
cy.get(".thumbnail-list li").should("have.length.greaterThan", 0);
cy.get(".thumbnail-list li").first().click();
cy.get(".modal-description").should("be.visible");
cy.get(".rating-label").should("contain.text", "최악이에요");
});
});

describe("홈으로 이동", () => {
beforeEach(() => {
cy.visit("localhost:5173");
Expand Down
6 changes: 3 additions & 3 deletions cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "node",
"types": ["node", "cypress"],
"module": "node16",
"moduleResolution": "node16",
"types": ["node"],
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 -euo pipefail

echo "[1] cy/Cypress 사용 위치"
rg -n --type ts '\b(cy|Cypress)\b' cypress || true

echo
echo "[2] cypress 타입 참조(파일별)"
rg -n --type ts 'reference types="cypress"' cypress || true

echo
echo "[3] 각 spec/support 파일의 상단 참조 확인"
fd -e ts . cypress | sort

Repository: woowacourse/javascript-movie-review

Length of output: 4760


🏁 Script executed:

cat -n cypress/tsconfig.json

Repository: woowacourse/javascript-movie-review

Length of output: 593


🏁 Script executed:

# Check if spec.cy.ts has any imports or references that might provide Cypress types
head -20 cypress/e2e/spec.cy.ts

Repository: woowacourse/javascript-movie-review

Length of output: 573


🏁 Script executed:

# Check if e2e.ts imports or references anything
head -20 cypress/support/e2e.ts

Repository: woowacourse/javascript-movie-review

Length of output: 746


spec.cy.ts에 Cypress 타입 참조가 없어 타입 에러 발생

Line 7에서 types에서 cypress를 제거하면, cy 메서드를 사용하는 cypress/e2e/spec.cy.ts가 TypeScript 오류를 일으킵니다. spec.cy.ts/// <reference types="cypress" />가 없고, commands.ts의 타입 참조는 해당 파일에만 적용되기 때문입니다.

spec.cy.ts에서 cy.visit(), cy.get() 등이 수십 줄에 걸쳐 사용되고 있으므로, 다음 중 하나의 해결책이 필요합니다:

  1. tsconfig.json에 cypress 타입 유지 (권장):
-      "types": ["node"],
+      "types": ["node", "cypress"],
  1. 또는 spec.cy.ts 상단에 개별 참조 추가 (파일마다 반복 필요):
+/// <reference types="cypress" />
 describe("초기 진입", () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"types": ["node"],
"types": ["node", "cypress"],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypress/tsconfig.json` at line 7, The TypeScript errors come from removing
"cypress" from the "types" array in cypress/tsconfig.json so Cypress global
types (cy, Cypress, etc.) are no longer available; restore "cypress" to the
"types" array in cypress/tsconfig.json (e.g., change "types": ["node"] to
include "cypress") OR alternatively add a triple-slash reference /// <reference
types="cypress" /> at the top of cypress/e2e/spec.cy.ts so that cy.visit(),
cy.get(), and other Cypress globals resolve; update whichever file
(cypress/tsconfig.json or spec.cy.ts) contains the project-wide type config or
the test file respectively.

"skipLibCheck": true,
"noEmit": true,
"strict": true,
Expand Down
15 changes: 11 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&family=Montserrat:wght@300;400;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./src/styles/reset.css" />
<link rel="stylesheet" href="./src/styles/main.css" />
<link rel="stylesheet" href="./src/styles/tab.css" />
<link rel="stylesheet" href="./src/styles/thumbnail.css" />
<link rel="stylesheet" href="./src/styles/modal.css" />
<title>영화 리뷰</title>
</head>
<body>
Expand All @@ -17,10 +21,12 @@
<div class="container">
<main>
<section>
<h2 class="main-title">지금 인기 있는 영화</h2>
<div class="main-result"></div>
<ul class="thumbnail-list"></ul>
<button class="btn-more" style="display: none">더 보기</button>
<div class="movie-section">
<h2 class="main-title">지금 인기 있는 영화</h2>
<div class="main-result"></div>
<ul class="thumbnail-list"></ul>
</div>
<div class="scroll-sentinel"></div>
</section>
</main>
</div>
Expand All @@ -31,6 +37,7 @@ <h2 class="main-title">지금 인기 있는 영화</h2>
<p>&copy; 우아한테크코스 All Rights Reserved.</p>
</footer>
</div>
<dialog class="modal"></dialog>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions src/constants/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const BACKDROP_IMAGE_URL: string =
"https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/";

export const THUMB_NAIL_URL: string = "https://media.themoviedb.org/t/p/w200";

export const POSTER_URL: string = "https://image.tmdb.org/t/p/w500";
Loading