Skip to content

Create Week04 Mission1,2,3#37

Open
jaewoni314 wants to merge 1 commit intomainfrom
jaewoni314/week04
Open

Create Week04 Mission1,2,3#37
jaewoni314 wants to merge 1 commit intomainfrom
jaewoni314/week04

Conversation

@jaewoni314
Copy link
Copy Markdown
Contributor

@jaewoni314 jaewoni314 commented Apr 7, 2026

📝 미션 번호

  • 4주차 Misson 1
  • 4주차 Misson 2
  • 4주차 Misson 3

📋 구현 사항

-Mission 1
커스텀 훅울 활용하여 영화 리스트 페이지 및 상세 페이지 구현

-Mission 2,3
로그인 및 회원가입 기능 구현
비밀번호 가시성 토글 구현
zod 스키마를 활용하여 이메일, 비밀번호 유효성 검증
유효성 검사 통과 시에만 버튼 활성화 구현
마이페이지 구현

📎 스크린샷

4주차 Misson 1
https://github.com/user-attachments/assets/0f51e1fe-05c6-4716-a0bf-638f4d482845

4주차 Misson 2
https://github.com/user-attachments/assets/28756ac2-937a-4d80-8373-a9b4fa299df9

4주차 Misson 3
https://github.com/user-attachments/assets/9157a4a3-6e10-4823-943b-91435dbe6a16

✅ 체크리스트

  • Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  • 로컬에서 실행했을 때 에러가 발생하지 않나요?
  • 불필요한 주석이 제거되었나요?
  • 코드 스타일이 일관적인가요?

🤔 질문 사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 영화 정보 조회 및 카테고리별 탐색 기능 추가
    • 영화 상세 정보 및 출연진 조회 기능
    • 로그인 및 회원가입 인증 기능 추가
    • 페이지네이션 지원
  • 문서

    • 프로젝트 설정 및 개발 가이드 추가
  • 기타

    • React 19, TypeScript, Vite 개발 환경 구성
    • Tailwind CSS, ESLint 등 개발 도구 설정

@jaewoni314 jaewoni314 requested a review from wantkdd April 7, 2026 07:54
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

📋 Walkthrough

세 개의 완전한 React + TypeScript + Vite 애플리케이션(mission1, mission2, mission3)이 Week04에 추가되었습니다. Mission1은 TMDB API를 이용한 영화 열람 앱으로 카테고리별 목록 및 상세 페이지 구현, Mission2는 폼 검증을 포함한 기본 로그인 기능, Mission3은 다단계 회원가입 및 localStorage 기반 인증을 확장한 구조입니다.


📊 Changes

Cohort / File(s) Summary
Mission1 설정 및 프로젝트 초기화
Week04/jaewoni314/mission1/.gitignore, README.md, package.json, index.html, vite.config.ts, eslint.config.js, tsconfig.*, src/main.tsx, src/index.css
Node.js/Vite 기반 React 프로젝트 설정. Tailwind CSS, React Router v7, TypeScript 5.8 등 표준 의존성 구성.
Mission1 API 및 데이터 계층
src/apis/axios.ts, src/apis/movies.ts, src/types/movies.ts, src/types/credit.ts
TMDB API 연동. Axios 인스턴스 설정, 영화 카테고리별 조회, 상세정보 및 출연진 데이터 타입 정의.
Mission1 라우팅 및 레이아웃
src/routes/router.tsx, src/layout/RootLayout.tsx, src/App.tsx
React Router 설정으로 6개 라우트 구성(/, /movies, /movies/now-playing, /movies/top-rated, /movies/upcoming, /movies/:movieId).
Mission1 페이지 컴포넌트
src/pages/HomePage.tsx, src/pages/NotFound.tsx, src/pages/movie/DetailPage.tsx, src/pages/movie/NowPlayingPage.tsx, src/pages/movie/PopularPage.tsx, src/pages/movie/TopRatedPage.tsx, src/pages/movie/UpcomingPage.tsx
홈, 오류 페이지, 영화 상세 및 카테고리별 목록 페이지 구현.
Mission1 리스트 및 상세 컴포넌트
src/components/movie/MovieList.tsx, src/components/movie/MovieGrid.tsx, src/components/movie/MovieItem.tsx, src/components/movie/detail/Up.tsx, src/components/movie/detail/Down.tsx
영화 목록 렌더링, 그리드 레이아웃, 개별 영화 카드, 상세 정보(포스터/출연진) 표시.
Mission1 UI 컴포넌트
src/components/GNB/Navbar.tsx, src/components/pagination/Pagination.tsx, src/components/pagination/PaginationButton.tsx, src/components/spinner/LoadingSpinner.tsx, src/components/error/Error.tsx
네비게이션, 페이지네이션, 로딩 스피너, 에러 메시지 UI 제공.
Mission1 커스텀 훅
src/hooks/useMovies.ts, src/hooks/useMovieCategories.ts, src/hooks/useMovieDetail.ts
API 호출 및 페이지네이션, 카테고리 매핑, 영화 상세정보 및 출연진 조회 로직 캡슐화.
Mission2 설정 및 프로젝트 초기화
Week04/jaewoni314/mission2/.gitignore, README.md, package.json, index.html, vite.config.ts, eslint.config.js, tsconfig.*, src/main.tsx, src/index.css
Framer Motion, React Hook Form, Zod 등 폼 처리 의존성 추가 포함.
Mission2 라우팅 및 레이아웃
src/routes/router.tsx, src/layout/RootLayout.tsx, src/App.tsx
2개 라우트(/, /login) 기본 구성.
Mission2 페이지 컴포넌트
src/pages/HomePage.tsx, src/pages/NotFound.tsx, src/pages/login/LogInPage.tsx, src/pages/login/components/LoginForm.tsx
홈 페이지, 로그인 페이지, 로그인 폼 컴포넌트(이메일/비밀번호 입력, 검증).
Mission2 UI 및 폼 컴포넌트
src/components/NavBar.tsx, src/components/button/Button.tsx, src/components/input/Input.tsx, src/components/auth/GoogleLoginButton.tsx
재사용 가능한 버튼, 입력 필드, Google 로그인 버튼, 네비게이션 바.
Mission2 폼 및 검증
src/hooks/useForm.ts, src/utils/validate.ts, src/utils/cn.ts
React Hook Form + Zod 기반 로그인 폼 검증, Tailwind 클래스 병합 유틸리티.
Mission3 설정 및 프로젝트 초기화
Week04/jaewoni314/mission3/.gitignore, README.md, package.json, index.html, vite.config.ts, eslint.config.js, tsconfig.*, src/main.tsx, src/index.css
Mission2와 유사한 설정, 추가 인증 관련 의존성.
Mission3 API 및 인증
src/apis/axios.ts, src/apis/auth.ts, src/types/auth/login.ts, src/types/auth/signup.ts, src/hooks/useLogin.ts, src/hooks/useLocalStorage.ts
Axios 기반 회원가입/로그인 API, localStorage 토큰 저장, 로그인/로그아웃 흐름 관리.
Mission3 라우팅 및 레이아웃
src/routes/router.tsx, src/layout/RootLayout.tsx, src/App.tsx
3개 라우트(/, /login, /signup) 구성.
Mission3 페이지 및 인증 컴포넌트
src/pages/HomePage.tsx, src/pages/NotFound.tsx, src/pages/login/LogInPage.tsx, src/pages/login/components/LoginForm.tsx, src/pages/signup/SignUpPage.tsx, src/pages/signup/components/SignUpForm.tsx, src/pages/signup/components/EmailStep.tsx, src/pages/signup/components/PasswordStep.tsx, src/pages/signup/components/ProfileStep.tsx
로그인, 다단계 회원가입(이메일→비밀번호→프로필) 페이지 및 컴포넌트.
Mission3 UI 및 애니메이션
src/components/NavBar.tsx, src/components/button/Button.tsx, src/components/input/Input.tsx, src/components/auth/GoogleLoginButton.tsx, src/components/animation/Animation.tsx
Framer Motion 기반 애니메이션, 비밀번호 표시/숨김 토글 기능 추가.
Mission3 폼 및 검증
src/hooks/useForm.ts, src/utils/validate.ts, src/utils/cn.ts
로그인/회원가입 다중 스키마 검증, 동적 폼 필드 에러 처리.

🔄 Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MovieApp as Movie App
    participant API as TMDB API
    participant State as Component State

    User->>MovieApp: 카테고리 클릭 (e.g., "Popular")
    MovieApp->>State: useMovieCategories("popular") 호출
    State->>API: GET /movie/popular?page=1
    API-->>State: Movie[] 반환
    State->>MovieApp: movies, loading, currentPage 업데이트
    MovieApp->>MovieApp: MovieList 렌더링
    MovieApp->>MovieApp: MovieGrid로 영화 목록 표시
    
    User->>MovieApp: 페이지네이션 "다음" 클릭
    MovieApp->>State: onChangePage(2) 호출
    State->>API: GET /movie/popular?page=2
    API-->>State: 다음 페이지 Movie[] 반환
    State->>MovieApp: movies, currentPage 업데이트
    MovieApp->>MovieApp: MovieGrid 재렌더링
    
    User->>MovieApp: 영화 카드 클릭
    MovieApp->>State: useMovieDetail(movieId) 호출
    State->>API: GET /movie/{movieId}
    State->>API: GET /movie/{movieId}/credits
    API-->>State: Movie, Credit 데이터 반환
    State->>MovieApp: DetailPage 렌더링 (Up, Down)
Loading
sequenceDiagram
    participant User
    participant SignUpApp as Signup App
    participant Form as SignUpForm
    participant Steps as Step Components
    participant API as Auth API
    participant Storage as localStorage

    User->>SignUpApp: /signup 접근
    SignUpApp->>Form: currentStep=1 (EmailStep)
    
    User->>Steps: 이메일 입력 및 "다음" 클릭
    Steps->>Form: trigger(['email']) 검증
    Form->>Steps: 검증 성공
    Steps->>Form: currentStep=2 (PasswordStep)
    
    User->>Steps: 비밀번호 입력 및 "다음" 클릭
    Steps->>Form: trigger(['password', 'passwordConfirm']) 검증
    Form->>Steps: 검증 성공
    Steps->>Form: currentStep=3 (ProfileStep)
    
    User->>Steps: 이름 입력 및 "회원가입 완료" 클릭
    Steps->>Form: trigger(['name']) 검증
    Form->>Steps: 검증 성공
    Steps->>Steps: onComplete(formData) 호출
    Steps->>API: POST /auth/signup
    API-->>Steps: SignUp 완료
    Steps->>Storage: accessToken, refreshToken 저장
    Steps->>SignUpApp: navigate('/') 리다이렉트
Loading

⏱️ Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes


🔗 Possibly Related PRs

  • PR #28: 동일한 영화 검색 앱 프로젝트 구조, 컴포넌트(App/router, Navbar, Pagination/LoadingSpinner), 영화 데이터 훅/타입, axios 설정 반복 구현.
  • PR #29: 동일한 API 계층(apis/axios, apis/movies), 데이터 훅(useMovies/useMovieCategories/useMovieDetail), UI 컴포넌트(MovieGrid/MovieItem/Pagination), 라우팅, 타입 정의가 여러 미션에서 재구현됨.

👥 Suggested Reviewers

  • wantkdd

🐰 Poem

🎬 영화 앤 로그인, 삼중 미션
폼 검증에 페이지네이션
Framer와 Zod로 춤을 추고
localStorage에 토큰 숨겨
세 마리 토끼를 한번에 잡았어! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 3개의 미션을 모두 명확하게 포함하고 있으며, 변경 사항의 주요 내용을 정확히 요약하고 있습니다.
Description check ✅ Passed PR 설명이 대부분 완성되었으며 필수 섹션(미션번호, 구현사항, 스크린샷, 체크리스트)이 모두 포함되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jaewoni314/week04

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
Week04/jaewoni314/mission1/src/components/pagination/PaginationButton.tsx-13-16 (1)

13-16: ⚠️ Potential issue | 🟡 Minor

버튼 type을 명시해 폼 내부 오동작을 방지해주세요.

Line [13]의 <button>은 기본값이 type="submit"이라, 폼 안에서 재사용되면 페이지네이션 클릭이 의도치 않은 submit을 유발할 수 있습니다.

🔧 제안 수정안
     <button
+      type="button"
       onClick={onClick}
       disabled={disabled}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/components/pagination/PaginationButton.tsx`
around lines 13 - 16, The <button> in the PaginationButton component can act as
type="submit" by default and cause unintended form submissions; update the
PaginationButton JSX (the <button> element that uses onClick and disabled props)
to explicitly set type="button" so clicks won't submit enclosing forms, keeping
existing onClick, disabled and className behavior intact.
Week04/jaewoni314/mission2/src/components/input/Input.tsx-12-14 (1)

12-14: ⚠️ Potential issue | 🟡 Minor

접근성 개선: <label><input> 연결 필요

<label>htmlFor 속성이 없고 <input>id가 없어서 스크린 리더 사용자가 레이블과 입력 필드의 관계를 인식하기 어렵습니다.

♿ 접근성 개선 제안
-const Input: React.FC<InputProps> = ({ className, label, error, ...props }) => {
+const Input: React.FC<InputProps> = ({ className, label, error, id, ...props }) => {
   return (
     <div className="flex flex-col gap-1 w-full">
       {label && (
-        <label className="text-sm font-medium text-gray-300">{label}</label>
+        <label htmlFor={id} className="text-sm font-medium text-gray-300">{label}</label>
       )}
       <input
+        id={id}
         className={cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission2/src/components/input/Input.tsx` around lines 12 -
14, The label and input are not connected: update the Input component so the
rendered <label> gets an htmlFor matching the <input>'s id. If the component
already accepts an id prop use that; otherwise generate a stable id inside the
Input component (e.g., React's useId or a useRef-based unique id) and assign it
to the <input id={...}> and the <label htmlFor={...}>; ensure you still only
render the <label> when label is provided and keep existing classNames and props
handling.
Week04/jaewoni314/mission1/index.html-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

페이지 제목이 잘못되었습니다.

이 파일은 mission1 폴더에 있지만 제목이 mission3으로 설정되어 있습니다. 복사-붙여넣기 오류로 보입니다.

🔧 수정 제안
-    <title>mission3</title>
+    <title>mission1</title>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/index.html` at line 7, The page title is
incorrect: in the index.html file the <title> element reads "mission3" but this
is the mission1 page; update the <title> element text from "mission3" to
"mission1" so the document title matches the folder and content (look for the
<title> tag in index.html).
Week04/jaewoni314/mission1/src/types/credit.ts-6-11 (1)

6-11: ⚠️ Potential issue | 🟡 Minor

profile_path는 nullable 타입으로 정의해야 합니다.

TMDB API에서 profile_path는 이미지가 없는 경우 null을 반환합니다. 현재 타입 정의가 이를 반영하지 않아, 이미지 렌더링 시 런타임 오류가 발생할 수 있습니다.

🛡️ 수정 제안
 export interface Cast {
   id: number;
   name: string;
-  profile_path: string;
+  profile_path: string | null;
   character: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/types/credit.ts` around lines 6 - 11, The Cast
interface's profile_path is currently declared as a non-nullable string but TMDB
can return null; update the Cast interface (symbol: Cast) so profile_path is
nullable (e.g., string | null) to reflect the API response and avoid runtime
errors when rendering images; search for other usages of Cast.profile_path and
adjust any code that assumes a non-null string (guard before rendering or
provide a fallback) to keep types consistent.
Week04/jaewoni314/mission3/src/components/NavBar.tsx-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Import 경로가 잘못되었습니다.

이 파일이 src/components/NavBar.tsx에 위치해 있다면, ../components/button/Button 경로는 src/components/button/Button이 아닌 상위 디렉토리의 components를 참조하게 됩니다. 올바른 상대 경로로 수정이 필요합니다.

🐛 수정 제안
-import Button from '../components/button/Button';
+import Button from './button/Button';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/components/NavBar.tsx` at line 2, The import
in NavBar.tsx is using the wrong relative path; update the import for the Button
component (the import statement referencing '../components/button/Button') to
the correct relative path from src/components (e.g. './button/Button') so it
resolves to the local button folder and exports used in the NavBar component.
Week04/jaewoni314/mission1/src/components/movie/MovieItem.tsx-20-24 (1)

20-24: ⚠️ Potential issue | 🟡 Minor

포스터 경로가 없을 때 깨진 이미지가 렌더링될 수 있습니다.

Line 21에서 movie.poster_path가 없으면 잘못된 URL이 생성됩니다. fallback 이미지 또는 placeholder 렌더링을 추가해 주세요.

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

In `@Week04/jaewoni314/mission1/src/components/movie/MovieItem.tsx` around lines
20 - 24, In MovieItem (the <img> rendering using movie.poster_path) handle
missing poster_path by using a fallback image or rendering a placeholder
element: check movie.poster_path (or use optional chaining) and if present build
the TMDB URL, otherwise use a known fallback URL or local placeholder image (or
render a div with a placeholder class); ensure the alt text remains movie.title
and keep the same className so layout doesn't break.
Week04/jaewoni314/mission3/package.json-17-19 (1)

17-19: ⚠️ Potential issue | 🟡 Minor

motion 패키지는 설치되었으나 코드에서 사용되지 않고 있습니다.

현재 코드는 framer-motion 패키지에서만 import하고 있으며(Animation.tsx, SignUpForm.tsx), motion 패키지는 전혀 사용되지 않습니다. motion은 레거시 framer-motion 패키지의 새로운 이름이 아니라 별도의 리브랜드 패키지이므로, 사용하지 않으면 제거하는 것이 좋습니다.

♻️ 미사용 패키지 제거 제안
    "framer-motion": "^12.23.22",
    "lucide-react": "^0.544.0",
-   "motion": "^12.23.22",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/package.json` around lines 17 - 19, The package
"motion" listed in package.json ("motion": "^12.23.22") is unused—your code
imports from "framer-motion" in Animation.tsx and SignUpForm.tsx but never from
"motion"; remove the "motion" entry from package.json, run your package manager
(npm/yarn/pnpm install) to update lockfile, and confirm there are no imports
from the "motion" package remaining in the codebase before committing.
Week04/jaewoni314/mission2/package.json-16-17 (1)

16-17: ⚠️ Potential issue | 🟡 Minor

불필요한 motion 패키지 제거 필요.

framer-motionmotion 패키지가 모두 포함되어 있지만, 코드베이스에서는 framer-motion만 사용 중입니다. motion 패키지는 import되거나 사용되지 않으므로 제거하는 것이 좋습니다.

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

In `@Week04/jaewoni314/mission2/package.json` around lines 16 - 17, Remove the
unused "motion" dependency from package.json (leave "framer-motion" intact) by
deleting the "motion": "^12.23.22" entry, then update the lockfile by running
your package manager (npm install or yarn install) to ensure node_modules and
package-lock.json / yarn.lock are consistent; also run a quick repo-wide search
for any remaining imports of "motion" to confirm no references remain.
Week04/jaewoni314/mission1/src/components/movie/detail/Up.tsx-9-16 (1)

9-16: ⚠️ Potential issue | 🟡 Minor

poster_path가 없을 때 배경 URL이 깨질 수 있습니다.

Line 9에서 poster_pathnull이면 .../null 요청이 발생합니다. null-safe fallback을 두는 편이 안전합니다.

🔧 제안 수정안
-  const backgroundUrl = `https://image.tmdb.org/t/p/w500/${movie.poster_path}`;
+  const backgroundUrl = movie.poster_path
+    ? `https://image.tmdb.org/t/p/w500/${movie.poster_path}`
+    : null;
...
-      style={{
-        backgroundImage: `url(${backgroundUrl})`,
-      }}
+      style={
+        backgroundUrl
+          ? { backgroundImage: `url(${backgroundUrl})` }
+          : undefined
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/components/movie/detail/Up.tsx` around lines 9
- 16, The backgroundUrl construction uses movie.poster_path directly, which
yields a broken URL when poster_path is null; change the code in Up
(backgroundUrl and the inline style that sets backgroundImage) to build a safe
fallback: check movie.poster_path (e.g., ternary or nullish coalescing) and use
either a default poster URL or an empty string when it's missing, then set
backgroundImage only to a valid url(...) string when poster exists to avoid
requests like /null.
Week04/jaewoni314/mission1/src/hooks/useMovies.ts-4-6 (1)

4-6: ⚠️ Potential issue | 🟡 Minor

fetchFunction 참조 안정성에 대한 주의가 필요합니다.

useEffect의 의존성 배열에 fetchFunction이 포함되어 있습니다. 만약 호출하는 컴포넌트에서 인라인 함수를 전달하면 (예: useMovies(() => api.getMovies())), 매 렌더링마다 새 함수 참조가 생성되어 무한 루프가 발생할 수 있습니다.

현재 useMovieCategories에서 moviesAPI의 안정적인 참조를 사용하고 있어 문제없이 동작하지만, 향후 다른 곳에서 재사용할 때 주의가 필요합니다.

🛡️ 안전한 사용을 위한 제안

옵션 1: useRef로 함수 참조 안정화

+import { useRef } from 'react';
+
 export const useMovies = (
   fetchFunction: (page?: number) => Promise<MovieResponse>
 ) => {
+  const fetchFunctionRef = useRef(fetchFunction);
+  fetchFunctionRef.current = fetchFunction;
+
   // ...
   useEffect(() => {
     const fetchMovies = async () => {
       // ...
-      const data = await fetchFunction(currentPage);
+      const data = await fetchFunctionRef.current(currentPage);
       // ...
     };
     fetchMovies();
-  }, [currentPage, fetchFunction]);
+  }, [currentPage]);

옵션 2: JSDoc으로 안정적인 참조 요구사항 문서화

+/**
+ * `@param` fetchFunction - 안정적인 참조여야 합니다 (useCallback 또는 모듈 레벨 함수)
+ */
 export const useMovies = (

Also applies to: 29-29

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

In `@Week04/jaewoni314/mission1/src/hooks/useMovies.ts` around lines 4 - 6, The
hook useMovies currently accepts fetchFunction and includes it in useEffect
dependencies, which can cause re-renders if callers pass inline functions;
stabilize the reference by capturing fetchFunction in a ref inside useMovies
(e.g., store fetchFunction in a mutable ref and call ref.current(...) inside
effects) or alternatively document via JSDoc that callers must pass a stable
function (as done by useMovieCategories passing moviesAPI); update useMovies to
useRef for fetchFunction (or add JSDoc) so useEffect no longer re-triggers on
each render.
🧹 Nitpick comments (23)
Week04/jaewoni314/mission1/src/pages/HomePage.tsx (1)

3-7: 불필요한 React Fragment 사용

자식 요소가 하나뿐이므로 Fragment(<>...</>)가 필요하지 않습니다. <div>를 직접 반환해도 됩니다.

♻️ 제안된 수정
 const HomePage = () => {
   return (
-    <>
-      <div className="flex text-9xl font-bold justify-center h-screen items-center">
-      CINEMA
-      </div>
-    </>
+    <div className="flex text-9xl font-bold justify-center h-screen items-center">
+      CINEMA
+    </div>
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/pages/HomePage.tsx` around lines 3 - 7, The
JSX fragment wrapping the single <div> is unnecessary—inside the HomePage
component (the function returning the fragment in HomePage.tsx), remove the
surrounding <>...</> and return the <div className="flex text-9xl font-bold
justify-center h-screen items-center">CINEMA</div> directly so the component
returns the single root element without the extra Fragment.
Week04/jaewoni314/mission2/src/pages/NotFound.tsx (1)

1-3: 404 페이지 문구 및 스타일링 개선 제안

"일반적으로 찾을 수 없는 페이지입니다"라는 문구가 다소 어색합니다. 일반적인 404 에러 메시지로는 "페이지를 찾을 수 없습니다" 또는 "요청하신 페이지가 존재하지 않습니다"가 더 자연스럽습니다. 또한 사용자 경험을 위해 홈으로 돌아가는 링크나 간단한 스타일링을 추가하는 것도 고려해 보세요.

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

In `@Week04/jaewoni314/mission2/src/pages/NotFound.tsx` around lines 1 - 3,
NotFound 컴포넌트의 문구와 UX를 개선하세요: 컴포넌트 NotFound의 텍스트를 더 자연스러운 문구(예: "페이지를 찾을 수 없습니다"
또는 "요청하신 페이지가 존재하지 않습니다")로 바꾸고, 홈으로 돌아가는 링크(예: react-router-dom의 Link 또는 <a
href="/">)를 추가해 사용자가 쉽게 복귀할 수 있게 하며, 간단한 스타일링(가운데 정렬, 여백, 버튼/링크 스타일 또는 className
추가)을 적용해 읽기 쉽고 접근성(예: role 또는 aria-label)이 유지되도록 수정하세요.
Week04/jaewoni314/mission1/.gitignore (1)

10-13: .env 파일이 gitignore에 누락됨

mission2의 .gitignore에는 .env가 포함되어 있지만, 여기에는 누락되어 있습니다. 환경 변수 파일이 실수로 커밋되는 것을 방지하기 위해 .env를 추가하는 것을 권장합니다.

♻️ 제안된 수정
 node_modules
 dist
 dist-ssr
 *.local
+
+.env
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/.gitignore` around lines 10 - 13, The .gitignore
in this diff is missing the .env entry which risks committing environment
variables; open the .gitignore file (the one containing node_modules, dist,
dist-ssr, and *.local) and add a line with .env to the list so .env files are
ignored by git.
Week04/jaewoni314/mission3/src/components/auth/GoogleLoginButton.tsx (1)

3-17: 구글 로그인 버튼에 클릭 핸들러가 없습니다.

현재 버튼이 UI만 렌더링하고 실제 Google OAuth 로직이 연결되어 있지 않습니다. 의도적인 플레이스홀더라면 괜찮지만, 사용자가 클릭 시 아무 반응이 없어 혼란을 줄 수 있습니다.

구글 OAuth 구현이 필요하시면 도움을 드릴 수 있습니다. 구현을 위한 이슈를 생성해 드릴까요?

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

In `@Week04/jaewoni314/mission3/src/components/auth/GoogleLoginButton.tsx` around
lines 3 - 17, The GoogleLoginButton currently only renders UI and lacks a click
handler; add an onClick that triggers the Google OAuth flow by defining a
handler (e.g., handleGoogleLogin) inside the GoogleLoginButton component and
wiring it to the Button's onClick prop; the handler should call your auth
function (e.g., signInWithGoogle, startGoogleOAuth, or redirect to your
/auth/google endpoint) and optionally set a local loading state to disable the
button while the flow starts and handle errors via a catch to surface feedback.
Week04/jaewoni314/mission2/src/components/NavBar.tsx (1)

9-14: 클릭 가능한 <h1>에 키보드 접근성 추가 필요

<h1> 요소가 클릭으로 네비게이션을 수행하지만 키보드 사용자는 접근할 수 없습니다. tabIndexonKeyDown 핸들러를 추가하거나, react-router-dom의 <Link> 컴포넌트를 사용하세요.

♻️ Link 컴포넌트 사용 제안
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, Link } from 'react-router-dom';
 import Button from './button/Button';
 
 const NavBar = () => {
   const navigate = useNavigate();
 
   return (
     <nav className="flex items-center justify-between bg-gray-900 px-6 py-4">
-      <h1
-        onClick={() => navigate('/')}
-        className="text-pink-500 font-bold text-lg cursor-pointer"
-      >
-        돌려돌려LP판
-      </h1>
+      <Link to="/" className="text-pink-500 font-bold text-lg">
+        <h1>돌려돌려LP판</h1>
+      </Link>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission2/src/components/NavBar.tsx` around lines 9 - 14,
The clickable <h1> in NavBar (the element with onClick={() => navigate('/')} and
text "돌려돌려LP판") lacks keyboard accessibility; either replace it with
react-router-dom's <Link> to="/" or add keyboard handlers: set tabIndex={0} and
implement an onKeyDown handler that triggers navigate('/') when Enter or Space
is pressed, and ensure the element retains the existing className and onClick
handler so both mouse and keyboard users can navigate.
Week04/jaewoni314/mission3/src/types/auth/login.ts (1)

6-16: LoginResponse가 항상 성공 응답만 가정하고 있습니다.

현재 data 필드가 항상 존재한다고 가정하지만, 로그인 실패 시 datanull이거나 없을 수 있습니다. 실제 API 응답에 따라 선택적 필드로 정의하거나, 성공/실패 응답 타입을 분리하는 것을 고려하세요.

♻️ 선택적 data 필드 제안
 export interface LoginResponse {
   status: boolean;
   statusCode: number;
   message: string;
-  data: {
+  data?: {
     id: number;
     name: string;
     accessToken: string;
     refreshToken: string;
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/types/auth/login.ts` around lines 6 - 16, The
LoginResponse currently assumes a successful response by requiring data; update
the type to handle failure cases by either making the data field optional or
splitting into distinct types (e.g., LoginSuccess and LoginFailure) and use a
discriminant (status or statusCode) to union them. Locate the LoginResponse
interface and change data from a required object to data?: { id: number; name:
string; accessToken: string; refreshToken: string } or create two interfaces
(LoginSuccess with data and LoginFailure with data: null or absent) and export a
union type LoginResponse = LoginSuccess | LoginFailure; update any code using
LoginResponse to handle the possible absence of data.
Week04/jaewoni314/mission3/src/components/button/Button.tsx (1)

16-17: disabled:hover 스타일 충돌 가능성

base 클래스에 disabled:hover:bg-gray-700이 있지만, 각 variant에도 별도의 disabled:hover 스타일이 정의되어 있습니다. cn() 유틸리티가 Tailwind merge를 지원하지 않으면 예상치 못한 스타일이 적용될 수 있습니다.

♻️ base에서 disabled:hover 제거 제안
   const base =
-    'rounded px-4 py-2 font-medium transition-colors duration-200 flex items-center justify-center cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-gray-700';
+    'rounded px-4 py-2 font-medium transition-colors duration-200 flex items-center justify-center cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/components/button/Button.tsx` around lines 16
- 17, The base class string (variable base) contains a generic
disabled:hover:bg-gray-700 which conflicts with variant-specific disabled:hover
rules; remove disabled:hover:bg-gray-700 from base and keep each variant's
disabled:hover classes in the variants map so variant styles take precedence,
and/or update the cn() usage to use a Tailwind-aware merge utility (e.g.,
twMerge) if you prefer merging behavior—change references to base and ensure
Button component uses the updated base and variant keys (the base variable, the
variants map, and the cn() call).
Week04/jaewoni314/mission3/src/components/animation/Animation.tsx (1)

36-36: className 결합 방식은 안전하게 처리하는 편이 좋습니다.

현재는 className이 없을 때 "undefined" 문자열이 포함될 수 있어, 조건부 결합으로 바꾸는 것을 권장합니다.

간단한 개선 예시
-      className={`w-full max-w-sm ${className}`}
+      className={['w-full max-w-sm', className].filter(Boolean).join(' ')}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/components/animation/Animation.tsx` at line
36, The template literal in the Animation component that builds the element's
className (currently `className={`w-full max-w-sm ${className}`}`) can inject
the string "undefined" when no className prop is passed; update the className
composition in Animation.tsx to handle missing values safely (for example use a
nullish check or conditional concatenation, or use a utility like
clsx/classnames) so the base classes "w-full max-w-sm" are always present and
any optional className is only appended when defined.
Week04/jaewoni314/mission3/src/pages/login/components/LoginForm.tsx (1)

26-31: 로그인 폼에도 비밀번호 보기 토글을 일관되게 적용하는 것을 권장합니다.

Input 컴포넌트가 이미 토글을 지원하므로, 로그인에서도 같은 UX를 제공하는 편이 자연스럽습니다.

적용 예시
         <Input
           placeholder="비밀번호를 입력해주세요!"
           type="password"
+          showPasswordToggle
           error={errors.password?.message}
           {...register('password')}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/pages/login/components/LoginForm.tsx` around
lines 26 - 31, LoginForm currently passes type="password" to the Input for the
password field but doesn't enable the Input's built-in show/hide toggle; update
the LoginForm password field to use the Input's toggle/show-password prop (the
same prop used elsewhere in the app) instead of hardcoding type="password" so
users can toggle visibility while preserving validation via register('password')
and displaying errors via errors.password?.message; ensure the Input receives
any required toggle handler/prop name from the Input component API so the
existing register and error props remain unchanged.
Week04/jaewoni314/mission1/src/pages/NotFound.tsx (1)

1-5: 복귀 동선(홈 이동) 추가를 권장합니다.

Line 2에서 안내 문구만 제공되어 사용자가 다음 행동을 결정하기 어렵습니다. Link로 홈 이동 버튼을 함께 제공하면 에러 페이지 UX가 좋아집니다.

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

In `@Week04/jaewoni314/mission1/src/pages/NotFound.tsx` around lines 1 - 5, The
NotFound component only renders a message and needs a clear return path for
users; update the NotFound functional component to import Link from your router
(e.g., react-router-dom) and render a home navigation control (a Link or button
with to="/" or navigate("/") handler) alongside the message so users can easily
go back to the homepage; modify the JSX in NotFound to include the Link element
and add the corresponding import at the top of the file.
Week04/jaewoni314/mission1/src/components/spinner/LoadingSpinner.tsx (1)

6-6: 스피너 크기가 너무 큽니다.

size={500}은 500픽셀로, 일반적인 로딩 스피너 크기(40-80px)에 비해 매우 큽니다. 작은 화면에서는 레이아웃 문제가 발생하거나 사용자 경험이 저하될 수 있습니다.

♻️ 권장 수정안
-      <ClipLoader color="#10b981" size={500} />
+      <ClipLoader color="#10b981" size={50} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/components/spinner/LoadingSpinner.tsx` at line
6, The ClipLoader in LoadingSpinner.tsx uses size={500}, which is far too large;
update the ClipLoader usage in the LoadingSpinner component to use a sensible
spinner size (e.g., between 40 and 80) or make the size configurable via a prop
(e.g., spinnerSize) so small screens are handled gracefully; locate the
ClipLoader call in LoadingSpinner and replace size={500} with a smaller fixed
value (like 48) or wire it to the new prop and ensure any parent usages pass an
appropriate size.
Week04/jaewoni314/mission2/src/routes/router.tsx (1)

12-17: 중첩 라우트에서 절대 경로 대신 상대 경로 사용을 권장합니다.

children 내에서 path: '/'path: '/login'처럼 절대 경로를 사용하고 있습니다. mission3의 라우터처럼 index: true와 상대 경로('login')를 사용하는 것이 react-router-dom의 관례입니다.

♻️ 권장 수정안
     children: [
-      { path: '/', element: <HomePage /> },
+      { index: true, element: <HomePage /> },
       {
-        path: '/login',
+        path: 'login',
         element: <LogIn />,
       },
     ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission2/src/routes/router.tsx` around lines 12 - 17, In
the routes children array inside router.tsx replace absolute paths with relative
routes: change the route for HomePage from path: '/' to index: true (referencing
the HomePage element) and change the LogIn route from path: '/login' to path:
'login' (referencing the LogIn element) so the children use relative routing
consistent with react-router-dom conventions.
Week04/jaewoni314/mission3/src/components/NavBar.tsx (1)

9-14: 클릭 가능한 제목에 키보드 접근성이 부족합니다.

h1 요소에 onClick 핸들러가 있지만, 키보드 사용자가 접근할 수 없습니다. role="button", tabIndex={0}, onKeyDown 핸들러를 추가하거나, 시맨틱하게 button 또는 Link 컴포넌트를 사용하는 것이 좋습니다.

♻️ Link 컴포넌트 사용 권장
+import { useNavigate, Link } from 'react-router-dom';
-import { useNavigate } from 'react-router-dom';
...
-      <h1
-        onClick={() => navigate('/')}
-        className="text-pink-500 font-bold text-lg cursor-pointer"
-      >
-         돌려돌려LP판
-      </h1>
+      <Link to="/" className="text-pink-500 font-bold text-lg">
+        돌려돌려LP판
+      </Link>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/components/NavBar.tsx` around lines 9 - 14,
The clickable title in NavBar.tsx is currently an h1 with an onClick using
navigate('/') which lacks keyboard accessibility; update the element to a
semantic interactive element (preferably replace the h1 with a Link or button)
or add role="button", tabIndex={0}, and an onKeyDown handler that triggers the
same navigate('/') logic (handle Enter/Space) so keyboard users can activate it;
ensure the element keeps the same className and text content (돌려돌려LP판) and that
any ARIA attributes remain accurate.
Week04/jaewoni314/mission2/src/pages/login/LogInPage.tsx (1)

10-12: 로그인 처리가 콘솔 로깅만 수행합니다.

Mission2의 범위상 의도된 구현일 수 있으나, 실제 로그인 로직 구현 시 이 부분을 업데이트해야 합니다.

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

In `@Week04/jaewoni314/mission2/src/pages/login/LogInPage.tsx` around lines 10 -
12, The handleLogin function currently only logs the LoginFormValues; replace
this placeholder with real async login handling: call your authentication
API/client from handleLogin (make it async), wrap the request in try/catch, on
success store the returned token/user (e.g., in localStorage or auth context)
and navigate to the protected route, and on failure show a user-facing error
message (e.g., set an error state or toast). Keep the function name handleLogin
and the LoginFormValues shape, ensure proper typing of the async return, and
remove the console.log after implementing the above.
Week04/jaewoni314/mission3/src/types/auth/signup.ts (1)

1-16: 인터페이스 간 중복을 줄일 수 있습니다.

SignUpRequestBodySignUpFormValues에서 passwordConfirm만 제외한 형태입니다. TypeScript의 Omit 유틸리티 타입을 활용하면 중복을 줄이고 유지보수성을 높일 수 있습니다.

♻️ 유틸리티 타입 활용 제안
 export interface SignUpFormValues {
   email: string;
   password: string;
   passwordConfirm: string;
   name: string;
   bio?: string;
   avatar?: File | null;
 }

-export interface SignUpRequestBody {
-  email: string;
-  password: string;
-  name: string;
-  bio?: string;
-  avatar?: File | null;
-}
+export type SignUpRequestBody = Omit<SignUpFormValues, 'passwordConfirm'>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/types/auth/signup.ts` around lines 1 - 16,
SignUpRequestBody duplicates SignUpFormValues except for passwordConfirm;
replace the duplicate definition by deriving SignUpRequestBody from
SignUpFormValues using TypeScript's Omit utility so SignUpRequestBody =
Omit<SignUpFormValues, 'passwordConfirm'> (keep optional fields like bio and
avatar as-is via the base type), update any imports/usages if necessary to
reference the single source of truth (SignUpFormValues).
Week04/jaewoni314/mission3/src/hooks/useLocalStorage.ts (2)

10-19: getItem 반환 타입 명확화 권장.

getItem이 항목이 없거나 파싱 실패 시 암시적으로 undefined를 반환합니다. 명시적인 반환 타입과 return undefined를 추가하면 코드 의도가 더 명확해집니다.

♻️ 반환 타입 명확화
-  const getItem = () => {
+  const getItem = <T>(): T | undefined => {
     try {
       const item = window.localStorage.getItem(key);
       if (item) {
-        return JSON.parse(item);
+        return JSON.parse(item) as T;
       }
+      return undefined;
     } catch (error) {
       console.log(error);
+      return undefined;
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/hooks/useLocalStorage.ts` around lines 10 -
19, The getItem function currently returns implicitly undefined when no item or
on parse error; update getItem (in useLocalStorage.ts) to explicitly declare its
return type (e.g., T | undefined) and add explicit return undefined branches:
after the if (item) block return undefined when item is null, and in the catch
block return undefined after logging the error so the intention and types are
clear to callers.

5-6: 에러 로깅에 console.error 사용 권장.

에러 상황에서 console.log 대신 console.error를 사용하면 브라우저 개발자 도구에서 에러를 더 쉽게 식별할 수 있습니다.

♻️ console.error로 변경
     } catch (error) {
-      console.log(error);
+      console.error('localStorage setItem error:', error);
     }

Also applies to: 16-17, 24-25

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

In `@Week04/jaewoni314/mission3/src/hooks/useLocalStorage.ts` around lines 5 - 6,
The catch blocks in useLocalStorage (in
Week04/jaewoni314/mission3/src/hooks/useLocalStorage.ts) currently call
console.log(error); replace those with console.error(error) so errors are
properly reported in the browser console; update every catch site in the file
(including the ones around the initialization and storage access logic inside
the useLocalStorage implementation) to use console.error and keep existing error
messages/context if any.
Week04/jaewoni314/mission2/src/utils/validate.ts (1)

8-12: 메시지 포맷 일관성 개선 권장.

다른 유효성 검사 메시지는 { message: '...' } 객체 형식을 사용하지만, max(16, '...')는 문자열 단축 형식을 사용합니다. 코드 일관성을 위해 통일하는 것이 좋습니다.

♻️ 메시지 포맷 일관성 수정
   password: z
     .string()
     .min(1, { message: '비밀번호를 입력해주세요.' })
     .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
-    .max(16, '비밀번호는 16자 이하여야 합니다.'),
+    .max(16, { message: '비밀번호는 16자 이하여야 합니다.' }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission2/src/utils/validate.ts` around lines 8 - 12, The
password schema's max validator uses the short string format max(16, '...')
which is inconsistent with other validators that pass an object with a message;
update the password schema (the password: z.string().min(...).min(...).max(...)
chain in validate.ts) to use max(16, { message: '비밀번호는 16자 이하여야 합니다.' }) so all
validation messages follow the { message: '...' } object format.
Week04/jaewoni314/mission1/src/types/movies.ts (1)

1-11: TMDB API 응답과 타입 불일치 가능성 확인 필요.

TMDB API에서 poster_pathbackdrop_pathnull일 수 있으며, runtimetagline은 목록 엔드포인트가 아닌 상세 엔드포인트에서만 반환됩니다. 실제 API 응답과 타입이 일치하는지 확인이 필요합니다.

♻️ 타입 안전성을 위한 수정 제안
 export type Movie = {
   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;
-  runtime: number;
-  tagline: string;
+  runtime?: number;
+  tagline?: string;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/types/movies.ts` around lines 1 - 11, The
Movie type doesn't match TMDB responses: poster_path and backdrop_path can be
null, and runtime/tagline are only returned by the detail endpoint (may be
absent). Update the Movie type definition (the Movie export) to allow
nullable/optional fields—e.g. make poster_path and backdrop_path string | null
(or optional and null), and make runtime and tagline optional and possibly null
(number | null and string | null) — and ensure the rest of your code consumes
those fields defensively or queries the detail endpoint when runtime/tagline are
required.
Week04/jaewoni314/mission1/src/apis/movies.ts (1)

6-14: 쿼리 파라미터 처리 방식이 일관되지 않습니다.

fetchMovies 함수에서 pageparams 객체로 전달하고, language는 엔드포인트 문자열에 직접 포함하고 있습니다. 기능적으로는 동작하지만, 일관성을 위해 모든 파라미터를 params 객체로 통합하는 것을 권장합니다.

♻️ 쿼리 파라미터 통합 제안
 const fetchMovies = async (
   endpoint: string,
-  page: number = 1
+  page: number = 1,
+  language: string = 'ko-KR'
 ): Promise<MovieResponse> => {
   const { data } = await API.get<MovieResponse>(endpoint, {
-    params: { page },
+    params: { page, language },
   });
   return data;
 };

 // 영화 카테고리별 API
 export const moviesAPI = {
   getNowPlaying: (page: number = 1) =>
-    fetchMovies('/movie/now_playing?language=ko-KR', page),
+    fetchMovies('/movie/now_playing', page),
   getTopRated: (page: number = 1) =>
-    fetchMovies('/movie/top_rated?language=ko-KR', page),
+    fetchMovies('/movie/top_rated', page),
   getUpcoming: (page: number = 1) =>
-    fetchMovies('/movie/upcoming?language=ko-KR', page),
+    fetchMovies('/movie/upcoming', page),
   getPopular: (page: number = 1) =>
-    fetchMovies('/movie/popular?language=ko-KR', page),
+    fetchMovies('/movie/popular', page),
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/apis/movies.ts` around lines 6 - 14, The
fetchMovies function mixes query parameter strategies by embedding language in
the endpoint string while sending page via the params object; update fetchMovies
to accept a params object (e.g., { page?: number; language?: string }) or add a
language parameter and pass both page and language inside the params argument to
API.get<MovieResponse>(endpoint, { params: { ... } }) so all query parameters
are sent consistently; adjust the function signature and callers that use
fetchMovies accordingly (refer to fetchMovies and MovieResponse).
Week04/jaewoni314/mission3/src/pages/signup/components/SignUpForm.tsx (1)

42-77: watch() 호출 최적화를 고려해 보세요.

현재 renderCurrentStep 함수에서 watch('email'), watch('password') 등을 직접 호출하고 있습니다. 매 렌더링마다 새로운 값을 구독하게 되어 불필요한 리렌더링이 발생할 수 있습니다.

현재 규모에서는 큰 문제가 되지 않지만, 추후 최적화가 필요하다면 useWatch 훅을 사용하거나 컴포넌트 상단에서 필요한 값들을 한 번에 구독하는 방식을 고려해 보세요.

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

In `@Week04/jaewoni314/mission3/src/pages/signup/components/SignUpForm.tsx` around
lines 42 - 77, renderCurrentStep currently calls watch('email'),
watch('password'), watch('name') inside the render which can re-subscribe on
every render; replace those inline watch calls by subscribing once at the top of
the component (or use React Hook Form's useWatch) and pass the subscribed values
into EmailStep, PasswordStep and ProfileStep (refer to renderCurrentStep,
EmailStep, PasswordStep, ProfileStep and consider useWatch or a single watch
call at the component top) to avoid unnecessary re-subscriptions and re-renders.
Week04/jaewoni314/mission3/src/pages/signup/components/ProfileStep.tsx (2)

55-59: 인라인 유효성 검사 규칙이 Zod 스키마와 중복됩니다.

부모 컴포넌트(SignUpForm)에서 이미 zodResolver(signUpSchema)를 사용하고 있으므로, register에 인라인 required 규칙을 추가하면 유효성 검사 로직이 중복됩니다. 두 곳에서 유지보수해야 하고, 에러 메시지가 불일치할 수 있습니다.

♻️ 인라인 규칙 제거 제안
         <Input
           placeholder="이름"
-          {...register('name', { required: '이름을 입력해주세요' })}
+          {...register('name')}
           error={errors.name?.message}
         />

Zod 스키마(signUpSchema)에서 name 필드의 유효성 검사 및 에러 메시지를 정의하세요.

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

In `@Week04/jaewoni314/mission3/src/pages/signup/components/ProfileStep.tsx`
around lines 55 - 59, Remove the inline validation rule from the Input's
register call to avoid duplicating Zod validation: in ProfileStep.tsx delete the
`{ required: '이름을 입력해주세요' }` passed to `register('name', ...)` and let the
parent `SignUpForm`'s `zodResolver(signUpSchema)` handle the check; ensure
`signUpSchema` defines the `name` field and its error message so
`errors.name?.message` on the `Input` still displays the Zod-provided message.

34-44: watch() 대신 getValues()를 사용하는 것이 더 적합합니다.

handleComplete 함수에서 폼 데이터를 한 번만 읽어오는 용도로 watch()를 사용하고 있습니다. watch()는 값 변경을 구독하고 리렌더링을 유발하지만, 여기서는 버튼 클릭 시점의 값만 필요하므로 getValues()가 더 적합합니다.

다만, 현재 props로 watch만 전달받고 있어 변경하려면 SignUpForm에서 getValues도 전달해야 합니다. 현재 구현도 정상 동작하므로 선택적으로 개선하시면 됩니다.

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

In `@Week04/jaewoni314/mission3/src/pages/signup/components/ProfileStep.tsx`
around lines 34 - 44, handleComplete currently uses watch() to read form values
which subscribes to changes and can cause unnecessary re-renders; replace the
watch() call with getValues() to read values once on button click, i.e., in
handleComplete call const formData = getValues() and use
formData.email/password/name when invoking onComplete; because only watch is
currently passed in, update the SignUpForm parent to also pass getValues through
props (or add it to the component's props) so handleComplete can call getValues
directly; keep trigger(['name']) and onComplete usage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 281cc811-8da8-4269-b26f-cdf45b94a647

📥 Commits

Reviewing files that changed from the base of the PR and between b0ad89d and 64513c4.

⛔ Files ignored due to path filters (9)
  • Week04/jaewoni314/mission1/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week04/jaewoni314/mission1/public/vite.svg is excluded by !**/*.svg
  • Week04/jaewoni314/mission1/src/assets/react.svg is excluded by !**/*.svg
  • Week04/jaewoni314/mission2/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week04/jaewoni314/mission2/public/vite.svg is excluded by !**/*.svg
  • Week04/jaewoni314/mission2/src/assets/react.svg is excluded by !**/*.svg
  • Week04/jaewoni314/mission3/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week04/jaewoni314/mission3/public/vite.svg is excluded by !**/*.svg
  • Week04/jaewoni314/mission3/src/assets/react.svg is excluded by !**/*.svg
📒 Files selected for processing (102)
  • Week04/jaewoni314/mission1/.gitignore
  • Week04/jaewoni314/mission1/README.md
  • Week04/jaewoni314/mission1/eslint.config.js
  • Week04/jaewoni314/mission1/index.html
  • Week04/jaewoni314/mission1/package.json
  • Week04/jaewoni314/mission1/src/App.tsx
  • Week04/jaewoni314/mission1/src/apis/axios.ts
  • Week04/jaewoni314/mission1/src/apis/movies.ts
  • Week04/jaewoni314/mission1/src/components/GNB/Navbar.tsx
  • Week04/jaewoni314/mission1/src/components/error/Error.tsx
  • Week04/jaewoni314/mission1/src/components/movie/MovieGrid.tsx
  • Week04/jaewoni314/mission1/src/components/movie/MovieItem.tsx
  • Week04/jaewoni314/mission1/src/components/movie/MovieList.tsx
  • Week04/jaewoni314/mission1/src/components/movie/detail/Down.tsx
  • Week04/jaewoni314/mission1/src/components/movie/detail/Up.tsx
  • Week04/jaewoni314/mission1/src/components/pagination/Pagination.tsx
  • Week04/jaewoni314/mission1/src/components/pagination/PaginationButton.tsx
  • Week04/jaewoni314/mission1/src/components/spinner/LoadingSpinner.tsx
  • Week04/jaewoni314/mission1/src/hooks/useMovieCategories.ts
  • Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts
  • Week04/jaewoni314/mission1/src/hooks/useMovies.ts
  • Week04/jaewoni314/mission1/src/index.css
  • Week04/jaewoni314/mission1/src/layout/RootLayout.tsx
  • Week04/jaewoni314/mission1/src/main.tsx
  • Week04/jaewoni314/mission1/src/pages/HomePage.tsx
  • Week04/jaewoni314/mission1/src/pages/NotFound.tsx
  • Week04/jaewoni314/mission1/src/pages/movie/DetailPage.tsx
  • Week04/jaewoni314/mission1/src/pages/movie/NowPlayingPage.tsx
  • Week04/jaewoni314/mission1/src/pages/movie/PopularPage.tsx
  • Week04/jaewoni314/mission1/src/pages/movie/TopRatedPage.tsx
  • Week04/jaewoni314/mission1/src/pages/movie/UpcomingPage.tsx
  • Week04/jaewoni314/mission1/src/routes/router.tsx
  • Week04/jaewoni314/mission1/src/types/credit.ts
  • Week04/jaewoni314/mission1/src/types/movies.ts
  • Week04/jaewoni314/mission1/tailwind.config.ts
  • Week04/jaewoni314/mission1/tsconfig.app.json
  • Week04/jaewoni314/mission1/tsconfig.json
  • Week04/jaewoni314/mission1/tsconfig.node.json
  • Week04/jaewoni314/mission1/vite.config.ts
  • Week04/jaewoni314/mission2/.gitignore
  • Week04/jaewoni314/mission2/README.md
  • Week04/jaewoni314/mission2/eslint.config.js
  • Week04/jaewoni314/mission2/index.html
  • Week04/jaewoni314/mission2/package.json
  • Week04/jaewoni314/mission2/src/App.tsx
  • Week04/jaewoni314/mission2/src/components/NavBar.tsx
  • Week04/jaewoni314/mission2/src/components/button/Button.tsx
  • Week04/jaewoni314/mission2/src/components/input/Input.tsx
  • Week04/jaewoni314/mission2/src/hooks/useForm.ts
  • Week04/jaewoni314/mission2/src/index.css
  • Week04/jaewoni314/mission2/src/layout/RootLayout.tsx
  • Week04/jaewoni314/mission2/src/main.tsx
  • Week04/jaewoni314/mission2/src/pages/HomePage.tsx
  • Week04/jaewoni314/mission2/src/pages/NotFound.tsx
  • Week04/jaewoni314/mission2/src/pages/login/LogInPage.tsx
  • Week04/jaewoni314/mission2/src/pages/login/components/LoginForm.tsx
  • Week04/jaewoni314/mission2/src/routes/router.tsx
  • Week04/jaewoni314/mission2/src/utils/cn.ts
  • Week04/jaewoni314/mission2/src/utils/validate.ts
  • Week04/jaewoni314/mission2/tailwind.config.ts
  • Week04/jaewoni314/mission2/tsconfig.app.json
  • Week04/jaewoni314/mission2/tsconfig.json
  • Week04/jaewoni314/mission2/tsconfig.node.json
  • Week04/jaewoni314/mission2/vite.config.ts
  • Week04/jaewoni314/mission3/.gitignore
  • Week04/jaewoni314/mission3/README.md
  • Week04/jaewoni314/mission3/eslint.config.js
  • Week04/jaewoni314/mission3/index.html
  • Week04/jaewoni314/mission3/package.json
  • Week04/jaewoni314/mission3/src/App.tsx
  • Week04/jaewoni314/mission3/src/apis/auth.ts
  • Week04/jaewoni314/mission3/src/apis/axios.ts
  • Week04/jaewoni314/mission3/src/components/NavBar.tsx
  • Week04/jaewoni314/mission3/src/components/animation/Animation.tsx
  • Week04/jaewoni314/mission3/src/components/auth/GoogleLoginButton.tsx
  • Week04/jaewoni314/mission3/src/components/button/Button.tsx
  • Week04/jaewoni314/mission3/src/components/input/Input.tsx
  • Week04/jaewoni314/mission3/src/hooks/useForm.ts
  • Week04/jaewoni314/mission3/src/hooks/useLocalStorage.ts
  • Week04/jaewoni314/mission3/src/hooks/useLogin.ts
  • Week04/jaewoni314/mission3/src/index.css
  • Week04/jaewoni314/mission3/src/layout/RootLayout.tsx
  • Week04/jaewoni314/mission3/src/main.tsx
  • Week04/jaewoni314/mission3/src/pages/HomePage.tsx
  • Week04/jaewoni314/mission3/src/pages/NotFound.tsx
  • Week04/jaewoni314/mission3/src/pages/login/LogInPage.tsx
  • Week04/jaewoni314/mission3/src/pages/login/components/LoginForm.tsx
  • Week04/jaewoni314/mission3/src/pages/signup/SignUpPage.tsx
  • Week04/jaewoni314/mission3/src/pages/signup/components/EmailStep.tsx
  • Week04/jaewoni314/mission3/src/pages/signup/components/PasswordStep.tsx
  • Week04/jaewoni314/mission3/src/pages/signup/components/ProfileStep.tsx
  • Week04/jaewoni314/mission3/src/pages/signup/components/SignUpForm.tsx
  • Week04/jaewoni314/mission3/src/routes/router.tsx
  • Week04/jaewoni314/mission3/src/types/auth/login.ts
  • Week04/jaewoni314/mission3/src/types/auth/signup.ts
  • Week04/jaewoni314/mission3/src/utils/cn.ts
  • Week04/jaewoni314/mission3/src/utils/validate.ts
  • Week04/jaewoni314/mission3/tailwind.config.ts
  • Week04/jaewoni314/mission3/tsconfig.app.json
  • Week04/jaewoni314/mission3/tsconfig.json
  • Week04/jaewoni314/mission3/tsconfig.node.json
  • Week04/jaewoni314/mission3/vite.config.ts

Comment on lines +4 to +12
const accessToken = import.meta.env.VITE_API_KEY;

// 기본 axios 인스턴스 생성
export const API = axios.create({
baseURL: baseURL,
headers: {
accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
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

클라이언트 번들에 Bearer 토큰을 직접 넣는 구조는 노출 리스크가 큽니다.

Line 4, Line 11처럼 VITE_API_KEY를 Authorization 헤더로 주입하면 브라우저에서 토큰이 그대로 확인됩니다. 프론트는 백엔드/프록시 엔드포인트만 호출하고, 실제 API 토큰은 서버에서 보관하는 구조로 분리하는 것이 안전합니다.

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

In `@Week04/jaewoni314/mission1/src/apis/axios.ts` around lines 4 - 12, The client
currently injects the API token into the front-end bundle by reading accessToken
(import.meta.env.VITE_API_KEY) and setting it on the API axios instance (API
created via axios.create with Authorization header); remove the Authorization
header from the client-side API instance and instead call a server-side/proxy
endpoint that attaches the Bearer token server-side. Concretely: delete or stop
using accessToken in the API axios.create configuration, change front-end calls
to hit a backend route (e.g., /api/proxy/*) which uses the real token stored
securely on the server to call the external API, and update server code to read
the secret and set Authorization on outbound requests so the token is never
exposed in the browser.

Comment on lines +16 to +19
<div
onClick={handleMovieDetailClick}
className="relative overflow-hidden rounded-xl cursor-pointer"
>
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

클릭 가능한 div는 키보드 접근이 막혀 있습니다.

현재 구조는 마우스 사용자만 상세 이동이 가능합니다. Link 또는 button 기반으로 바꿔 키보드 포커스/Enter 접근을 보장해 주세요.

접근성 개선 예시
-import { useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
@@
-  const navigate = useNavigate();
   const movieId = movie.id;
-
-  const handleMovieDetailClick = () => {
-    if (!movieId) return;
-    navigate(`/movies/${movieId}`);
-  };
   return (
-    <div
-      onClick={handleMovieDetailClick}
-      className="relative overflow-hidden rounded-xl cursor-pointer"
-    >
+    <Link
+      to={movieId ? `/movies/${movieId}` : '#'}
+      className="relative overflow-hidden rounded-xl cursor-pointer block"
+      aria-disabled={!movieId}
+    >
@@
-    </div>
+    </Link>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/components/movie/MovieItem.tsx` around lines
16 - 19, The clickable <div> using onClick (handleMovieDetailClick) is not
keyboard-accessible; replace it with a semantic interactive element (e.g., a
<button> or your routing <Link>) so it supports focus and Enter/Space
activation, preserve the existing className and handleMovieDetailClick behavior
(or call it from the element's onClick/onKeyPress as needed), and ensure any
image/child markup remains unchanged so styling and navigation still work;
update tests/ARIA if present to reflect the new interactive element.

Comment on lines +12 to +34
useEffect(() => {
const fetchMovieData = async () => {
try {
setLoading(true);
setError(null);

const [movieData, creditsData] = await Promise.all([
getMovieDetail(movieId),
getMovieCredits(movieId),
]);

setMovie(movieData);
setCredits(creditsData);
} catch (err) {
console.error(err);
setError('영화 데이터를 불러오는 데 실패했습니다.');
} finally {
setLoading(false);
}
};

fetchMovieData();
}, [movieId]);
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
nl -ba Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts | sed -n '1,140p'
rg -n "return \\(\\) =>|cancelled|AbortController" Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 113


🏁 Script executed:

cat -n Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts | head -50

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 1446


🏁 Script executed:

rg -n "cancelled|AbortController|cleanup|return.*=>.*{" Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 230


🏁 Script executed:

rg -n "return" Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts | head -5

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 86


🏁 Script executed:

rg -n "cancelled" Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 55


비동기 응답 경쟁으로 stale 데이터가 반영될 수 있습니다.

movieId 변경 중에 이전 Promise.all 요청(18-21줄)이 완료되면, 최신 movieId의 결과를 이전 응답이 덮어쓸 수 있습니다. cleanup 가드를 추가해 상태 반영을 방지하세요.

🔧 제안 수정안
   useEffect(() => {
+    let cancelled = false;
+
     const fetchMovieData = async () => {
       try {
         setLoading(true);
         setError(null);

         const [movieData, creditsData] = await Promise.all([
           getMovieDetail(movieId),
           getMovieCredits(movieId),
         ]);

-        setMovie(movieData);
-        setCredits(creditsData);
+        if (!cancelled) {
+          setMovie(movieData);
+          setCredits(creditsData);
+        }
       } catch (err) {
-        console.error(err);
-        setError('영화 데이터를 불러오는 데 실패했습니다.');
+        if (!cancelled) {
+          console.error(err);
+          setError('영화 데이터를 불러오는 데 실패했습니다.');
+        }
       } finally {
-        setLoading(false);
+        if (!cancelled) {
+          setLoading(false);
+        }
       }
     };

     fetchMovieData();
+    return () => {
+      cancelled = true;
+    };
   }, [movieId]);
📝 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
useEffect(() => {
const fetchMovieData = async () => {
try {
setLoading(true);
setError(null);
const [movieData, creditsData] = await Promise.all([
getMovieDetail(movieId),
getMovieCredits(movieId),
]);
setMovie(movieData);
setCredits(creditsData);
} catch (err) {
console.error(err);
setError('영화 데이터를 불러오는 데 실패했습니다.');
} finally {
setLoading(false);
}
};
fetchMovieData();
}, [movieId]);
useEffect(() => {
let cancelled = false;
const fetchMovieData = async () => {
try {
setLoading(true);
setError(null);
const [movieData, creditsData] = await Promise.all([
getMovieDetail(movieId),
getMovieCredits(movieId),
]);
if (!cancelled) {
setMovie(movieData);
setCredits(creditsData);
}
} catch (err) {
if (!cancelled) {
console.error(err);
setError('영화 데이터를 불러오는 데 실패했습니다.');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchMovieData();
return () => {
cancelled = true;
};
}, [movieId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/hooks/useMovieDetail.ts` around lines 12 - 34,
The effect’s async fetchMovieData can race and set stale state when movieId
changes; add a cleanup guard (e.g., a local "cancelled" flag or requestId token
inside the useEffect) checked before calling setMovie, setCredits, setError and
setLoading so only the latest request updates state; implement the guard in
useEffect surrounding fetchMovieData and flip it in the returned cleanup
function to prevent older Promise.all results from overwriting newer movieId
responses.

Comment on lines +5 to +6
const { movieId } = useParams<{ movieId: string }>();
const isDetail = window.location.pathname.includes('/movies/' + movieId);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# RootLayout의 현재 판별 로직과 라우트 정의를 함께 확인합니다.
rg -n -C2 "window\\.location\\.pathname|useMatch|useParams" Week04/jaewoni314/mission1/src/layout/RootLayout.tsx
rg -n -C3 "path:\\s*['\"]/movies|:movieId" Week04/jaewoni314/mission1/src/routes/router.tsx

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 522


🏁 Script executed:

cat -n Week04/jaewoni314/mission1/src/layout/RootLayout.tsx

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 549


🏁 Script executed:

# Check if useMatch is already imported or available in react-router-dom
rg -n "useMatch" Week04/jaewoni314/mission1/src

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 55


🏁 Script executed:

# Check how isDetail variable is used elsewhere in the file
rg -n "isDetail" Week04/jaewoni314/mission1/src

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 290


🏁 Script executed:

cat -n Week04/jaewoni314/mission1/src/routes/router.tsx

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 1514


🏁 Script executed:

# Check for edge case: does movieId need to be fully matched or can partial match cause issues
# For example, if movieId="1" and path contains "/movies/123", would it match incorrectly
python3 << 'EOF'
# Simulate current behavior
movieId = "1"
path1 = "/movies/1"
path2 = "/movies/123"
path3 = "/movies/456/comments"

print(f"movieId: {movieId}")
print(f"path1 '{path1}': includes → {path1.find('/movies/' + movieId) >= 0}")
print(f"path2 '{path2}': includes → {path2.find('/movies/' + movieId) >= 0}")  # False positive!
print(f"path3 '{path3}': includes → {path3.find('/movies/' + movieId) >= 0}")
EOF

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 202


라우터 매칭을 통한 상세 페이지 판별 방식으로 변경을 권장합니다.

현재 코드에는 두 가지 문제가 있습니다:

  1. useParams 사용 불가: useParams<{ movieId: string }>()는 부모 컴포넌트(RootLayout)에서 자식 라우트의 매개변수를 추출할 수 없습니다. movieId는 자식 라우트 'movies/:movieId'에서 정의되므로 undefined가 됩니다.
  2. 문자열 매칭의 엣지 케이스: window.location.pathname.includes('/movies/' + movieId)는 movieId="1"일 때 "/movies/123" 경로와도 일치하는 오탐 가능성이 있습니다.

useMatch('/movies/:movieId')로 변경하면 라우터 상태를 직접 확인하므로 두 문제를 모두 해결하고 코드 의도도 명확해집니다.

권장 수정안
-import { Outlet, useParams } from 'react-router-dom';
+import { Outlet, useMatch } from 'react-router-dom';
 import Navbar from '../components/GNB/Navbar';

 const RootLayout = () => {
-  const { movieId } = useParams<{ movieId: string }>();
-  const isDetail = window.location.pathname.includes('/movies/' + movieId);
+  const isDetail = Boolean(useMatch('/movies/:movieId'));
   return (
     <>
       {!isDetail && <Navbar />}
       <Outlet />
📝 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
const { movieId } = useParams<{ movieId: string }>();
const isDetail = window.location.pathname.includes('/movies/' + movieId);
import { Outlet, useMatch } from 'react-router-dom';
import Navbar from '../components/GNB/Navbar';
const RootLayout = () => {
const isDetail = Boolean(useMatch('/movies/:movieId'));
return (
<>
{!isDetail && <Navbar />}
<Outlet />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission1/src/layout/RootLayout.tsx` around lines 5 - 6, The
current RootLayout reads movieId via useParams and checks isDetail by matching
window.location.pathname which fails because useParams cannot access child route
params and the string includes check produces false positives; replace that
logic by using React Router's useMatch('/movies/:movieId') (or
useResolvedPath+matchPath) to set isDetail based on whether the route matches,
remove useParams usage for this purpose, and update any references to the
isDetail variable accordingly so the layout checks router state rather than
pathname string matching.

Comment on lines +4 to +6
export default defineConfig({
plugins: [tailwindcss()],
});
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
# Description: Check if `@vitejs/plugin-react` is in package.json dependencies

echo "=== Checking mission2 package.json for React plugin ==="
cat Week04/jaewoni314/mission2/package.json | grep -E "plugin-react|@vitejs"

echo ""
echo "=== Checking mission1 vite.config for comparison ==="
cat Week04/jaewoni314/mission1/vite.config.ts 2>/dev/null || echo "File not found"

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 533


React 플러그인이 vite.config.ts에서 누락되었습니다.

패키지.json에는 @vitejs/plugin-react가 설치되어 있지만, vite.config.ts에서 가져오거나 등록하지 않았습니다. React 프로젝트에서는 JSX 변환과 Fast Refresh를 위해 반드시 필요합니다.

🔧 수정 제안
 import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
 import tailwindcss from '@tailwindcss/vite';

 export default defineConfig({
-  plugins: [tailwindcss()],
+  plugins: [react(), tailwindcss()],
 });
📝 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
export default defineConfig({
plugins: [tailwindcss()],
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission2/vite.config.ts` around lines 4 - 6, vite.config.ts
is missing the React Vite plugin; import the plugin from "@vitejs/plugin-react"
and register it in the plugins array returned by defineConfig (e.g., add
pluginReact = react() alongside tailwindcss()). Update the top of the file to
import react from "@vitejs/plugin-react" and ensure the plugins array in
defineConfig includes react() so JSX transformation and Fast Refresh are
enabled.


const login = async (data: LoginData) => {
try {
console.log('로그인 시도:', data);
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

로그에 인증정보가 노출됩니다.

Line 17은 로그인 payload(password 포함 가능)를 그대로 출력하므로 제거가 필요합니다. 성공/실패 로그도 민감정보가 섞이지 않게 최소화하는 것이 안전합니다.

🔧 제안 수정안
-      console.log('로그인 시도:', data);
+      // 민감정보(password 등) 로그 출력 금지

...
-        console.log('로그인 성공:', response.message);
+        // 필요 시 개발환경에서만 최소 정보 로깅

...
-      console.error('로그인 실패:', errorMessage);
+      // 에러 메시지는 UI 반환으로 처리, 콘솔 출력은 최소화

Also applies to: 28-28, 46-46

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

In `@Week04/jaewoni314/mission3/src/hooks/useLogin.ts` at line 17, The console.log
in useLogin.ts is printing the full login payload (including sensitive fields
like password); remove or replace these logs in the useLogin hook (e.g., inside
the login handler such as functions handling the request/response where you see
"로그인 시도:" and other similar console logs at lines referenced) so credentials are
not exposed — either remove the logs entirely or log only non-sensitive context
(e.g., "login attempt" / "login success" / "login failure") and, if you must
include identifiers, log a safely masked username or userId rather than the full
payload; update all occurrences mentioned (including the other console.logs
around lines 28 and 46) accordingly.

const navigate = useNavigate();
const { login } = useLogin();

const handleLogin = async (data: LoginData) => await login(data);
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

로그인 실패 시 에러 처리가 누락되었습니다.

handleLogin 함수가 login(data)의 결과를 반환만 하고, 실패 시 사용자에게 피드백을 제공하지 않습니다. 네트워크 오류나 잘못된 자격 증명 등의 상황에서 사용자가 아무런 피드백을 받지 못할 수 있습니다.

🛠️ 에러 처리 추가 제안
- const handleLogin = async (data: LoginData) => await login(data);
+ const handleLogin = async (data: LoginData) => {
+   try {
+     await login(data);
+     // 성공 시 리다이렉트 또는 상태 업데이트
+   } catch (error) {
+     // 에러 메시지 표시 (예: toast, alert 등)
+     console.error('로그인 실패:', error);
+   }
+ };
📝 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
const handleLogin = async (data: LoginData) => await login(data);
const handleLogin = async (data: LoginData) => {
try {
await login(data);
// 성공 시 리다이렉트 또는 상태 업데이트
} catch (error) {
// 에러 메시지 표시 (예: toast, alert 등)
console.error('로그인 실패:', error);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/pages/login/LogInPage.tsx` at line 12,
handleLogin currently just returns await login(data) and lacks error handling;
wrap the await login(data) call in a try/catch inside handleLogin (keeping the
same signature with LoginData), await the login call in the try block and handle
success as before, and in the catch block capture the error, log it for
debugging and provide user feedback (e.g., call your component's error state
setter, show a toast/alert, or set form errors) with a clear user-facing message
for network/credential failures; ensure handleLogin returns or rethrows
appropriately after handling so callers receive the correct outcome.

Comment on lines +13 to +15
console.log('회원가입 데이터:', data);
const response = await signUp(data);
console.log('회원가입 성공:', response);
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

회원가입 요청 본문 로그는 민감정보 노출 위험이 있습니다.

Line 13의 console.log('회원가입 데이터:', data)는 비밀번호까지 노출될 수 있어 제거가 필요합니다. 성공 로그도 최소 정보만 남기도록 축소해 주세요.

로그 최소화 예시
   const handleSignUpComplete = async (data: SignUpRequestBody) => {
     try {
-      console.log('회원가입 데이터:', data);
       const response = await signUp(data);
-      console.log('회원가입 성공:', response);
+      console.log('회원가입 성공');
       navigate('/');
     } catch (error) {
       console.error('회원가입 실패:', error);
     }
   };
📝 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('회원가입 데이터:', data);
const response = await signUp(data);
console.log('회원가입 성공:', response);
const response = await signUp(data);
console.log('회원가입 성공');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/pages/signup/SignUpPage.tsx` around lines 13 -
15, In SignUpPage (the submit handler that calls signUp), remove the sensitive
debug log console.log('회원가입 데이터:', data) so passwords/PII are not printed, and
replace the success log console.log('회원가입 성공:', response) with a minimal,
non-sensitive message — e.g., log only a status or safe identifier from the
response (response.status or response.userId) or a simple "signup successful"
string; update the handler (where signUp(data) is called) to avoid printing the
full request/response objects.

Comment on lines +23 to +30
export const signUpSchema = z.object({
email: emailSchema,
password: passwordSchema,
passwordConfirm: z
.string()
.min(1, { message: '비밀번호를 다시 입력해주세요.' }),
name: z.string().min(1, { message: '닉네임을 입력해주세요.' }),
});
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
rg -nP "signUpSchema|passwordConfirm|refine\\(|superRefine\\(|password\\s*===\\s*passwordConfirm"

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 1733


🏁 Script executed:

cat -n Week04/jaewoni314/mission3/src/pages/signup/components/PasswordStep.tsx | head -60

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 2045


🏁 Script executed:

cat -n Week04/jaewoni314/mission3/src/pages/signup/components/SignUpForm.tsx | head -50

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 1892


🏁 Script executed:

cat -n Week04/jaewoni314/mission3/src/utils/validate.ts

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 1083


Zod 스키마에 비밀번호 일치 검증을 추가해야 합니다.

passwordConfirm이 비어있지 않기만 하면 통과되어, 서로 다른 비밀번호로도 Zod 검증을 통과할 수 있습니다. 현재 클라이언트 컴포넌트에서 검증하고 있지만, 스키마 레벨의 검증이 필요합니다.

수정 예시 (Zod refine)
 export const signUpSchema = z.object({
   email: emailSchema,
   password: passwordSchema,
   passwordConfirm: z
     .string()
     .min(1, { message: '비밀번호를 다시 입력해주세요.' }),
   name: z.string().min(1, { message: '닉네임을 입력해주세요.' }),
-});
+}).refine((data) => data.password === data.passwordConfirm, {
+  message: '비밀번호가 일치하지 않습니다.',
+  path: ['passwordConfirm'],
+});
📝 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
export const signUpSchema = z.object({
email: emailSchema,
password: passwordSchema,
passwordConfirm: z
.string()
.min(1, { message: '비밀번호를 다시 입력해주세요.' }),
name: z.string().min(1, { message: '닉네임을 입력해주세요.' }),
});
export const signUpSchema = z.object({
email: emailSchema,
password: passwordSchema,
passwordConfirm: z
.string()
.min(1, { message: '비밀번호를 다시 입력해주세요.' }),
name: z.string().min(1, { message: '닉네임을 입력해주세요.' }),
}).refine((data) => data.password === data.passwordConfirm, {
message: '비밀번호가 일치하지 않습니다.',
path: ['passwordConfirm'],
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/src/utils/validate.ts` around lines 23 - 30, Add a
cross-field validation to signUpSchema so passwordConfirm must equal password:
update the signUpSchema (which currently uses password, passwordConfirm,
passwordSchema) to refine the entire object and compare data.password and
data.passwordConfirm, returning a clear message when they differ; ensure
passwordConfirm still has its non-empty message but the schema-level refine
enforces equality and attaches the error to passwordConfirm.

Comment on lines +4 to +6
export default defineConfig({
plugins: [tailwindcss()],
});
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
# Description: Check if `@vitejs/plugin-react` is in package.json dependencies

echo "=== Checking mission3 package.json for React plugin ==="
cat Week04/jaewoni314/mission3/package.json | grep -E "plugin-react|@vitejs"

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 255


🏁 Script executed:

#!/bin/bash
# Check the full vite.config.ts file
echo "=== Full vite.config.ts content ==="
cat -n Week04/jaewoni314/mission3/vite.config.ts

# Also check main.tsx to confirm JSX usage
echo -e "\n=== Checking main.tsx for JSX ==="
head -20 Week04/jaewoni314/mission3/src/main.tsx

Repository: HSU-Makeus-Challenge-10th/Web

Length of output: 560


@vitejs/plugin-react 플러그인이 구성에 추가되지 않았습니다.

package.json에 설치되어 있지만 vite.config.ts에 import 되지 않았으며 plugins 배열에 추가되지 않았습니다. React JSX 변환 및 HMR을 위해 필요합니다.

🔧 수정 제안
 import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
 import tailwindcss from '@tailwindcss/vite';

 export default defineConfig({
-  plugins: [tailwindcss()],
+  plugins: [react(), tailwindcss()],
 });
📝 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
export default defineConfig({
plugins: [tailwindcss()],
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/jaewoni314/mission3/vite.config.ts` around lines 4 - 6, vite.config.ts
is missing the React plugin: import the React plugin from '@vitejs/plugin-react'
and add it to the plugins array used in defineConfig so JSX/HMR work correctly;
locate the defineConfig call and the plugins array (currently containing
tailwindcss()) and add the imported react plugin alongside tailwindcss().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant