Create Week10 Mission1-2 - #86
Conversation
📝 WalkthroughWalkthroughTMDB API를 사용하는 영화 검색 React SPA를 Changes영화 검색 앱 전체 구현
Sequence Diagram(s)sequenceDiagram
actor User as 사용자
participant HomePage as HomePage
participant useMovieSearch as useMovieSearch
participant useFetch as useFetch
participant tmdbClient as TMDB API
User->>HomePage: 검색어 입력 후 제출
HomePage->>useMovieSearch: submitSearch()
useMovieSearch->>useMovieSearch: draft → submitted, page=1 리셋
useMovieSearch->>useFetch: fetcher(searchMovies / fetchMovies) 전달
useFetch->>tmdbClient: GET /search/movie 또는 /movie/popular
tmdbClient-->>useFetch: MovieListResponse
useFetch-->>useMovieSearch: data, isLoading, error
useMovieSearch-->>HomePage: movies, title, totalPages
HomePage->>HomePage: MovieGrid 렌더링
User->>HomePage: 영화 카드 클릭
HomePage->>HomePage: selectedMovie 상태 설정
HomePage->>MovieModal: movie, onClose 전달
MovieModal->>useMovieDetail: useMovieDetail(movie.id)
useMovieDetail->>tmdbClient: Promise.all([fetchMovieDetail, fetchMovieCredits])
tmdbClient-->>useMovieDetail: MovieDetail + Credits
useMovieDetail-->>MovieModal: movie, director, mainCast
MovieModal->>MovieModal: MovieHero + CastGrid + MovieMetaCards 렌더링
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (1)
Week10/gureum/mission1/src/components/movies/Pagination.tsx (1)
18-44: ⚡ Quick win아이콘 버튼에 접근성 라벨과
type을 추가해 주세요.현재
<,>만으로는 보조기기에서 의도가 불명확합니다.aria-label과type="button"을 명시하면 접근성과 안정성이 좋아집니다.제안 패치
<button + type="button" + aria-label="이전 페이지" onClick={onPrev} disabled={currentPage === 1} className={`w-12 h-12 rounded-full font-bold text-xl transition-colors ${ @@ <button + type="button" + aria-label="다음 페이지" onClick={onNext} disabled={currentPage >= totalPages} className={`w-12 h-12 rounded-full font-bold text-xl transition-colors ${🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week10/gureum/mission1/src/components/movies/Pagination.tsx` around lines 18 - 44, The pagination buttons in the Pagination component lack proper accessibility attributes. Add aria-label attributes to both the previous button (onClick={onPrev}) with text like "Previous page" and the next button (onClick={onNext}) with text like "Next page" to clarify their purpose for assistive devices. Additionally, add explicit type="button" attributes to both button elements to ensure proper HTML semantics and prevent unexpected form submission behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Week10/gureum/mission1/README.md`:
- Around line 17-19: The VITE_TMDB_API_KEY environment variable with the Bearer
token is being exposed to the client bundle because VITE_ prefixed variables are
automatically bundled into the browser. To fix this security issue, remove the
VITE_TMDB_API_KEY from the client environment variables, move the TMDB Bearer
token to server-side environment variables (not prefixed with VITE_), create a
server-side proxy endpoint or API route that uses the token to make requests to
TMDB, and update the client code to call your server endpoint instead of
directly calling the TMDB API with the token. This way the token remains
protected on the server and the client never has access to it.
- Around line 14-19: The README currently instructs developers to reference a
`.env.example` file for environment variable setup, but this file does not exist
in the repository. Either create a `.env.example` file in the project root that
contains the required environment variable templates (VITE_TMDB_BASE_URL and
VITE_TMDB_API_KEY with placeholder values), or update the README instruction in
lines 14-19 to remove the reference to the non-existent `.env.example` file and
directly provide the environment variable names and format that developers need
to configure.
In `@Week10/gureum/mission1/src/api/tmdb.ts`:
- Around line 11-25: The ensureToken function currently only validates the
TMDB_TOKEN but does not validate BASE_URL, which creates a security risk where
the Authorization header could be exposed to the current web app origin if
BASE_URL is not configured. Modify the ensureToken function to also check that
BASE_URL is properly set, throwing an error with an appropriate message if
BASE_URL is missing or empty, ensuring both required configuration values are
validated before axios client usage.
- Around line 14-19: The tmdbClient axios instance is missing an explicit
timeout configuration, which means network requests can hang indefinitely and
leave the isLoading state in loading status. Add a timeout property to the
axios.create configuration object in tmdbClient to set a reasonable timeout
value (typically in milliseconds, such as 10000 for 10 seconds). This ensures
that requests will fail gracefully after the specified timeout period rather
than hanging indefinitely.
In `@Week10/gureum/mission1/src/components/common/ErrorState.tsx`:
- Around line 15-28: The ErrorState component is enforcing full-screen height
with the min-h-screen class on its outer container div, which causes layout
overflow issues when used within modal dialogs. Remove the min-h-screen class
from the div that currently has className="min-h-screen bg-white text-gray-800
flex items-center justify-center" to allow the component to size naturally based
on its content. Keep the centering flex utilities and other styling, but let the
parent container control the overall layout and height constraints. This makes
the ErrorState component reusable across different contexts like modals and
full-page displays without forcing unwanted screen height.
In `@Week10/gureum/mission1/src/components/common/LoadingSpinner.tsx`:
- Around line 13-20: The LoadingSpinner component uses `min-h-screen` on the
outer div which forces the component to occupy the full viewport height, causing
layout issues when used inside the MovieModal. Remove the `min-h-screen` class
from the outer div's className attribute so the component adapts to its parent
container's size instead of forcing a minimum viewport height.
In `@Week10/gureum/mission1/src/components/movie-detail/CastGrid.tsx`:
- Around line 18-25: The img element in the CastGrid component lacks error
handling for when images fail to load. The current implementation only has a
static fallback URL that may not be reliable. Add an onError event handler to
the img element that sets a valid fallback image source when the TMDB profile
image fails to load, and replace the unreliable `/api/placeholder/150/225`
placeholder with a more robust fallback URL or state management approach to
handle image loading failures gracefully.
In `@Week10/gureum/mission1/src/components/movie-detail/MovieHero.tsx`:
- Around line 95-99: The anchor element with the imdbSearchUrl href uses
target="_blank" to open in a new tab, but the rel attribute only includes
"noreferrer". For security best practices when using target="_blank", add
"noopener" to the rel attribute alongside "noreferrer" to prevent the new page
from accessing the window.opener property. Update the rel attribute value in the
anchor tag to include both "noreferrer" and "noopener".
- Around line 30-32: The onError handler in the MovieHero component uses an API
route `/api/placeholder/300/450` as a fallback image source, which will fail in
a static SPA deployment since API routes are not available. Replace this
fallback path with a valid static asset URL that points to an actual image file
stored in your public or assets directory, such as a placeholder image file that
exists in your project's static resources.
In `@Week10/gureum/mission1/src/components/movies/MovieGrid.tsx`:
- Around line 21-28: The onError handler in the img element within MovieGrid is
setting display: none when the poster fails to load, causing the card to
collapse and the movie item to disappear from the grid. Instead of hiding the
image, modify the error handling to display a fallback placeholder or default
image that maintains the same dimensions as the poster image. This could involve
rendering a default image, placeholder content, or styled fallback element in
the same space where the poster would appear, ensuring the card height and
layout remain consistent even when the actual poster fails to load.
In `@Week10/gureum/mission1/src/components/movies/MovieModal.tsx`:
- Line 25: The aria-labelledby="movie-modal-title" attribute in the MovieModal
component references an id that does not exist within the modal's content,
breaking screen reader accessibility. Fix this by either adding an
id="movie-modal-title" attribute to an actual heading or title element inside
the modal (such as the modal title text), or replace the aria-labelledby
attribute with aria-label and provide a descriptive label string directly to the
dialog element.
In `@Week10/gureum/mission1/src/hooks/useFetch.ts`:
- Around line 26-60: The useFetch hook has two race condition issues: first, the
runFetch function lacks request ordering, so when multiple fetch calls happen
rapidly, older responses can overwrite newer data; second, the
Promise.resolve().then(runFetch) pattern in the useEffect can cause setState
calls on unmounted components if the component unmounts before the microtask
executes. To fix this, introduce a requestIdRef that increments with each fetch
call, then in the try and catch blocks of runFetch, only update state if the
current requestId matches the latest one (ensuring stale responses are ignored).
Additionally, call runFetch directly in the useEffect instead of wrapping it in
Promise.resolve().then() to eliminate the microtask timing issue and ensure the
cleanup function properly prevents state updates.
In `@Week10/gureum/mission1/src/types/movie.ts`:
- Around line 16-23: The Movie interface in the file has poster_path typed as
string, but the TMDB API can return null for image and link-related fields.
Update the Movie interface to reflect the actual API response schema by changing
poster_path to string | null, and similarly update any other optional image or
link fields (like backdrop_path, homepage, tagline if present) to include null
in their type union. This will align the type definition with the null checks
already present in useMovieSearch.ts and MovieHero.tsx, and match the pattern
already used in CastMember's profile_path field.
In `@Week10/gureum/mission1/tsconfig.node.json`:
- Around line 2-25: The compilerOptions in tsconfig.node.json is missing the
"composite": true setting, which is required for TypeScript project references.
Add "composite": true to the compilerOptions object in tsconfig.node.json. Apply
the same fix to tsconfig.app.json by adding "composite": true to its
compilerOptions as well, ensuring both referenced configuration files support
project reference builds.
---
Nitpick comments:
In `@Week10/gureum/mission1/src/components/movies/Pagination.tsx`:
- Around line 18-44: The pagination buttons in the Pagination component lack
proper accessibility attributes. Add aria-label attributes to both the previous
button (onClick={onPrev}) with text like "Previous page" and the next button
(onClick={onNext}) with text like "Next page" to clarify their purpose for
assistive devices. Additionally, add explicit type="button" attributes to both
button elements to ensure proper HTML semantics and prevent unexpected form
submission behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f04f7dfe-67e1-4b56-be34-751feffdcf20
⛔ Files ignored due to path filters (3)
Week10/gureum/mission1/package-lock.jsonis excluded by!**/package-lock.jsonWeek10/gureum/mission1/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlWeek10/gureum/mission1/public/favicon.svgis excluded by!**/*.svg
📒 Files selected for processing (37)
Week10/gureum/mission1/.gitignoreWeek10/gureum/mission1/README.mdWeek10/gureum/mission1/eslint.config.jsWeek10/gureum/mission1/index.htmlWeek10/gureum/mission1/package.jsonWeek10/gureum/mission1/src/App.cssWeek10/gureum/mission1/src/App.tsxWeek10/gureum/mission1/src/api/tmdb.tsWeek10/gureum/mission1/src/components/Layout.tsxWeek10/gureum/mission1/src/components/MoviesPage.tsxWeek10/gureum/mission1/src/components/Navbar.tsxWeek10/gureum/mission1/src/components/common/ErrorState.tsxWeek10/gureum/mission1/src/components/common/LoadingSpinner.tsxWeek10/gureum/mission1/src/components/movie-detail/CastGrid.tsxWeek10/gureum/mission1/src/components/movie-detail/MovieHero.tsxWeek10/gureum/mission1/src/components/movie-detail/MovieMetaCards.tsxWeek10/gureum/mission1/src/components/movies/MovieGrid.tsxWeek10/gureum/mission1/src/components/movies/MovieModal.tsxWeek10/gureum/mission1/src/components/movies/MovieSearchForm.tsxWeek10/gureum/mission1/src/components/movies/Pagination.tsxWeek10/gureum/mission1/src/hooks/useFetch.tsWeek10/gureum/mission1/src/hooks/useMovieDetail.tsWeek10/gureum/mission1/src/hooks/useMovieSearch.tsWeek10/gureum/mission1/src/hooks/useMovies.tsWeek10/gureum/mission1/src/index.cssWeek10/gureum/mission1/src/main.tsxWeek10/gureum/mission1/src/pages/HomePage.tsxWeek10/gureum/mission1/src/pages/MovieDetailPage.tsxWeek10/gureum/mission1/src/pages/PopularPage.tsxWeek10/gureum/mission1/src/pages/TopRatedPage.tsxWeek10/gureum/mission1/src/pages/UpcomingPage.tsxWeek10/gureum/mission1/src/types/movie.tsWeek10/gureum/mission1/tsconfig.app.jsonWeek10/gureum/mission1/tsconfig.jsonWeek10/gureum/mission1/tsconfig.node.jsonWeek10/gureum/mission1/vercel.jsonWeek10/gureum/mission1/vite.config.ts
| `.env` 파일은 Git에 올리지 않습니다. `.env.example`을 참고해 아래 값을 설정해주세요. | ||
|
|
||
| ```bash | ||
| VITE_TMDB_BASE_URL=https://api.themoviedb.org/3 | ||
| VITE_TMDB_API_KEY=your_tmdb_bearer_token | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== .env.example 존재 확인 =="
fd -HI '^\.env\.example$'
echo
echo "== README 환경변수 키 확인 =="
rg -n 'VITE_TMDB_BASE_URL|VITE_TMDB_API_KEY' Week10/gureum/mission1/README.md -C2
echo
echo "== .env.example 내 키 확인 (파일이 있을 때만) =="
if fd -HI '^\.env\.example$' | grep -q .; then
fd -HI '^\.env\.example$' -x sh -c 'echo "--- $1 ---"; rg -n "VITE_TMDB_BASE_URL|VITE_TMDB_API_KEY" "$1" -C2 || true' sh {}
fiRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 284
.env.example 파일이 존재하지 않습니다. 파일을 생성하거나 README 지시문을 수정해야 합니다.
README 14번 라인에서는 개발자들이 .env.example을 참고하도록 안내하고 있으나, 해당 파일이 저장소에 존재하지 않습니다. .env.example 파일을 생성하여 필요한 환경 변수를 포함시키거나, README의 참고 지시문을 수정하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/README.md` around lines 14 - 19, The README currently
instructs developers to reference a `.env.example` file for environment variable
setup, but this file does not exist in the repository. Either create a
`.env.example` file in the project root that contains the required environment
variable templates (VITE_TMDB_BASE_URL and VITE_TMDB_API_KEY with placeholder
values), or update the README instruction in lines 14-19 to remove the reference
to the non-existent `.env.example` file and directly provide the environment
variable names and format that developers need to configure.
| VITE_TMDB_BASE_URL=https://api.themoviedb.org/3 | ||
| VITE_TMDB_API_KEY=your_tmdb_bearer_token | ||
| ``` |
There was a problem hiding this comment.
클라이언트 번들에 Bearer 토큰을 노출하고 있습니다.
Line 17-19처럼 VITE_ 접두사 환경변수에 TMDB Bearer 토큰을 넣으면 브라우저에서 그대로 노출됩니다. 현재 구조는 키 남용/쿼터 소진 리스크가 있어, 토큰은 서버(프록시/API route)로 이동하고 프론트는 서버 엔드포인트만 호출하도록 바꾸는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/README.md` around lines 17 - 19, The VITE_TMDB_API_KEY
environment variable with the Bearer token is being exposed to the client bundle
because VITE_ prefixed variables are automatically bundled into the browser. To
fix this security issue, remove the VITE_TMDB_API_KEY from the client
environment variables, move the TMDB Bearer token to server-side environment
variables (not prefixed with VITE_), create a server-side proxy endpoint or API
route that uses the token to make requests to TMDB, and update the client code
to call your server endpoint instead of directly calling the TMDB API with the
token. This way the token remains protected on the server and the client never
has access to it.
| const BASE_URL = import.meta.env.VITE_TMDB_BASE_URL; | ||
| const TMDB_TOKEN = import.meta.env.VITE_TMDB_API_KEY; | ||
|
|
||
| const tmdbClient = axios.create({ | ||
| baseURL: BASE_URL, | ||
| headers: { | ||
| Authorization: `Bearer ${TMDB_TOKEN}`, | ||
| }, | ||
| }); | ||
|
|
||
| const ensureToken = () => { | ||
| if (!TMDB_TOKEN) { | ||
| throw new Error('TMDB API 토큰이 설정되지 않았습니다.'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n Week10/gureum/mission1/src/api/tmdb.tsRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 2256
🏁 Script executed:
# Check for env file configuration
find Week10/gureum/mission1 -type f \( -name ".env*" -o -name "vite.config.*" \) | head -20
# Search for other uses of BASE_URL or VITE_TMDB_BASE_URL
rg "VITE_TMDB_BASE_URL|VITE_TMDB_API_KEY" Week10/gureum/mission1 -nRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 463
🏁 Script executed:
# Check vite.config.ts to see if there's any env validation
cat -n Week10/gureum/mission1/vite.config.ts
# Search for any other validation of these env vars
rg "BASE_URL|TMDB_TOKEN" Week10/gureum/mission1 --type ts -B 2 -A 2Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 1364
BASE_URL 미검증으로 인한 인증 토큰 노출 위험
ensureToken()이 TMDB_TOKEN만 검증하고 BASE_URL을 검증하지 않으면, VITE_TMDB_BASE_URL이 비어 있을 경우 axios가 상대 경로로 요청을 전송합니다. 이때 Line 17의 Authorization 헤더가 그대로 포함되어 현재 웹앱 오리진으로 Bearer 토큰이 전송됩니다. 토큰 누락과 동일하게 BASE_URL 누락도 fail-fast 처리가 필요합니다.
제안 수정
-const BASE_URL = import.meta.env.VITE_TMDB_BASE_URL;
-const TMDB_TOKEN = import.meta.env.VITE_TMDB_API_KEY;
+const BASE_URL = import.meta.env.VITE_TMDB_BASE_URL?.trim();
+const TMDB_TOKEN = import.meta.env.VITE_TMDB_API_KEY?.trim();
const tmdbClient = axios.create({
baseURL: BASE_URL,
headers: {
- Authorization: `Bearer ${TMDB_TOKEN}`,
+ ...(TMDB_TOKEN ? { Authorization: `Bearer ${TMDB_TOKEN}` } : {}),
},
});
-const ensureToken = () => {
+const ensureConfig = () => {
+ if (!BASE_URL) {
+ throw new Error('TMDB BASE_URL이 설정되지 않았습니다.');
+ }
if (!TMDB_TOKEN) {
throw new Error('TMDB API 토큰이 설정되지 않았습니다.');
}
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/api/tmdb.ts` around lines 11 - 25, The ensureToken
function currently only validates the TMDB_TOKEN but does not validate BASE_URL,
which creates a security risk where the Authorization header could be exposed to
the current web app origin if BASE_URL is not configured. Modify the ensureToken
function to also check that BASE_URL is properly set, throwing an error with an
appropriate message if BASE_URL is missing or empty, ensuring both required
configuration values are validated before axios client usage.
| const tmdbClient = axios.create({ | ||
| baseURL: BASE_URL, | ||
| headers: { | ||
| Authorization: `Bearer ${TMDB_TOKEN}`, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate and examine the tmdb.ts file
find . -path "*/Week10/gureum/mission1/src/api/tmdb.ts" -type f 2>/dev/nullRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 114
🏁 Script executed:
#!/bin/bash
# Also try broader search in case the path varies
fd -type f -name "tmdb.ts" 2>/dev/null | head -20Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Search for axios.create usage to understand the pattern
rg "axios\.create" --type ts --type tsx -B 2 -A 5 2>/dev/nullRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Read the tmdb.ts file to see the actual implementation
cat -n ./Week10/gureum/mission1/src/api/tmdb.tsRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 2256
🏁 Script executed:
#!/bin/bash
# Check if timeout is already configured anywhere in the file
grep -n "timeout" ./Week10/gureum/mission1/src/api/tmdb.ts 2>/dev/null || echo "No timeout found"Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 90
🏁 Script executed:
#!/bin/bash
# Search for isLoading usage in the codebase to understand how it's used with tmdbClient
rg "isLoading" --type ts --type tsx -B 2 -A 2 2>/dev/null | head -50Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Search more broadly for loading/isLoading patterns in the component files
find ./Week10/gureum/mission1 -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.js" \) | xargs grep -l "loading\|Loading" 2>/dev/null | head -10Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 560
🏁 Script executed:
#!/bin/bash
# Check for async/await usage and error handling patterns
grep -r "await.*tmdbClient\|fetchMovies\|searchMovies\|fetchMovieDetail\|fetchMovieCredits" ./Week10/gureum/mission1/src --include="*.ts" --include="*.tsx" -B 2 -A 3 2>/dev/null | head -80Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 5231
🏁 Script executed:
#!/bin/bash
# Look at component structure to understand loading state management
find ./Week10/gureum/mission1/src -type f -name "*.tsx" -o -name "*.jsx" | head -10 | xargs -I {} sh -c 'echo "=== {} ===" && head -50 {}'Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 12916
🏁 Script executed:
#!/bin/bash
# Check the useFetch hook to understand loading/error handling
cat -n ./Week10/gureum/mission1/src/hooks/useFetch.tsRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 2294
🌐 Web query:
axios default timeout behavior no timeout
💡 Result:
The default timeout value for Axios is 0, which means there is no timeout applied by default [1][2][3]. Consequently, without an explicit timeout configuration, a request can hang indefinitely if the server does not respond [2]. It is highly recommended to set a specific timeout value in production environments to prevent requests from hanging in the event of network issues or server stalls [2]. You can configure this globally, for a specific instance, or on a per-request basis [1][4]. When a timeout is exceeded, Axios will abort the request and typically throw an error with the code ECONNABORTED [2]. You can further refine error handling by configuring the transitional.clarifyTimeoutError setting to true, which can help in distinguishing timeout errors [2].
Citations:
- 1: https://github.com/axios/axios/blob/v1.15.0/docs/pages/advanced/config-defaults.md
- 2: https://github.com/axios/axios/
- 3: https://deepwiki.com/axios/axios/2.3-configuration-options
- 4: https://www.mintlify.com/axios/axios/core/config-defaults
axios 인스턴스에 명시적 timeout 설정이 필요합니다.
axios 기본 timeout 값이 없어(무제한) 네트워크 hang 시 isLoading 상태가 무기한 지속됩니다. 명시적 timeout을 설정하면 이 문제를 방지할 수 있습니다.
🔧 제안 수정
const tmdbClient = axios.create({
baseURL: BASE_URL,
+ timeout: 10000,
headers: {
Authorization: `Bearer ${TMDB_TOKEN}`,
},
});📝 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.
| const tmdbClient = axios.create({ | |
| baseURL: BASE_URL, | |
| headers: { | |
| Authorization: `Bearer ${TMDB_TOKEN}`, | |
| }, | |
| }); | |
| const tmdbClient = axios.create({ | |
| baseURL: BASE_URL, | |
| timeout: 10000, | |
| headers: { | |
| Authorization: `Bearer ${TMDB_TOKEN}`, | |
| }, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/api/tmdb.ts` around lines 14 - 19, The tmdbClient
axios instance is missing an explicit timeout configuration, which means network
requests can hang indefinitely and leave the isLoading state in loading status.
Add a timeout property to the axios.create configuration object in tmdbClient to
set a reasonable timeout value (typically in milliseconds, such as 10000 for 10
seconds). This ensures that requests will fail gracefully after the specified
timeout period rather than hanging indefinitely.
| <div className="min-h-screen bg-white text-gray-800 flex items-center justify-center"> | ||
| <div className="text-center"> | ||
| <div className="text-6xl mb-4">{icon}</div> | ||
| <p className="text-xl text-red-500 mb-4">{message}</p> | ||
| {actionLabel && onAction && ( | ||
| <button | ||
| onClick={onAction} | ||
| className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors" | ||
| > | ||
| {actionLabel} | ||
| </button> | ||
| )} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
에러 컴포넌트도 min-h-screen 고정으로 모달 UI를 과확장합니다.
ErrorState가 화면 높이를 강제해서 모달 내부 에러 화면이 불필요하게 커지고 스크롤이 생깁니다. 공통 컴포넌트는 full-screen 책임을 가지지 않게 분리해 주세요.
제안 패치
- <div className="min-h-screen bg-white text-gray-800 flex items-center justify-center">
+ <div className="flex items-center justify-center text-gray-800 py-10">
<div className="text-center">
<div className="text-6xl mb-4">{icon}</div>
<p className="text-xl text-red-500 mb-4">{message}</p>📝 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.
| <div className="min-h-screen bg-white text-gray-800 flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="text-6xl mb-4">{icon}</div> | |
| <p className="text-xl text-red-500 mb-4">{message}</p> | |
| {actionLabel && onAction && ( | |
| <button | |
| onClick={onAction} | |
| className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors" | |
| > | |
| {actionLabel} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-center text-gray-800 py-10"> | |
| <div className="text-center"> | |
| <div className="text-6xl mb-4">{icon}</div> | |
| <p className="text-xl text-red-500 mb-4">{message}</p> | |
| {actionLabel && onAction && ( | |
| <button | |
| onClick={onAction} | |
| className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors" | |
| > | |
| {actionLabel} | |
| </button> | |
| )} | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/components/common/ErrorState.tsx` around lines 15
- 28, The ErrorState component is enforcing full-screen height with the
min-h-screen class on its outer container div, which causes layout overflow
issues when used within modal dialogs. Remove the min-h-screen class from the
div that currently has className="min-h-screen bg-white text-gray-800 flex
items-center justify-center" to allow the component to size naturally based on
its content. Keep the centering flex utilities and other styling, but let the
parent container control the overall layout and height constraints. This makes
the ErrorState component reusable across different contexts like modals and
full-page displays without forcing unwanted screen height.
| <img | ||
| src={getPosterUrl(movie.poster_path)} | ||
| alt={movie.title} | ||
| className="w-full h-auto object-cover transition-all duration-300 group-hover:blur-sm group-hover:brightness-50" | ||
| onError={(event) => { | ||
| event.currentTarget.style.display = 'none'; | ||
| }} | ||
| /> |
There was a problem hiding this comment.
포스터 로드 실패 시 카드를 숨겨서 영화 항목이 사라집니다.
onError에서 이미지를 display: none 처리하면 카드 높이가 무너져 해당 영화가 사실상 보이지 않게 됩니다. 실패 시에도 같은 영역에 fallback을 렌더링해야 목록 일관성이 유지됩니다.
제안 패치
- <img
- src={getPosterUrl(movie.poster_path)}
- alt={movie.title}
- className="w-full h-auto object-cover transition-all duration-300 group-hover:blur-sm group-hover:brightness-50"
- onError={(event) => {
- event.currentTarget.style.display = 'none';
- }}
- />
+ <div className="aspect-[2/3] w-full bg-gray-200">
+ <img
+ src={getPosterUrl(movie.poster_path)}
+ alt={movie.title}
+ className="h-full w-full object-cover transition-all duration-300 group-hover:blur-sm group-hover:brightness-50"
+ onError={(event) => {
+ event.currentTarget.src =
+ 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="450"><rect width="100%" height="100%" fill="%23e5e7eb"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%236b7280" font-size="18">No Image</text></svg>';
+ }}
+ />
+ </div>📝 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.
| <img | |
| src={getPosterUrl(movie.poster_path)} | |
| alt={movie.title} | |
| className="w-full h-auto object-cover transition-all duration-300 group-hover:blur-sm group-hover:brightness-50" | |
| onError={(event) => { | |
| event.currentTarget.style.display = 'none'; | |
| }} | |
| /> | |
| <div className="aspect-[2/3] w-full bg-gray-200"> | |
| <img | |
| src={getPosterUrl(movie.poster_path)} | |
| alt={movie.title} | |
| className="h-full w-full object-cover transition-all duration-300 group-hover:blur-sm group-hover:brightness-50" | |
| onError={(event) => { | |
| event.currentTarget.src = | |
| 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="450"><rect width="100%" height="100%" fill="%23e5e7eb"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%236b7280" font-size="18">No Image</text></svg>'; | |
| }} | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/components/movies/MovieGrid.tsx` around lines 21 -
28, The onError handler in the img element within MovieGrid is setting display:
none when the poster fails to load, causing the card to collapse and the movie
item to disappear from the grid. Instead of hiding the image, modify the error
handling to display a fallback placeholder or default image that maintains the
same dimensions as the poster image. This could involve rendering a default
image, placeholder content, or styled fallback element in the same space where
the poster would appear, ensuring the card height and layout remain consistent
even when the actual poster fails to load.
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-8" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-labelledby="movie-modal-title" |
There was a problem hiding this comment.
aria-labelledby 참조 대상이 없어 대화상자 제목이 노출되지 않습니다.
Line 25에서 aria-labelledby="movie-modal-title"를 선언했지만, 모달 내부에 해당 id가 없습니다. 스크린리더 접근성에 직접 영향을 줍니다. aria-label을 직접 부여하거나 실제 제목 요소와 연결해 주세요.
수정 예시
- aria-labelledby="movie-modal-title"
+ aria-label={`${movie.title} 상세 정보`}📝 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.
| aria-labelledby="movie-modal-title" | |
| aria-label={`${movie.title} 상세 정보`} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/components/movies/MovieModal.tsx` at line 25, The
aria-labelledby="movie-modal-title" attribute in the MovieModal component
references an id that does not exist within the modal's content, breaking screen
reader accessibility. Fix this by either adding an id="movie-modal-title"
attribute to an actual heading or title element inside the modal (such as the
modal title text), or replace the aria-labelledby attribute with aria-label and
provide a descriptive label string directly to the dialog element.
| const runFetch = useCallback(async () => { | ||
| setIsLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const nextData = await fetcher(); | ||
|
|
||
| if (isMountedRef.current) { | ||
| setData(nextData); | ||
| } | ||
| } catch (requestError) { | ||
| console.error('데이터 요청 실패:', requestError); | ||
|
|
||
| if (isMountedRef.current) { | ||
| setError(options?.errorMessage ?? '데이터를 불러오지 못했습니다.'); | ||
| } | ||
| } finally { | ||
| if (isMountedRef.current) { | ||
| setIsLoading(false); | ||
| } | ||
| } | ||
| }, [fetcher, options?.errorMessage]); | ||
|
|
||
| useEffect(() => { | ||
| isMountedRef.current = true; | ||
|
|
||
| // fetcher 참조가 바뀌면 이 effect가 다시 돌고, 자동 재요청됩니다. | ||
| // React Hooks lint가 effect 내부의 동기 setState를 막기 때문에 요청 시작을 한 틱 늦춥니다. | ||
| void Promise.resolve().then(runFetch); | ||
|
|
||
| return () => { | ||
| // 이 시점부터는 "화면에서 내려간 상태"이므로 setState를 막습니다. | ||
| isMountedRef.current = false; | ||
| }; | ||
| }, [runFetch]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "useFetch.ts" -type fRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 167
🏁 Script executed:
git ls-files | grep -i usefetchRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 163
🏁 Script executed:
cat -n Week10/gureum/mission1/src/hooks/useFetch.tsRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 2294
요청 경합과 언마운트 타이밍 문제로 인한 상태 오염 위험이 실제로 존재합니다.
두 가지 문제가 있습니다:
-
요청 순서 경합: 빠르게 연속 호출되면 느린 이전 요청의 응답이 나중에 도착하여 최신 데이터를 덮어쓸 수 있습니다.
runFetch()를 여러 번 호출했을 때 응답 순서를 추적하지 않으면 이 문제가 발생합니다. -
언마운트 타이밍 경합: 라인 54의
Promise.resolve().then(runFetch)로 인해 마이크로태스크 큐에 쌓인 상태에서 컴포넌트가 빠르게 언마운트되면, 정리 함수가isMountedRef.current = false로 설정한 후에도 마이크로태스크가 실행될 수 있습니다. 이 경우 라인 27~28의 동기setState호출이 언마운트된 컴포넌트에서 실행되어 경고를 유발합니다.
제안된 수정(requestIdRef 추가 + 직접 실행)이 두 문제를 모두 해결합니다:
🔧 제안 수정
import { useCallback, useEffect, useRef, useState } from 'react';
@@
const isMountedRef = useRef(true);
+ const requestIdRef = useRef(0);
@@
const runFetch = useCallback(async () => {
+ if (!isMountedRef.current) return;
+ const requestId = ++requestIdRef.current;
+
setIsLoading(true);
setError(null);
try {
const nextData = await fetcher();
- if (isMountedRef.current) {
+ if (isMountedRef.current && requestId === requestIdRef.current) {
setData(nextData);
}
} catch (requestError) {
console.error('데이터 요청 실패:', requestError);
- if (isMountedRef.current) {
+ if (isMountedRef.current && requestId === requestIdRef.current) {
setError(options?.errorMessage ?? '데이터를 불러오지 못했습니다.');
}
} finally {
- if (isMountedRef.current) {
+ if (isMountedRef.current && requestId === requestIdRef.current) {
setIsLoading(false);
}
}
}, [fetcher, options?.errorMessage]);
@@
- void Promise.resolve().then(runFetch);
+ void runFetch();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/hooks/useFetch.ts` around lines 26 - 60, The
useFetch hook has two race condition issues: first, the runFetch function lacks
request ordering, so when multiple fetch calls happen rapidly, older responses
can overwrite newer data; second, the Promise.resolve().then(runFetch) pattern
in the useEffect can cause setState calls on unmounted components if the
component unmounts before the microtask executes. To fix this, introduce a
requestIdRef that increments with each fetch call, then in the try and catch
blocks of runFetch, only update state if the current requestId matches the
latest one (ensuring stale responses are ignored). Additionally, call runFetch
directly in the useEffect instead of wrapping it in Promise.resolve().then() to
eliminate the microtask timing issue and ensure the cleanup function properly
prevents state updates.
| export interface Movie { | ||
| id: number; | ||
| title: string; | ||
| overview: string; | ||
| poster_path: string; | ||
| release_date: string; | ||
| vote_average: number; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd Week10/gureum/mission1 && find . -type f -name "*.ts" | grep -E "(movie|types)" | head -20Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 94
🏁 Script executed:
cat -n Week10/gureum/mission1/src/types/movie.tsRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 1815
🏁 Script executed:
cat -n Week10/gureum/mission1/src/hooks/useMovieSearch.ts | head -60Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 2345
🏁 Script executed:
grep -r "poster_path\|backdrop_path\|tagline\|homepage" Week10/gureum/mission1/src --include="*.tsx" --include="*.ts" -n | head -30Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 1569
🌐 Web query:
TMDB API Movie response schema nullable fields poster_path backdrop_path
💡 Result:
In the TMDB API, the fields poster_path and backdrop_path are nullable [1]. When a movie does not have an associated image, these fields will be returned as null rather than being omitted from the response [1]. In technical implementations, you should handle these fields as potentially null strings [1]. For example, in many client libraries and TypeScript definitions, they are commonly typed as string | null or optional strings (string?) to account for this behavior [1]. If you are parsing the JSON response, ensure your code checks for null or undefined values before attempting to construct the full image URL, as the API returns only the relative path (e.g., /path/to/image.jpg) [2], which requires appending to a base image URL provided by the TMDB configuration endpoint.
Citations:
- 1: https://github.com/seerr-team/seerr/blob/92c486d3/server/api/themoviedb/interfaces.ts
- 2: https://developer.themoviedb.org/docs/search-and-query-for-details
🌐 Web query:
TMDB API documentation movie detail response null values fields
💡 Result:
The TMDB API response handling for null values, missing fields, and empty strings follows a flexible pattern rather than a strictly enforced schema. You should design your API integration to be resilient to these variations [1][2][3]. Key points regarding null values and missing fields: 1. Absence of Fields: The API may omit fields entirely from the JSON response if there is no data to return [2]. Relying on the existence of a key without checking for it can lead to runtime errors in your application [2]. Always verify that a field exists before attempting to access it. 2. Null Values: When data is present but explicitly unknown or not set, the API often returns a null value [3]. However, consistency can vary, and some fields may occasionally return empty strings or omit the key entirely depending on the specific endpoint and record [4][2][3]. 3. Append to Response: When using the append_to_response parameter to request additional data (e.g., images, videos), fields that are not explicitly requested will be absent or return as null in your local object model [5][6][7]. Ensure your code handles these cases by checking for null/missing data before processing appended objects [5][7]. 4. Documentation vs. Reality: While official documentation outlines standard response structures, it may not explicitly mark every nullable field [1]. Community discussions indicate that developers should not strictly assume a field is always present or non-null based solely on the documentation [1][2]. Best Practice: Implement robust defensive programming techniques. Check for the existence of keys and validate that values are not null or empty before performing operations on them. Do not treat the absence of a field in documentation as a guarantee that the field will always be present in the response [1][2].
Citations:
- 1: https://www.themoviedb.org/talk/64a568258c44b900ebe904ef
- 2: https://www.themoviedb.org/talk/642323e90792e1009c093261
- 3: https://www.themoviedb.org/talk/5c5028edc3a368479394f922?language=en-BZ
- 4: https://www.themoviedb.org/talk/66554d41aa4e15de7101d0e4
- 5: https://github.com/holgerbrandl/themoviedbapi/
- 6: https://devliz.mintlify.app/quick-reference/miscellenous
- 7: https://context7.com/c-eg/themoviedbapi/llms.txt
TMDB nullable 필드 타입을 실제 응답 스키마에 맞춰야 합니다.
Line 20의 poster_path를 string으로 고정하면 실제 null 응답을 타입이 숨기게 됩니다. 이미 useMovieSearch.ts Line 48에서 .filter((movie) => movie.poster_path)로 null 가능성을 전제로 필터링하고 있고, MovieHero.tsx에서도 movie.tagline &&, movie.homepage &&, movie.backdrop_path ?로 null 체크를 하고 있어 계약이 어긋나 있습니다. TMDB API는 이미지나 텍스트 필드가 없을 때 이들 필드를 null로 반환합니다. CastMember의 profile_path가 이미 string | null로 정의된 것처럼, MovieDetail의 이미지/링크 계열 필드도 null 허용으로 일관되게 맞춰야 다운스트림 렌더링 오류를 줄이고 타입 안전성을 개선할 수 있습니다.
🔧 제안 수정
export interface Movie {
id: number;
title: string;
overview: string;
- poster_path: string;
+ poster_path: string | null;
release_date: string;
vote_average: number;
}
export interface MovieDetail {
id: number;
title: string;
overview: string;
- poster_path: string;
- backdrop_path: string;
+ poster_path: string | null;
+ backdrop_path: string | null;
release_date: string;
vote_average: number;
vote_count: number;
runtime: number;
genres: Genre[];
budget: number;
revenue: number;
- tagline: string;
- homepage: string;
+ tagline: string | null;
+ homepage: string | null;
status: string;
original_language: string;
original_title: string;
popularity: number;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/src/types/movie.ts` around lines 16 - 23, The Movie
interface in the file has poster_path typed as string, but the TMDB API can
return null for image and link-related fields. Update the Movie interface to
reflect the actual API response schema by changing poster_path to string | null,
and similarly update any other optional image or link fields (like
backdrop_path, homepage, tagline if present) to include null in their type
union. This will align the type definition with the null checks already present
in useMovieSearch.ts and MovieHero.tsx, and match the pattern already used in
CastMember's profile_path field.
| "compilerOptions": { | ||
| "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||
| "target": "ES2023", | ||
| "lib": ["ES2023"], | ||
| "module": "ESNext", | ||
| "types": ["node"], | ||
| "skipLibCheck": true, | ||
|
|
||
| /* Bundler mode */ | ||
| "moduleResolution": "bundler", | ||
| "allowImportingTsExtensions": true, | ||
| "verbatimModuleSyntax": true, | ||
| "moduleDetection": "force", | ||
| "noEmit": true, | ||
|
|
||
| /* Linting */ | ||
| "strict": true, | ||
| "noUnusedLocals": true, | ||
| "noUnusedParameters": true, | ||
| "erasableSyntaxOnly": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "noUncheckedSideEffectImports": true | ||
| }, | ||
| "include": ["vite.config.ts"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import json, pathlib
p = pathlib.Path("Week10/gureum/mission1/package.json")
data = json.loads(p.read_text())
ts = data.get("devDependencies", {}).get("typescript") or data.get("dependencies", {}).get("typescript") or "missing"
print(f"typescript: {ts}")
PY
rg -n '"references"|"composite"|"erasableSyntaxOnly"' Week10/gureum/mission1/tsconfig*.jsonRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 302
🏁 Script executed:
cat -n Week10/gureum/mission1/tsconfig.jsonRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 241
🏁 Script executed:
cat -n Week10/gureum/mission1/tsconfig.node.jsonRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 908
🏁 Script executed:
cat -n Week10/gureum/mission1/tsconfig.app.jsonRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 1001
tsconfig.node.json과 tsconfig.app.json에 "composite": true를 추가하세요.
tsconfig.json에서 프로젝트 참조(references)를 사용할 때, 참조되는 설정 파일들은 반드시 compilerOptions에 "composite": true를 포함해야 합니다. 현재 두 파일 모두 이 옵션이 누락되어 있어 tsc -b 실행 시 빌드 오류가 발생할 수 있습니다.
"compilerOptions": {
"composite": true,
...
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week10/gureum/mission1/tsconfig.node.json` around lines 2 - 25, The
compilerOptions in tsconfig.node.json is missing the "composite": true setting,
which is required for TypeScript project references. Add "composite": true to
the compilerOptions object in tsconfig.node.json. Apply the same fix to
tsconfig.app.json by adding "composite": true to its compilerOptions as well,
ensuring both referenced configuration files support project reference builds.
📝 미션 번호
10주차 Misson 1-2
📋 구현 사항
📎 스크린샷
✅ 체크리스트
🤔 질문 사항
Summary by CodeRabbit
릴리스 노트
New Features
Documentation
Chores