-
Notifications
You must be signed in to change notification settings - Fork 155
[2단계 - 상세 정보 & UI/UX 개선하기] 레스 미션 제출합니다. #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: lee-eojin
Are you sure you want to change the base?
Changes from all commits
3ff14f5
548df30
b6df0ab
fcd70db
2ea61fb
7d4bcea
eefad93
5da001b
ca6e882
fb1d599
1af32c7
56052e4
d492562
a77d683
16e4068
556f560
1a6880c
7a384fa
0949590
ee129d6
9d8b641
72008a0
9ae7212
347f9cc
cb22129
bb13291
2c1c847
f8acf72
141285f
0c9c4b8
8c71fc9
3989ae9
4c0c3c5
8cdac5a
2433275
596b8bf
54c6b7b
7732130
a793723
8726578
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ name: Deploy to GitHub Pages | |
| on: | ||
| push: | ||
| branches: | ||
| - step1 | ||
| - step2 | ||
|
|
||
| jobs: | ||
| deploy: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| { | ||
| "endOfLine": "auto" | ||
| "endOfLine": "lf", | ||
| "printWidth": 120 | ||
| } |
| 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 핵심 설계 결정 | ||
|
|
||
|  | ||
| ### 별점 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% → 채운 별 | ||
| ``` | ||
|
|
||
| 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하고 렌더 완료 후 재등록한다. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { vi, expect, test, describe, afterEach } from "vitest"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 fRepository: 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.tsRepository: 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 -80Repository: woowacourse/javascript-movie-review Length of output: 2520
다음 점들을 확인해 보세요:
현재 코드에서는 각 테스트마다 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| 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("서버가 일시적으로 사용 불가 상태입니다."); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,21 +9,6 @@ describe("초기 진입", () => { | |
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(() => { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스피너 검증이 비결정적이라 플래키해질 수 있습니다. 상세 API 응답이 빠르면 🤖 Prompt for AI Agents |
||
| }); | ||
| }); | ||
|
|
||
| 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"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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"], | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 | sortRepository: woowacourse/javascript-movie-review Length of output: 4760 🏁 Script executed: cat -n cypress/tsconfig.jsonRepository: 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.tsRepository: woowacourse/javascript-movie-review Length of output: 573 🏁 Script executed: # Check if e2e.ts imports or references anything
head -20 cypress/support/e2e.tsRepository: woowacourse/javascript-movie-review Length of output: 746
Line 7에서
- "types": ["node"],
+ "types": ["node", "cypress"],
+/// <reference types="cypress" />
describe("초기 진입", () => {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "skipLibCheck": true, | ||||||
| "noEmit": true, | ||||||
| "strict": true, | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드 펜스 언어를 지정해 주세요.
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