Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1b4f372
feat: 영화 목록을 받아오는 fetch 구현 및 클릭 이벤트 테스트
iftype Apr 8, 2026
7a9e41d
feat: Modal 구현
iftype Apr 9, 2026
a3960c0
feat: 별점을 제출하는 형식 정의
iftype Apr 9, 2026
0e15c68
feat: 로컬 스토리지 사용하여 평점기능추가
iftype Apr 9, 2026
3347c6d
refactor: Search Page 모달 적용
iftype Apr 9, 2026
4aebacb
refactor: 리스트 이벤트 위임으로 변경
iftype Apr 9, 2026
c52cd17
style: 반응형 레이아웃 설정
iftype Apr 9, 2026
f358ea2
feat: 무한 스크롤 구현
iftype Apr 9, 2026
9ed6647
feat: AP 중복 호출 문제 해결
iftype Apr 9, 2026
934b327
stype: 검색창 css 수정
iftype Apr 9, 2026
7b9e255
test: 스크롤로 바뀜에 따라 테스트 수정
iftype Apr 9, 2026
60cf7d4
feat: 페이지 캐싱적용
iftype Apr 9, 2026
cc3739f
fix: 페이지 캐싱 적용 중 생긴 버그 해결
iftype Apr 9, 2026
4606ee1
feat: Repository 레이어 도입
iftype Apr 10, 2026
ddbc854
fix: console log 삭제
iftype Apr 10, 2026
a5f0828
style: 모달 레이아웃 변경
iftype Apr 10, 2026
d9f25ba
feat: modal 키다운 이벤트 추가
iftype Apr 10, 2026
1c0f52c
fix: console log 삭제
iftype Apr 10, 2026
1a298a0
feat: 레포지토리 비동기로 매핑
iftype Apr 10, 2026
07401a9
chore: 주석 제거 및 중복 스타일 제거
iftype Apr 10, 2026
9ea392a
fix: 타이핑 오류 수정
iftype Apr 10, 2026
32a09d4
refactor: Error 컴포넌트 위치 변경
iftype Apr 10, 2026
cd7cda0
test: 모달과 별점 테스트 추가
iftype Apr 10, 2026
34f4bec
fix: 스크롤 내린 후 새로고침 문제 해결
iftype Apr 11, 2026
8600412
chore: window 객체 명시
iftype Apr 12, 2026
473fb51
refactor: 불필요한 Repo 레이어 제거
iftype Apr 12, 2026
05c9606
refactor: IntersectionObserver 도입 및 중복 로직 삭제
iftype Apr 12, 2026
80c0f9b
refactor: 모달의 이미지 스켈레톤 도입
iftype Apr 12, 2026
f7b6367
refactor: HomePage, SearchPage 통합
iftype Apr 13, 2026
be28652
refactor: Modal 생성 위치 변경
iftype Apr 13, 2026
39db170
refactor: TopRateHeader와 SearchHeader 통합
iftype Apr 13, 2026
332e839
test: 로컬 스토리지가 아닌 실제 요소를 보도록 수정
iftype Apr 13, 2026
5ea837e
fix: 모달 스켈레톤 에러 수정
iftype Apr 13, 2026
03970e1
feat: 헤더의 버튼에 디테일 이벤트 추가
iftype Apr 13, 2026
532d0b4
refactor: 엔트리포인트 부터 의존성 주입
iftype 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
1 change: 1 addition & 0 deletions __test__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchPopularMovies, fetchSearchMovies } from '../src/api/fetchApi.ts';
import MOCK_DATA from './mock/page_1.json';
import MOCK_ERROR from './mock/page_error.json';

vi.stubGlobal('fetch', vi.fn());

Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ describe('영화 리뷰 앱 E2E 테스트', () => {
cy.get('.thumbnail-list > li').should('have.length', 20);
});

it('더보기 버튼을 누르면 다음 페이지의 영화 20개를 추가로 렌더링한다.', () => {
it('스크롤을 내리면 다음 페이지의 영화 20개를 추가로 렌더링한다.', () => {
cy.visit('http://localhost:5175/');
cy.wait('@getPopularMovies');

cy.get('.more-button').click();
cy.scrollTo('bottom');
cy.wait('@getNextPopularMovies');

cy.get('.thumbnail-list > li').should('have.length', 40);
Comment on lines +34 to 41
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
# Description: Check if there are any scroll-related test utilities or configurations

# Search for any existing scroll test helpers or wait configurations
rg -n "scroll" --type=ts -C2 cypress/

Repository: woowacourse/javascript-movie-review

Length of output: 315


🏁 Script executed:

# Search for scroll event handlers and custom event implementation
rg -n "SCROOL_END|scroll.*Event|scrollTo" --type=ts -C3 src/

Repository: woowacourse/javascript-movie-review

Length of output: 2455


🏁 Script executed:

# Check the event utilities mentioned in the comment
cat -n src/utils/event.ts | head -50

Repository: woowacourse/javascript-movie-review

Length of output: 873


🏁 Script executed:

# Look for API interceptor setup to understand `@getNextPopularMovies`
rg -n "getNextPopularMovies|@getPopularMovies|cy\.intercept" --type=ts -C2 cypress/

Repository: woowacourse/javascript-movie-review

Length of output: 2431


스크롤 기반 테스트의 구조는 적절합니다.

cy.scrollTo('bottom')은 네이티브 스크롤 이벤트를 발생시키고, src/main.ts의 스크롤 리스너가 이를 감지하여 scrollEvent()를 호출합니다. src/utils/event.ts에서 300ms 쓰로틀을 거쳐 CUSTOM_EVENT.SCROOL_END를 디스패치하고, 페이지가 이 이벤트를 수신하여 API 요청을 트리거합니다. cy.wait('@getNextPopularMovies')는 기본값으로 5초 동안 대기하므로, 300ms 쓰로틀 딜레이는 충분히 처리됩니다.

현재 테스트 구조는 단일 스크롤 액션이므로 타이밍 문제 없이 작동해야 합니다. 다만, CI 환경에서 실제로 안정적으로 동작하는지 여러 번 실행해 확인하면 좋습니다.

🤖 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 26 - 33, The test can flake on CI due to
the 300ms throttle in src/utils/event.ts and network variability; update the
test that calls cy.scrollTo('bottom') to wait longer for the API alias by
changing cy.wait('@getNextPopularMovies') to cy.wait('@getNextPopularMovies', {
timeout: 10000 }) (or another increased timeout) so the
scrollEvent()/CUSTOM_EVENT.SCROOL_END debounce in src/main.ts has ample time to
trigger the second fetch after the initial cy.wait('@getPopularMovies'); keep
the same aliases and assertions ('.thumbnail-list > li' length check) but
increase the wait timeout to stabilize CI runs.

Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="stylesheet" href="./styles/main.css" />
<link rel="stylesheet" href="./styles/tab.css" />
<link rel="stylesheet" href="./styles/thumbnail.css" />
<link rel="stylesheet" href="./styles/modal.css" />
</head>

<body>
Expand Down
1 change: 1 addition & 0 deletions src/api/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const URL = {
export const PATH = {
MOVIE_POPULAR: '/movie/popular',
SEARCH_MOVIE: '/search/movie',
MOVIE_DETAIL: (movie_id: number) => `/movie/${movie_id}`,
} as const;
16 changes: 12 additions & 4 deletions src/api/fetchApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { URL, PATH } from './constant.ts';
import TMDBError from './TMDBError.ts';
import { ResponseMovie, Request, TmdbErrorType } from './types.ts';
import { ResponseMovie, Request, TmdbErrorType, MovieDetail } from './types.ts';

const API_KEY = import.meta.env.VITE_API_KEY;
const options = {
Expand All @@ -11,14 +11,15 @@ const options = {
},
};

const fetchAPI = async (req: Request): Promise<ResponseMovie> => {
const fetchAPI = async <T extends ResponseMovie | MovieDetail>(req: Request): Promise<T> => {
const url = URL.BASE + req.path;

const { query, page } = req.params;
const params = new URLSearchParams({ language: 'ko-KR', page: String(page), region: 'kr' });
const params = new URLSearchParams({ language: 'ko-KR', region: 'kr' });
if (query) params.set('query', query);

if (page) params.set('page', String(page));
const resultUrl = url + '?' + params.toString();
console.log(resultUrl);
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

디버깅용 console.log 제거 필요

프로덕션 코드에 디버깅용 console.log가 남아있습니다. 배포 전에 제거하거나, 필요하다면 환경 변수를 통해 개발 모드에서만 출력되도록 조건부 로깅을 적용하는 것이 좋습니다.

🔧 제안하는 수정
-  console.log(resultUrl);
📝 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
console.log(resultUrl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/fetchApi.ts` at line 22, Remove the leftover debugging
console.log(resultUrl) from src/api/fetchApi.ts (or guard it behind a dev-only
check); either delete the console.log call or wrap it so it only runs in
non-production (e.g., check process.env.NODE_ENV or a DEBUG env var) to prevent
debug output in production builds while preserving dev visibility when needed.


const response = await fetch(resultUrl, options);
const data = await response.json();
Expand All @@ -43,3 +44,10 @@ export const fetchPopularMovies = (page: number = 1): Promise<ResponseMovie> =>
params: { page },
});
};

export const fetchMovieDetails = (movie_id: number): Promise<MovieDetail> => {
return fetchAPI({
path: PATH.MOVIE_DETAIL(movie_id),
params: {},
});
};
16 changes: 15 additions & 1 deletion src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
export type MovieData = {
id: number;
title: string;
poster_path: string;
backdrop_path: string;
vote_average: number;
};

type Genres = {
name: string;
};
export type MovieDetail = {
id: number;
title: string;
overview: string;
poster_path: string;
genres: Genres[];
vote_average: string;
release_date: string;
};
Comment on lines +12 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

🌐 Web query:

TMDB API movie details vote_average type

💡 Result:

In TMDB’s Movie Details response (GET /3/movie/{movie_id}), vote_average is a JSON number (i.e., a float/double), representing the movie’s average user rating. The value may be returned with more than 1 decimal place (TMDB has discussed it showing up to ~3 decimals at times). [1][2]

Sources:
[1] (gist.github.com)
[2] (themoviedb.org)

Citations:


MovieDetail.vote_average 타입 수정 필요

TMDB API 검증 결과에 따르면 vote_average는 **숫자 타입(JSON number)**으로 반환됩니다.

현재 코드에서:

  • MovieData.vote_averagenumber로 올바르게 정의됨
  • MovieDetail.vote_averagestring으로 정의됨

두 타입이 불일치합니다.

다음 질문을 고려하며 해결 방법을 생각해 보세요:

  • TMDB API가 실제로 반환하는 타입은 무엇인가요?
  • 두 타입 정의가 같은 데이터 소스를 다루고 있다면, 왜 다르게 정의되어 있을까요?
  • 타입이 일치하지 않으면 데이터 처리 시 어떤 문제가 발생할 수 있을까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/types.ts` around lines 12 - 20, MovieDetail.vote_average is
incorrectly typed as string while TMDB returns a JSON number and
MovieData.vote_average is already a number; update the MovieDetail type to use
number for vote_average to match the API and MovieData, then search for usages
of MovieDetail.vote_average (e.g., any code that assumes a string) and update
parsing/formatting (toFixed, toString, etc.) as needed to handle numeric values
consistently.


export type ResponseMovie = {
results: MovieData[];
page: number;
total_pages: number;
};

type Params = {
page: number;
page?: number;
query?: string | undefined;
language?: string;
region?: string;
Expand Down
14 changes: 14 additions & 0 deletions src/components/common/Rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Star } from './Star';

export const Rate = (vote_average: string, filled?: boolean) => {
const $p = document.createElement('p');
$p.className = 'rate';

const $span = document.createElement('span');

const roundVote = Math.round(Number(vote_average) * 10) / 10;
$span.textContent = roundVote.toFixed(1);

$p.append(Star(filled), $span);
return $p;
};
6 changes: 3 additions & 3 deletions src/components/common/Star.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const Star = () => {
export const Star = (filled?: boolean) => {
const $img = document.createElement('img');
$img.className = 'star';
$img.src = './images/star_empty.png';
$img.alt = 'star_empty';
$img.src = filled ? './images/star_filled.png' : './images/star_empty.png';
$img.alt = filled ? 'star_filled' : 'star_empty';

return $img;
};
5 changes: 4 additions & 1 deletion src/components/header/SearchForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ export const SearchForm = (onSubmit: (query: string) => void) => {

$form.addEventListener('submit', (e) => {
e.preventDefault();
const query = $<HTMLInputElement>($form, '#search-input').value;

const $input = $<HTMLInputElement>($form, '#search-input');
const query = $input.value;
onSubmit(query);
$input.value = '';
});
return $form;
};
5 changes: 3 additions & 2 deletions src/components/header/SearchHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export default class SearchHeader {
this.#$element = document.createElement('header');
this.#$element.innerHTML = `
<div class="background-container">
<div class="top-rated-container"></div>
<div class="search-container"></div>
</div>
`;

$<HTMLElement>(this.#$element, '.top-rated-container').append(Logo(), SearchForm(onSubmit));
const $justLayout = document.createElement('div');
$<HTMLElement>(this.#$element, '.search-container').append(Logo(), SearchForm(onSubmit), $justLayout);
}

get $element() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/header/TopRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export const TopRate = (data: MovieData): HTMLElement => {
$container.className = 'top-rated-movie';

$container.innerHTML = `
<div class="title"></div>
<div class="rate">
<span class="rate-value">${data.vote_average.toFixed(1)}</span>
</div>
<div class="title"></div>
<button class="primary detail">자세히 보기</button>
`;

Expand Down
49 changes: 26 additions & 23 deletions src/components/main/Main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { MovieData } from '../../api/types.ts';
import { $ } from '../../utils/dom.ts';
import { Error } from './Error.ts';
import { MoreButton } from './MoreButton.ts';
import { MovieItem } from './MovieItem.ts';
import { MovieItemSkeleton } from './MovieItemSkeleton.ts';
import { NothingResult } from './NothingResult.ts';

export default class Main {
#$element: HTMLElement;
#$list: HTMLElement;
#$skeletons: HTMLElement[] = [];
#$moreButton: HTMLElement | null;
#$skeletons: Map<string, HTMLElement[]>;

constructor(title: string, onDetail: (movie_id: number) => void) {
this.#$skeletons = new Map<string, HTMLElement[]>();

constructor(title: string) {
this.#$element = document.createElement('div');
this.#$element.className = 'container';
this.#$element.innerHTML = `
Expand All @@ -24,38 +24,41 @@ export default class Main {
</main>
`;
this.#$list = $<HTMLElement>(this.#$element, '.thumbnail-list');
this.#$moreButton = null;

const $ul = $(this.#$element, 'ul');
$ul.addEventListener('click', (e) => {
const $li = (e.target as Element).closest('li');
const id = $li?.dataset.id;
onDetail(Number(id));
});
Comment on lines +26 to +31
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

클릭 이벤트 위임 시 유효성 검사가 필요합니다.

현재 구현에서 ul 영역을 클릭했지만 li 요소가 아닌 경우(예: 목록 사이의 빈 공간), $linull이 되어 onDetail(NaN)이 호출될 수 있습니다.

이 경우를 어떻게 처리하면 좋을지 생각해 보시겠어요?

💡 힌트
    $ul.addEventListener('click', (e) => {
      const $li = (e.target as Element).closest('li');
+     if (!$li) return;
      const id = $li?.dataset.id;
      onDetail(Number(id));
    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/main/Main.ts` around lines 28 - 33, Click handler on $ul can
call onDetail(NaN) when the click target isn't an li or the li has no data-id;
update the listener in Main.ts to validate the clicked element and id before
calling onDetail: inside the $ul click callback (the $ul variable and existing
$li extraction), check that $li is non-null and that $li.dataset.id is defined
(use parseInt or Number safely), and return early if validation fails so
onDetail is only invoked with a valid numeric id; ensure you reference $li,
$li.dataset.id and onDetail when making the change.

}

get $element() {
return this.#$element;
}

renderMovies(movies: MovieData[]) {
this.removeSkeletons();
renderMovies(movies: MovieData[], page: number) {
this.removeSkeletons(page);
const $fragment = new DocumentFragment();
movies.forEach((movie) => $fragment.append(MovieItem(movie)));
this.#$list.append($fragment);
}

renderSkeletons(length: number = 20) {
this.#$skeletons = Array.from({ length }, () => MovieItemSkeleton());
this.#$skeletons.forEach(($skeleton) => this.#$list.append($skeleton));
}

removeSkeletons() {
this.#$skeletons.forEach(($skeleton) => $skeleton.remove());
this.#$skeletons = [];
}

renderMoreButton(onClick: () => void) {
this.#$moreButton = MoreButton(onClick);
$<HTMLElement>(this.#$element, 'section').append(this.#$moreButton);
renderSkeletons(page: number, length: number = 20) {
if (this.#$skeletons.has(String(page))) {
this.removeSkeletons(page);
}
const $newSkeletons = Array.from({ length }, () => MovieItemSkeleton());
$newSkeletons.forEach(($skeleton) => this.#$list.append($skeleton));
this.#$skeletons.set(String(page), $newSkeletons);
}

removeMoreButton() {
this.#$moreButton?.remove();
this.#$moreButton = null;
removeSkeletons(page: number) {
if (!this.#$skeletons.has(String(page))) {
return;
}
const $skeletonList = this.#$skeletons.get(String(page));
$skeletonList?.forEach(($skeleton) => $skeleton.remove());
}

renderError(messsage: string) {
Expand Down
14 changes: 7 additions & 7 deletions src/components/main/MoreButton.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const MoreButton = (onClick: () => void): HTMLElement => {
const $button = document.createElement('button');
$button.className = 'more-button';
$button.textContent = '더 보기';
$button.addEventListener('click', onClick);
// export const MoreButton = (onClick: () => void): HTMLElement => {
// const $button = document.createElement('button');
// $button.className = 'more-button';
// $button.textContent = '더 보기';
// $button.addEventListener('click', onClick);

return $button;
};
// return $button;
// };
3 changes: 2 additions & 1 deletion src/components/main/MovieItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { Star } from '../common/Star.ts';
import { MovieData } from '../../api/types.ts';

export const MovieItem = (data: MovieData) => {
const { title, poster_path, vote_average } = data;
const { id, title, poster_path, vote_average } = data;

const $li = document.createElement('li');
$li.dataset.id = String(id);

$li.innerHTML = `
<div class="item">
Expand Down
94 changes: 94 additions & 0 deletions src/components/modal/Modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { getOriginalImageUrl } from '../../api/renderImage';
import { MovieDetail } from '../../api/types';
import LocalStorage from '../../storage/LocalStorage';
import { $ } from '../../utils/dom';
import { Star } from '../common/Star';
import SubmitRate from './SubmitRate';

export default class Modal {
#$modal: HTMLElement;
#$body: HTMLElement;

constructor($body: HTMLElement) {
this.#$body = $body;

this.#$modal = document.createElement('div');
this.#$modal.id = 'modalBackground';
this.#$modal.className = 'modal-background';
this.#$modal.innerHTML = /*html */ `
<div class="modal">
<button class="close-modal" id="closeModal">
<img src="./images/modal_button_close.png" />
</button>
<div class="modal-container">
<div class="modal-image"><img /></div>
<div class="modal-description">
<h2></h2>
<p class="category"></p>
<div class="rate"></div>
<hr />
<div class="modal-submit-star">
<h3>내 별점</h3>
</div>
<hr />
<div>
<h3>줄거리</h3>
<p class="detail"></p>
</div>
</div>
</div>
</div>
`;

$(this.#$modal, 'button').addEventListener('click', () => this.close());
}

get $element() {
return this.#$modal;
}

#update(movie: MovieDetail) {
const { title, release_date, overview, poster_path, genres, vote_average } = movie;

$<HTMLImageElement>(this.#$modal, '.modal-image img').src = getOriginalImageUrl(poster_path);
$(this.#$modal, 'h2').textContent = title;
$(this.#$modal, '.detail').textContent = overview;
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

poster_path가 null일 경우를 고려해 보세요.

TMDB API에서 일부 영화는 poster_pathnull일 수 있습니다. 이 경우 getOriginalImageUrl(null)이 호출되어 유효하지 않은 이미지 URL이 생성됩니다.

기본 이미지(placeholder)를 표시하는 방법을 고려해 보시겠어요?

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

In `@src/components/modal/Modal.ts` around lines 50 - 55, In `#update`(movie:
MovieDetail) guard against movie.poster_path being null before calling
getOriginalImageUrl: if poster_path is falsy, set the img src to a
placeholder/default image (or a getPlaceholderImage() helper) instead of calling
getOriginalImageUrl(null); update the assignment that targets
$<HTMLImageElement>(this.#$modal, '.modal-image img').src and ensure any helper
used (getOriginalImageUrl or new getPlaceholderImage) is referenced so reviewers
can locate the change.


const releaseYear = new Date(release_date).getFullYear();
const category = genres.map((g) => g.name).join(' ');
$(this.#$modal, '.category').textContent = `${releaseYear} · ${category}`;

const $rateContainer = $(this.#$modal, '.rate');
$rateContainer.innerHTML = '';

const $starIcon = Star(true);
const $score = document.createElement('span');
$score.textContent = `평균 ${Number(vote_average).toFixed(1)}`;

$rateContainer.append($starIcon, $score);
}
Comment on lines +61 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

모달을 닫고 다른 영화를 열었을 때 새 이미지가 로드되기 전까지 이전 이미지가 보이는 이유가 요 메서드 동작 때문일 것 같군요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

지금 보니 여기도 기본 이미지나 스켈레톤을 달 수 있겠네요. 수정하겠습니다


open(movie: MovieDetail) {
this.#$body.className = 'modal-open';
this.#$modal.classList.add('active');
this.#update(movie);
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

body의 기존 클래스가 덮어씌워질 수 있습니다.

this.#$body.className = 'modal-open'은 body 요소의 기존 클래스를 모두 제거하고 'modal-open'만 남깁니다. close() 메서드에서는 classList.remove()를 사용하고 있어 일관성이 없습니다.

🔧 수정 제안
  open(movie: MovieDetail) {
-   this.#$body.className = 'modal-open';
+   this.#$body.classList.add('modal-open');
    this.#$modal.classList.add('active');
📝 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
open(movie: MovieDetail) {
this.#$body.className = 'modal-open';
this.#$modal.classList.add('active');
this.#update(movie);
open(movie: MovieDetail) {
this.#$body.classList.add('modal-open');
this.#$modal.classList.add('active');
this.#update(movie);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/modal/Modal.ts` around lines 71 - 74, The open(movie:
MovieDetail) method on Modal overwrites the body element's existing classes by
setting this.#$body.className = 'modal-open'; change this to add the class
instead (use classList.add('modal-open')) so existing classes are preserved and
behavior is consistent with close() which uses classList.remove; update the open
method where this.#$body is referenced (and leave
this.#$modal.classList.add('active') and this.#update(movie) unchanged).


const { id } = movie;
const movieRate = Number(LocalStorage.getRate(`${id}`)) || 0;

const $submitRate = new SubmitRate(movieRate, (rate) => {
LocalStorage.saveRate(`${id}`, String(rate));
}).$element;

const $container = $(this.#$modal, '.modal-submit-star');
const $oldCon = $container.querySelector('.submit-rate-container');
if ($oldCon) $oldCon.remove();
$container.append($submitRate);
}

close() {
console.log('object');
this.#$body.classList.remove('modal-open');
this.#$modal.classList.remove('active');
}
Comment on lines +107 to +110
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

디버그용 console.log 구문을 제거해 주세요.

console.log('object')는 디버깅 중 남은 것으로 보입니다.

🧹 정리 제안
  close() {
-   console.log('object');
    this.#$body.classList.remove('modal-open');
    this.#$modal.classList.remove('active');
  }
📝 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
close() {
console.log('object');
this.#$body.classList.remove('modal-open');
this.#$modal.classList.remove('active');
}
close() {
this.#$body.classList.remove('modal-open');
this.#$modal.classList.remove('active');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/modal/Modal.ts` around lines 89 - 93, Remove the leftover
debug console.log from the Modal.close method: delete the statement
console.log('object') inside the close() function so only the DOM class removals
(this.#$body.classList.remove('modal-open') and
this.#$modal.classList.remove('active')) remain; ensure no other debug logs are
left in the Modal class.

}
Loading