Skip to content

Create Week04 Mission1 ,2, 3 #35

Open
SJ01-max wants to merge 1 commit intomainfrom
SJ01-max/week04
Open

Create Week04 Mission1 ,2, 3 #35
SJ01-max wants to merge 1 commit intomainfrom
SJ01-max/week04

Conversation

@SJ01-max
Copy link
Copy Markdown
Contributor

@SJ01-max SJ01-max commented Apr 7, 2026

📝 미션 번호

4주차 Misson 1

📋 구현 사항

  • useCustomFetch 커스텀 훅 구현 — data, isLoading, error 상태 반환
  • URL / params 변경 시 자동 재요청, AbortController로 언마운트 시 요청 취소
  • 영화 목록 페이지 — 카테고리 탭(인기/현재상영/최고평점/개봉예정) + 페이지네이션
  • 영화 상세 페이지 — 배경 이미지, 평점, 줄거리, 제작비/수익, 제작사 표시
  • 로딩 스피너 및 에러 메시지 UI 구현
  • Tailwind CSS 다크 테마 반응형 디자인 적용

📎 스크린샷

image

📝 미션 번호

4주차 Misson 2

📋 구현 사항

  • useForm 커스텀 훅 구현 — values, errors, touched, isValid 반환
  • 로그인 페이지 UI 구현 (구글 로그인, 이메일/비밀번호 폼)
  • 이메일 형식 / 비밀번호 8자 미만 유효성 검사 및 에러 메시지 표시
  • 유효성 통과 시에만 로그인 버튼 활성화
  • POST /v1/auth/signin 연동, 토큰 localStorage 저장
  • 구글 OAuth 콜백 라우트 처리

📎 스크린샷

image

📝 미션 번호

4주차 Misson 3

📋 구현 사항

  • Zod 스키마 정의 및 z.infer<>로 TypeScript 타입 자동 추출
  • react-hook-form + zodResolver 연동으로 폼 유효성 검사 처리
  • 3단계 회원가입 플로우 구현 (이메일 → 비밀번호 → 닉네임)
  • 비밀번호 가시성 토글, refine으로 비밀번호 일치 검사
  • useLocalStorage 제네릭 커스텀 훅 구현 — JSON 직렬화 자동 처리
  • POST /v1/auth/signup 연동, 성공 시 로그인 페이지 이동

📎 스크린샷

image

✅ 체크리스트

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

🤔 질문 사항

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 영화 검색 및 상세 조회 기능이 있는 영화 애플리케이션 추가
    • 이메일/비밀번호 기반 회원가입 및 로그인 시스템 구현
    • Google OAuth를 통한 소셜 로그인 지원
    • 회원가입 단계별 폼 검증 및 사용자 경험 개선
    • 반응형 UI 및 다크 테마 디자인 적용
  • Documentation

    • 프로젝트 설정 및 사용 가이드 문서 추가

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Week04/SJ01-max 디렉토리에 세 개의 React + TypeScript + Vite 프로젝트를 추가합니다. Mission1은 TMDB API를 통한 영화 검색 및 상세 조회 기능을 구현하고, Mission2는 이메일/비밀번호 및 Google OAuth 기반 로그인을 제공합니다. Mission3는 Mission2를 확장하여 3단계 회원가입 폼 입력을 추가합니다.

Changes

Cohort / File(s) Summary
Mission1 기본 설정
Week04/SJ01-max/Mission1/.gitignore, package.json, tsconfig*.json, vite.config.ts, index.html
Vite 및 TypeScript 프로젝트 기본 설정 파일 추가. 개발 스크립트, 의존성 및 빌드 구성 정의.
Mission1 문서 및 설정
Week04/SJ01-max/Mission1/README.md, eslint.config.js
React + Vite 템플릿 설명서 및 ESLint 플랫 설정 파일 추가. TypeScript 및 React 후킹 규칙 포함.
Mission1 TMDB 영화 앱 (스타일 및 엔트리)
Week04/SJ01-max/Mission1/src/index.css, src/main.tsx, src/App.tsx, src/App.css
Tailwind CSS 임포트, 루트 컴포넌트 초기화, 라우팅 설정 및 레이아웃 스타일 정의.
Mission1 타입 정의
Week04/SJ01-max/Mission1/src/types/movie.ts
TMDB API 응답 모델링을 위한 Movie, MovieDetail, MovieListResponse 인터페이스 정의.
Mission1 API 및 훅
Week04/SJ01-max/Mission1/src/lib/tmdbClient.ts, src/hooks/useCustomFetch.ts
TMDB API 클라이언트 구성 및 제너릭 데이터 페칭 훅(로딩/에러 상태 관리 포함) 구현.
Mission1 공유 컴포넌트
Week04/SJ01-max/Mission1/src/components/LoadingSpinner.tsx, ErrorMessage.tsx, MovieCard.tsx
로딩 스피너, 에러 메시지 표시, 영화 카드(포스터, 점수, 네비게이션 링크)를 렌더링하는 UI 컴포넌트.
Mission1 페이지
Week04/SJ01-max/Mission1/src/pages/MovieListPage.tsx, MovieDetailPage.tsx
카테고리별 영화 목록(페이지네이션 포함), 영화 상세 정보(배경, 장르, 제작사 포함) 페이지 구현.
Mission2 기본 설정
Week04/SJ01-max/Mission2/.gitignore, package.json, tsconfig*.json, vite.config.ts, index.html, UMC-8th-BE
Vite 프로젝트 기본 설정 및 백엔드 서브모듈 참조 추가.
Mission2 인증 및 API
Week04/SJ01-max/Mission2/src/lib/apiClient.ts, src/lib/authApi.ts
로컬 스토리지 기반 토큰 관리, signIn 함수, 요청 인터셉터를 통한 인증 헤더 자동 첨부.
Mission2 폼 및 훅
Week04/SJ01-max/Mission2/src/hooks/useForm.ts
필드별 검증, 터치 추적 및 폼 유효성 상태 관리 제너릭 훅.
Mission2 네비게이션 및 루팅
Week04/SJ01-max/Mission2/src/App.tsx, src/main.tsx, src/components/Navbar.tsx, src/index.css
라우팅 구조(홈/로그인/Google 콜백), 네비게이션 바 및 Tailwind 스타일 임포트 정의.
Mission2 인증 페이지
Week04/SJ01-max/Mission2/src/pages/HomePage.tsx, LoginPage.tsx, GoogleCallbackPage.tsx
랜딩 페이지, 이메일/비밀번호 로그인 폼(검증 및 에러 처리 포함), Google OAuth 토큰 저장 로직.
Mission3 기본 설정
Week04/SJ01-max/Mission3/.gitignore, package.json, tsconfig*.json, vite.config.ts, index.html
React Hook Form, Zod 검증, 로컬 스토리지 훅을 포함한 Vite 프로젝트 설정.
Mission3 검증 및 API
Week04/SJ01-max/Mission3/src/lib/schemas.ts, src/lib/authApi.ts, src/lib/apiClient.ts
Zod 기반 이메일/비밀번호/닉네임 검증 스키마, signUp/signIn 함수, 토큰 인터셉터 구성.
Mission3 훅
Week04/SJ01-max/Mission3/src/hooks/useForm.ts, useLocalStorage.ts
폼 상태 관리 및 로컬 스토리지 동기화 훅(읽기/쓰기/삭제 기능 포함).
Mission3 네비게이션 및 레이아웃
Week04/SJ01-max/Mission3/src/App.tsx, src/main.tsx, src/components/Navbar.tsx, src/index.css
라우팅 구조 및 네비게이션 바 컴포넌트 정의.
Mission3 다단계 회원가입
Week04/SJ01-max/Mission3/src/components/signup/Step1Email.tsx, Step2Password.tsx, Step3Nickname.tsx
이메일(Google OAuth 옵션), 비밀번호(토글 표시), 닉네임(API 호출 포함) 입력 단계 렌더링.
Mission3 인증 페이지
Week04/SJ01-max/Mission3/src/pages/HomePage.tsx, LoginPage.tsx, SignupPage.tsx, GoogleCallbackPage.tsx
랜딩, 로그인(Google OAuth 지원), 3단계 회원가입 상태 관리, Google 콜백 토큰 저장.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Component as MovieListPage
    participant Hook as useCustomFetch
    participant Client as tmdbClient
    participant API as TMDB API

    User->>Component: 영화 목록 페이지 방문
    Component->>Hook: useCustomFetch<MovieListResponse>(url, params)
    Hook->>Hook: 초기 상태 설정 (loading=true)
    Hook->>Client: get<T>(url, {params, signal})
    Client->>API: GET /3/movie/popular?page=1&language=ko-KR
    API-->>Client: JSON 응답 (results[], total_pages)
    Client-->>Hook: response.data
    Hook->>Hook: 상태 업데이트 (data=response, loading=false)
    Hook-->>Component: {data, isLoading=false, error=null}
    Component->>Component: MovieCard[] 렌더링
    Component-->>User: 영화 그리드 표시
Loading
sequenceDiagram
    participant User as 사용자
    participant Form as LoginPage
    participant Validation as zodResolver
    participant API as authApi.signIn
    participant Backend as 백엔드
    participant Storage as localStorage

    User->>Form: 이메일/비밀번호 입력 후 로그인 클릭
    Form->>Validation: validate (email, password)
    Validation-->>Form: 검증 결과
    Form->>API: signIn(email, password)
    API->>Backend: POST /v1/auth/signin
    Backend-->>API: {data: {accessToken, refreshToken}}
    API-->>Form: SignInResponse
    Form->>Storage: localStorage.setItem('accessToken', token)
    Form->>Storage: localStorage.setItem('refreshToken', token)
    Form->>Form: navigate('/')
    Form-->>User: 홈페이지로 리디렉트
Loading
sequenceDiagram
    participant User as 사용자
    participant Page as SignupPage
    participant Step1 as Step1Email
    participant Step2 as Step2Password
    participant Step3 as Step3Nickname
    participant API as authApi.signUp
    participant Backend as 백엔드

    User->>Page: 회원가입 페이지 접속
    Page->>Step1: onNext, onBack 콜백 전달
    User->>Step1: 이메일 입력 및 '다음' 클릭
    Step1->>Step1: emailSchema 검증
    Step1->>Page: onNext(email)
    Page->>Step2: email, onNext, onBack 전달
    User->>Step2: 비밀번호 입력 및 '다음' 클릭
    Step2->>Step2: passwordSchema 검증
    Step2->>Page: onNext(password)
    Page->>Step3: email, password, onBack 전달
    User->>Step3: 닉네임 입력 및 제출
    Step3->>Step3: nicknameSchema 검증
    Step3->>API: signUp({name, email, password})
    API->>Backend: POST /v1/auth/signup
    Backend-->>API: 성공 응답
    Step3->>Step3: navigate('/login')
    Step3-->>User: 로그인 페이지로 리디렉트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Create Week03 Mission1~3 #31: TMDB 클라이언트, 영화 타입 정의, 영화 목록/상세 페이지의 동일한 구현이 겹침 (tmdb client, types, pages 중복).
  • Create Week03 Mission1,2,3 #28: Week04 영화 앱이 Week03 PR의 영화 앱 추가를 반영함—동일한 컴포넌트 (LoadingSpinner, MovieCard), 페이지 (MovieList/MovieDetail), 페칭 훅 (useCustomFetch vs useFetch) 및 types/movie.ts 포함.

Suggested reviewers

  • wantkdd

Poem

🐰 이번 주간의 변화

세 개의 미션이 나타났어요,
영화는 TMDB에서 춤을 추고,
로그인과 회원가입이 손을 잡으며,
Zod와 React Hook Form이 함께 노래하고,
토큰은 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 The PR title 'Create Week04 Mission1 ,2, 3' accurately describes the changeset but has a formatting inconsistency with inconsistent spacing and commas.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with all required sections (미션 번호, 구현 사항, 스크린샷, 체크리스트) completed for all three missions.

✏️ 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 SJ01-max/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.

@SJ01-max SJ01-max requested a review from wantkdd April 7, 2026 06:44
@SJ01-max SJ01-max self-assigned this Apr 7, 2026
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: 19

🧹 Nitpick comments (14)
Week04/SJ01-max/Mission2/src/hooks/useForm.ts (2)

42-45: isValid 계산에서 검증 함수 중복 호출을 줄이는 것을 권장합니다.

현재 key 개수만큼 validate(values)를 반복 호출하고 있어, 검증 결과를 한 번 계산해 재사용하는 편이 효율적입니다.

중복 검증 호출 제거 예시
-  const isValid = Object.keys(initialValues).every((key) => {
-    const k = key as keyof T;
-    return values[k].length > 0 && !validate(values)[k];
-  });
+  const validationResult = validate(values);
+  const isValid = Object.keys(initialValues).every((key) => {
+    const k = key as keyof T;
+    return values[k].length > 0 && !validationResult[k];
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/src/hooks/useForm.ts` around lines 42 - 45, The
isValid computation calls validate(values) repeatedly for each key; compute the
validation once and reuse it: inside useForm.ts replace the inline repeated
calls in the isValid block by calling validate(values) a single time (e.g.,
const errors = validate(values)) and then use errors[k] when iterating
Object.keys(initialValues) (keep casting key as keyof T and checking
values[k].length > 0 && !errors[k]) so validation runs once and isValid uses the
cached result.

10-10: 제네릭 타입 제약이 중복되어 있습니다.

동일 의미 제약을 두 번 걸고 있어 선언을 단순화하는 편이 유지보수에 유리합니다.

타입 제약 단순화 예시
-function useForm<T extends Record<string, string> & { [key: string]: string }>({ initialValues, validate }: UseFormOptions<T>) {
+function useForm<T extends Record<string, string>>({ initialValues, validate }: UseFormOptions<T>) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/src/hooks/useForm.ts` at line 10, The generic
constraint on useForm is redundant—remove the duplicated equivalent constraint
and simplify it to a single constraint (e.g., change the generic declaration on
function useForm to use one constraint such as T extends Record<string, string>)
while keeping the existing UseFormOptions<T> usage; update the useForm<T ...>
signature to the simplified generic so the function, its parameters, and callers
compile the same but with a clearer single type constraint.
Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts (1)

27-30: removeValue도 예외 처리를 맞춰두는 편이 안전합니다.

같은 훅 내 다른 localStorage 접근과 동일하게 try/catch를 적용하면 런타임 안정성이 높아집니다.

🔧 제안 수정
   const removeValue = () => {
-    setStoredValue(initialValue);
-    localStorage.removeItem(key);
+    try {
+      setStoredValue(initialValue);
+      localStorage.removeItem(key);
+    } catch (error) {
+      console.error(`useLocalStorage removeValue error [${key}]:`, error);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts` around lines 27 - 30,
The removeValue function currently calls setStoredValue(initialValue) and
localStorage.removeItem(key) without error handling; wrap its body in a
try/catch (matching the pattern used elsewhere in this hook) so any
localStorage.removeItem or state update errors are caught and handled (e.g., log
the error or silently ignore) and ensure removeValue still resets stored state
on success; locate the removeValue function in useLocalStorage.ts to apply this
change.
Week04/SJ01-max/Mission3/src/hooks/useForm.ts (3)

10-10: 타입 제약 조건이 중복됩니다.

T extends Record<string, string> & { [key: string]: string }에서 두 타입은 동일하므로 하나만 사용해도 됩니다.

♻️ 간소화 제안
-function useForm<T extends Record<string, string> & { [key: string]: string }>({
+function useForm<T extends Record<string, string>>({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/hooks/useForm.ts` at line 10, The generic
constraint on useForm is redundant: change the signature of function useForm<T
extends Record<string, string>>({...}) by removing the duplicate & { [key:
string]: string } part so only a single indexable string-record constraint
remains; update any references to the useForm generic if necessary to match the
simplified T constraint.

39-42: isValid 계산이 매 렌더링마다 validate를 호출하여 비효율적입니다.

validate(values)가 렌더링마다 호출되어 불필요한 연산이 발생합니다. 이미 errors 상태에 유효성 검사 결과가 있으므로 이를 활용하거나 useMemo로 메모이제이션하세요.

♻️ errors 상태 활용 제안
-  const isValid = Object.keys(initialValues).every((key) => {
-    const k = key as keyof T;
-    return values[k].length > 0 && !validate(values)[k];
-  });
+  const isValid = Object.keys(initialValues).every((key) => {
+    const k = key as keyof T;
+    return values[k].length > 0 && !errors[k];
+  });

또는 useMemo 사용:

const isValid = useMemo(() => {
  const validationErrors = validate(values);
  return Object.keys(initialValues).every((key) => {
    const k = key as keyof T;
    return values[k].length > 0 && !validationErrors[k];
  });
}, [values, validate, initialValues]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/hooks/useForm.ts` around lines 39 - 42, The
isValid computation currently calls validate(values) on every render; change it
to use the existing errors state or memoize the validation result to avoid
repeated work: refer to the isValid constant and the values, initialValues,
validate and errors symbols — either compute isValid from errors (e.g., check
Object.keys(initialValues).every(k => values[k].length>0 && !errors[k])) or wrap
the validation call in useMemo (depend on [values, validate, initialValues]) and
use that memoized validationErrors when calculating isValid.

18-28: handleChange에서 values 의존성으로 인해 클로저 문제가 발생할 수 있습니다.

useCallback의 의존성에 values가 포함되어 있어, 값이 변경될 때마다 새로운 함수가 생성됩니다. 함수형 업데이트를 사용하면 values 의존성을 제거할 수 있습니다.

♻️ 함수형 업데이트 사용 제안
 const handleChange = useCallback(
   (e: ChangeEvent<HTMLInputElement>) => {
     const { name, value } = e.target;
-    const newValues = { ...values, [name]: value } as T;
-    setValues(newValues);
-    if (touched[name as keyof T]) {
-      setErrors(validate(newValues));
-    }
+    setValues((prev) => {
+      const newValues = { ...prev, [name]: value } as T;
+      if (touched[name as keyof T]) {
+        setErrors(validate(newValues));
+      }
+      return newValues;
+    });
   },
-  [values, touched, validate],
+  [touched, validate],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/hooks/useForm.ts` around lines 18 - 28,
handleChange closes over values causing unnecessary re-creations; switch to a
functional state update: inside handleChange compute newValues from prev (e.g.,
setValues(prev => { const newValues = { ...prev, [name]: value }; if
(touched[name as keyof T]) setErrors(validate(newValues)); return newValues;
})), remove values from the useCallback deps array, and keep touched and
validate in deps so handleChange remains stable and uses the latest state when
updating values and errors (refer to handleChange, setValues, setErrors,
validate, touched).
Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx (2)

11-21: 유효성 검사 로직이 빈 필드를 처리하지 않습니다.

values.emailvalues.password가 truthy일 때만 검증하므로, 빈 필드는 에러를 표시하지 않습니다. useFormisValid가 비어있지 않은 값을 확인하므로 기능적으로는 동작하지만, 사용자가 blur 시 "필수 입력" 에러를 볼 수 없습니다.

♻️ 빈 필드 에러 메시지 추가 제안
 function validate(values: LoginValues): Partial<Record<keyof LoginValues, string>> {
   const errors: Partial<Record<keyof LoginValues, string>> = {};
   const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-  if (values.email && !emailRegex.test(values.email)) {
+  if (!values.email) {
+    errors.email = '이메일을 입력해주세요.';
+  } else if (!emailRegex.test(values.email)) {
     errors.email = '올바른 이메일 형식을 입력해주세요.';
   }
-  if (values.password && values.password.length < 8) {
+  if (!values.password) {
+    errors.password = '비밀번호를 입력해주세요.';
+  } else if (values.password.length < 8) {
     errors.password = '비밀번호는 8자 이상이어야 합니다.';
   }
   return errors;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx` around lines 11 - 21, The
validate function currently only checks email/password when they are truthy;
update validate (function name: validate, params: values: LoginValues) to also
set errors.email and errors.password when values.email or values.password are
empty or missing (e.g., === '' or falsy), using appropriate messages like "필수 입력
항목입니다." and keep the existing format checks (emailRegex.test and password
length) so both required and format/length validations run and populate the
errors object returned.

52-54: 하드코딩된 Google 로그인 URL을 환경 변수로 분리하세요.

Mission3과 동일하게 localhost:8000이 하드코딩되어 있습니다. 환경 변수를 사용하여 설정하세요.

♻️ 환경 변수 사용 제안
 const handleGoogleLogin = () => {
-  window.location.href = 'http://localhost:8000/v1/auth/google/login';
+  window.location.href = `${import.meta.env.VITE_API_URL}/v1/auth/google/login`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx` around lines 52 - 54,
Replace the hardcoded Google login URL in handleGoogleLogin with a URL built
from an environment variable (e.g., API base like REACT_APP_API_BASE or
NEXT_PUBLIC_API_BASE); update the handleGoogleLogin function to read the env var
(fall back to a sensible default if needed) and call window.location.href =
`${API_BASE}/v1/auth/google/login`; also add/update the .env entry for the
chosen variable and document its expected value.
Week04/SJ01-max/Mission3/src/lib/authApi.ts (1)

22-25: signUp 함수의 반환 타입이 unknown으로 지정되어 타입 안전성이 부족합니다.

API가 회원가입 후 유용한 데이터(예: 생성된 사용자 정보)를 반환하는 경우, 이를 활용할 수 없습니다. 실제 응답 구조에 맞는 타입을 정의하거나, 반환값을 사용하지 않는다면 void를 명시하는 것이 좋습니다.

♻️ 명시적 반환 타입 제안
+export interface SignUpResponse {
+  id: number;
+  name: string;
+  email: string;
+}

-export async function signUp(payload: SignUpPayload) {
-  const res = await apiClient.post<ApiResponse<unknown>>('/auth/signup', payload);
+export async function signUp(payload: SignUpPayload): Promise<SignUpResponse> {
+  const res = await apiClient.post<ApiResponse<SignUpResponse>>('/auth/signup', payload);
   return res.data.data;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/lib/authApi.ts` around lines 22 - 25, The signUp
function currently returns ApiResponse<unknown>, which loses type safety; update
the ApiResponse generic to the proper response type (e.g., ApiResponse<User> or
ApiResponse<SignUpResponse>) and change the signUp signature to return that
concrete type (or, if the endpoint returns nothing useful, explicitly return
Promise<void>), ensuring you update the apiClient.post call generic and the
return type of signUp to match (refer to signUp and
apiClient.post<ApiResponse<...>> in this file).
Week04/SJ01-max/Mission2/src/lib/authApi.ts (1)

21-24: 토큰 저장 방식이 Mission3과 일관되지 않습니다.

Mission3의 apiClient.tsauth_tokens 키에서 JSON 객체로 토큰을 읽지만, 여기서는 accessTokenrefreshToken을 별도의 키로 저장합니다. Mission2 내부적으로는 동작할 수 있지만, 코드 재사용 시 불일치가 발생할 수 있습니다.

♻️ 일관된 토큰 저장 방식 제안
 export function saveTokens(accessToken: string, refreshToken: string) {
-  localStorage.setItem('accessToken', accessToken);
-  localStorage.setItem('refreshToken', refreshToken);
+  localStorage.setItem('auth_tokens', JSON.stringify({ accessToken, refreshToken }));
 }

 export function clearTokens() {
-  localStorage.removeItem('accessToken');
-  localStorage.removeItem('refreshToken');
+  localStorage.removeItem('auth_tokens');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/src/lib/authApi.ts` around lines 21 - 24, saveTokens
currently stores accessToken and refreshToken under separate keys which is
inconsistent with Mission3's apiClient.ts that expects a single 'auth_tokens'
JSON object; update the saveTokens function to serialize both tokens into one
object and save it under the 'auth_tokens' key (JSON.stringify({ accessToken,
refreshToken })) so reads by apiClient.ts (which parses 'auth_tokens') will work
uniformly across missions.
Week04/SJ01-max/Mission3/src/pages/LoginPage.tsx (1)

47-49: 하드코딩된 Google 로그인 URL을 환경 변수로 분리하세요.

localhost:8000이 하드코딩되어 있어 프로덕션 환경에서 동작하지 않습니다. 환경 변수를 사용하여 설정하는 것이 좋습니다.

♻️ 환경 변수 사용 제안
 const handleGoogleLogin = () => {
-  window.location.href = 'http://localhost:8000/v1/auth/google/login';
+  window.location.href = `${import.meta.env.VITE_API_URL}/v1/auth/google/login`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/pages/LoginPage.tsx` around lines 47 - 49, The
handleGoogleLogin function hardcodes
'http://localhost:8000/v1/auth/google/login'; change it to build the URL from an
environment variable (e.g. REACT_APP_API_BASE_URL or VITE_API_BASE_URL depending
on your build) so the app works in prod; update handleGoogleLogin to read the
base URL from process.env.REACT_APP_API_BASE_URL or
import.meta.env.VITE_API_BASE_URL with a safe fallback, append
'/v1/auth/google/login', and replace the hardcoded string; also add/update your
.env/.env.production entries for that variable.
Week04/SJ01-max/Mission1/src/components/LoadingSpinner.tsx (1)

3-5: 로딩 상태 접근성 속성 추가를 권장합니다.

현재도 동작은 문제없지만, 보조기기 사용자에게 로딩 상태를 더 안정적으로 전달하려면 role="status"aria-live를 붙이는 편이 좋습니다.

접근성 개선 예시
 export default function LoadingSpinner() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-64 gap-4">
-      <div className="w-12 h-12 rounded-full border-4 border-gray-700 border-t-rose-500 animate-spin" />
+    <div
+      role="status"
+      aria-live="polite"
+      className="flex flex-col items-center justify-center min-h-64 gap-4"
+    >
+      <div
+        aria-hidden="true"
+        className="w-12 h-12 rounded-full border-4 border-gray-700 border-t-rose-500 animate-spin"
+      />
       <p className="text-gray-400 text-sm">불러오는 중...</p>
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission1/src/components/LoadingSpinner.tsx` around lines 3 -
5, Add accessibility attributes to the LoadingSpinner component: on the outer
container (the div with className "flex flex-col items-center justify-center
min-h-64 gap-4") add role="status" and aria-live="polite" (and optionally
aria-busy="true") so assistive technologies reliably announce the loading state;
ensure the visible message element (the p with text "불러오는 중...") remains for
screen reader output.
Week04/SJ01-max/Mission1/src/lib/tmdbClient.ts (1)

8-10: 환경 변수 누락 시 "Bearer undefined" 문제 발생 가능

VITE_TMDB_KEY 환경 변수가 설정되지 않은 경우, Authorization 헤더가 Bearer undefined가 되어 API 요청이 실패합니다. 개발 중 디버깅을 용이하게 하기 위해 환경 변수 검증을 추가하는 것을 권장합니다.

🛡️ 환경 변수 검증 추가 제안
 import axios from 'axios';
 
+const TMDB_KEY = import.meta.env.VITE_TMDB_KEY;
+
+if (!TMDB_KEY) {
+  console.warn('VITE_TMDB_KEY 환경 변수가 설정되지 않았습니다.');
+}
+
 const tmdbClient = axios.create({
   baseURL: 'https://api.themoviedb.org/3',
   params: {
     language: 'ko-KR',
   },
   headers: {
-    Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
+    Authorization: `Bearer ${TMDB_KEY}`,
   },
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission1/src/lib/tmdbClient.ts` around lines 8 - 10, The
Authorization header construction uses import.meta.env.VITE_TMDB_KEY directly
which can yield "Bearer undefined" if the env var is missing; add a validation
step where the TMDB key is read (e.g., const tmdbKey =
import.meta.env.VITE_TMDB_KEY) and if falsy either throw an informative error or
log and abort startup, and only set headers.Authorization to `Bearer ${tmdbKey}`
when tmdbKey is present; update the code around the header creation (the
Authorization header line) to reference this validated tmdbKey variable so no
requests are sent with "Bearer undefined".
Week04/SJ01-max/Mission1/src/pages/MovieListPage.tsx (1)

45-55: 카테고리 버튼에 선택 상태를 노출해 주세요.

지금은 활성 카테고리가 색상으로만 구분돼서 보조기기에서는 현재 선택된 탭을 알기 어렵습니다. 최소한 aria-pressed 정도는 같이 내려주는 편이 좋겠습니다.

제안 코드
             <button
               key={cat.value}
               onClick={() => handleCategoryChange(cat.value)}
+              aria-pressed={category === cat.value}
               className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
                 category === cat.value
                   ? 'bg-rose-600 text-white shadow-lg shadow-rose-900/50'
                   : 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white'
               }`}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission1/src/pages/MovieListPage.tsx` around lines 45 - 55,
The category buttons only use color to show selection which is inaccessible;
update the button element in MovieListPage (the JSX that renders each category
button using cat.value, cat.label and handleCategoryChange) to expose selection
state to assistive tech by adding an aria-pressed attribute bound to the boolean
expression (category === cat.value) (and optionally aria-current="true" for the
active one), ensuring the attribute updates when handleCategoryChange runs so
screen readers can announce the active category.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Week04/SJ01-max/Mission1/src/App.css`:
- Around line 1-184: App.css contains unused Vite template boilerplate selectors
(.counter, .hero, .framework, .vite, `#next-steps`, `#docs`, .ticks, etc.) and
should be removed; delete the App.css file and ensure there are no imports
referencing it (check main.tsx and any other entry files for an import of
App.css and remove that import), and run a quick global search for the listed
selectors to confirm nothing else depends on them.

In `@Week04/SJ01-max/Mission1/src/hooks/useCustomFetch.ts`:
- Around line 18-50: The current finally block in useEffect always calls
setIsLoading(false) even when the request was aborted, causing a canceled
request to clear loading for a subsequent request; modify fetchData/useEffect to
avoid clearing loading for aborted requests by tracking request liveness: create
a local let isCurrent = true (or check controller.signal.aborted) at the top of
useEffect, call controller.abort() and set isCurrent = false in the cleanup, and
then in the finally of fetchData only call setIsLoading(false) if isCurrent &&
!controller.signal.aborted; reference symbols: useEffect, fetchData, controller,
controller.abort, setIsLoading.

In `@Week04/SJ01-max/Mission1/src/pages/MovieDetailPage.tsx`:
- Around line 69-79: The poster/placeholder always has the '-mt-24' offset
causing it to overlap the header when no backdrop exists; update the
MovieDetailPage rendering for the poster (<img> and the placeholder <div>) to
apply the '-mt-24' class only when movie.backdrop_path is truthy (e.g., compute
a conditional className or template string inside the component), leaving it out
when movie.backdrop_path is falsy so the poster doesn't shift upward; locate the
poster markup inside the MovieDetailPage component where movie.poster_path and
the placeholder are rendered and adjust the className to conditionally include
'-mt-24'.

In `@Week04/SJ01-max/Mission2/index.html`:
- Line 2: The HTML root element currently has lang="en" which mismatches the
Korean UI; update the html element's lang attribute (the <html lang="en">
declaration) to lang="ko" so the document language correctly reflects Korean for
accessibility and language detection.

In `@Week04/SJ01-max/Mission2/package.json`:
- Line 2: 패키지 메타정보의 "name" 값이 프로젝트 디렉터리와 일치하지 않습니다; package.json의 "name"
필드("name": "mission1")를 프로젝트 폴더명에 맞게 예: "mission2"로 변경하여 패키지 식별/배포 혼동을 제거하세요.

In `@Week04/SJ01-max/Mission2/src/App.tsx`:
- Around line 12-15: Navbar contains a "/signup" link but App.tsx's Routes
(Routes, Route components that currently include HomePage, LoginPage,
GoogleCallbackPage) lack a matching "/signup" route; add a Route for "/signup" —
either mount the real SignupPage component (SignupPage) or add a redirect Route
that returns <Navigate to="/login" replace /> to send users to LoginPage, and
ensure Navigate is imported so the route resolves when the Navbar "회원가입" button
is clicked.

In `@Week04/SJ01-max/Mission2/src/components/Navbar.tsx`:
- Around line 16-18: The Navbar's Link pointing to "/signup" (in Navbar.tsx
where Link to="/signup") targets a route that doesn't exist in the Mission2
router defined in App.tsx, causing broken navigation; fix by either adding a
matching route for "/signup" in the Mission2 route list inside App.tsx (create a
Signup component and register it), or change/remove the Link in Navbar.tsx
(replace with an existing valid route path or conditionally hide the link when
the Signup route is not registered) so the Link always points to a valid route.

In `@Week04/SJ01-max/Mission2/src/lib/apiClient.ts`:
- Line 4: The baseURL in apiClient.ts is hardcoded to 'http://localhost:8000/v1'
(symbol: baseURL) and the same pattern is repeated in LoginPage.tsx (around the
login fetch at line ~53); replace these hardcoded URLs with an
environment-driven configuration: read a single env var (e.g., API_BASE_URL or
VITE_API_BASE_URL / REACT_APP_API_BASE_URL depending on your build) and fall
back to a sensible default such as `${window.location.origin}/v1` if the env var
is absent; update apiClient.ts to construct baseURL from that env var and change
LoginPage.tsx to consume the same variable (or import the client) so all HTTP
entrypoints use the centralized env-configured base URL and add instructions to
add the variable to the project .env for each environment.

In `@Week04/SJ01-max/Mission2/src/pages/GoogleCallbackPage.tsx`:
- Around line 15-17: The OAuth callback currently navigates using navigate('/')
and navigate('/login') which leaves the token-bearing callback URL in browser
history; update the two navigate calls in GoogleCallbackPage (the navigate(...)
invocations) to perform replacement navigation by passing the option { replace:
true } so the redirect replaces the current history entry and prevents exposing
accessToken/refreshToken via the back button.

In `@Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx`:
- Around line 61-67: The back-navigation button inside the LoginPage form is
missing an explicit type and will default to type="submit", causing accidental
form submission; update the button element with type="button" (the button that
calls onClick={() => navigate(-1)}) so it doesn't trigger form submit, i.e., add
the type attribute to the button in the LoginPage component.

In `@Week04/SJ01-max/Mission2/UMC-8th-BE`:
- Line 1: The UMC-8th-BE entry is present as a git submodule marker (mode
160000) but the repository lacks a .gitmodules file and the submodule directory
is empty; decide whether the backend is required and either (A) add and
configure the .gitmodules entry for UMC-8th-BE and initialize/commit the
populated submodule (ensure .gitmodules references the correct URL and path and
run git submodule add/init/update so the directory contains the backend code),
or (B) remove the submodule entry for UMC-8th-BE from the index (git rm --cached
the submodule path, remove any 160000 entry) and update the README to include a
link and run instructions for the backend repository; update the PR description
to reflect the chosen option and ensure no stale 160000 mode entries remain in
the git index.

In `@Week04/SJ01-max/Mission3/index.html`:
- Line 2: The document's root <html> element currently uses lang="en" which
mismatches the Korean UI; update the <html lang="en"> attribute to lang="ko" so
the root <html> tag reflects the Korean language for accessibility and screen
readers (ensure any other language-related attributes/meta tags are consistent
as well).

In `@Week04/SJ01-max/Mission3/package.json`:
- Line 2: The package.json "name" field currently reads "mission1" which
mismatches the project directory; update the "name" property in package.json to
match this project (e.g., "mission3" or a directory-consistent slug like
"sj01-max-mission3"), ensuring it is lowercase, URL-safe (no spaces), and
consistent with the Mission3 directory so workspace/deployment tooling
recognizes the package correctly.

In `@Week04/SJ01-max/Mission3/src/components/signup/Step1Email.tsx`:
- Around line 24-26: The handleGoogleSignup function currently hardcodes the
OAuth URL to 'http://localhost:8000', which breaks in non-local deployments;
update handleGoogleSignup in Step1Email.tsx to build the redirect URL from an
environment variable (e.g., use process.env.REACT_APP_API_URL or
process.env.NEXT_PUBLIC_API_URL depending on the app) and append
'/v1/auth/google/login' so the host/port come from config rather than localhost,
and ensure the env var is documented/validated (fallback or error if missing).

In `@Week04/SJ01-max/Mission3/src/components/signup/Step2Password.tsx`:
- Around line 79-85: The password visibility toggle button lacks accessible
labeling; update the button (the element wrapping EyeIcon, which uses
showPassword and setShowPassword) to include an aria-label that reflects
action/state (e.g., "Show password" when showPassword is false and "Hide
password" when true) and add aria-pressed={showPassword} so screen readers know
the toggle state; apply the same changes to the other toggle button instance as
well.

In `@Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts`:
- Around line 13-21: setValue currently computes valueToStore using the stale
closed-over storedValue; change it to use the functional updater of
setStoredValue to get the latest prev state. In other words, inside setValue
call setStoredValue(prev => { const valueToStore = value instanceof Function ?
value(prev) : value; if (valueToStore == null) localStorage.removeItem(key);
else localStorage.setItem(key, JSON.stringify(valueToStore)); return
valueToStore; }) so updates use the freshest state; keep references to setValue,
storedValue, setStoredValue, localStorage and key.

In `@Week04/SJ01-max/Mission3/src/lib/apiClient.ts`:
- Around line 13-16: 현재 stored에서 파싱한 AuthTokens를 검증하지 않고
config.headers.Authorization에 바로 설정해 Bearer undefined가 전송될 수 있습니다; 수정 방법은
JSON.parse(stored)로 얻은 tokens와 tokens.accessToken이 존재하는지, 문자열인지, 비어있지 않은지 확인한
뒤에만 config.headers.Authorization = `Bearer ${tokens.accessToken}`을 설정하고 그렇지 않으면
Authorization 헤더를 제거하거나 설정하지 않도록 변경하세요 (참고 식별자: stored, AuthTokens,
tokens.accessToken, config.headers.Authorization).
- Around line 4-7: Replace the hardcoded axios.create config so apiClient uses a
baseURL taken from an environment variable (e.g., process.env.API_BASE_URL) with
a safe fallback to the localhost URL, and do not set withCredentials: true by
default; remove or set withCredentials: false in the apiClient creation and
enable credentials only on requests that explicitly need cookie auth (set
per-request or via a separate axios instance). Update the axios.create call
(apiClient) to read baseURL from env and avoid global withCredentials to prevent
CORS/auth issues.

In `@Week04/SJ01-max/Mission3/src/pages/GoogleCallbackPage.tsx`:
- Around line 15-20: The callback handler in GoogleCallbackPage navigates using
navigate('/') and navigate('/login') which leaves the callback URL (with
?accessToken=...&refreshToken=...) in browser history; update the navigate calls
in the component (where setTokens and navigate are used) to include the replace
option (navigate('/', { replace: true }) and navigate('/login', { replace: true
})) so the callback URL is not retained in history after processing the tokens.

---

Nitpick comments:
In `@Week04/SJ01-max/Mission1/src/components/LoadingSpinner.tsx`:
- Around line 3-5: Add accessibility attributes to the LoadingSpinner component:
on the outer container (the div with className "flex flex-col items-center
justify-center min-h-64 gap-4") add role="status" and aria-live="polite" (and
optionally aria-busy="true") so assistive technologies reliably announce the
loading state; ensure the visible message element (the p with text "불러오는 중...")
remains for screen reader output.

In `@Week04/SJ01-max/Mission1/src/lib/tmdbClient.ts`:
- Around line 8-10: The Authorization header construction uses
import.meta.env.VITE_TMDB_KEY directly which can yield "Bearer undefined" if the
env var is missing; add a validation step where the TMDB key is read (e.g.,
const tmdbKey = import.meta.env.VITE_TMDB_KEY) and if falsy either throw an
informative error or log and abort startup, and only set headers.Authorization
to `Bearer ${tmdbKey}` when tmdbKey is present; update the code around the
header creation (the Authorization header line) to reference this validated
tmdbKey variable so no requests are sent with "Bearer undefined".

In `@Week04/SJ01-max/Mission1/src/pages/MovieListPage.tsx`:
- Around line 45-55: The category buttons only use color to show selection which
is inaccessible; update the button element in MovieListPage (the JSX that
renders each category button using cat.value, cat.label and
handleCategoryChange) to expose selection state to assistive tech by adding an
aria-pressed attribute bound to the boolean expression (category === cat.value)
(and optionally aria-current="true" for the active one), ensuring the attribute
updates when handleCategoryChange runs so screen readers can announce the active
category.

In `@Week04/SJ01-max/Mission2/src/hooks/useForm.ts`:
- Around line 42-45: The isValid computation calls validate(values) repeatedly
for each key; compute the validation once and reuse it: inside useForm.ts
replace the inline repeated calls in the isValid block by calling
validate(values) a single time (e.g., const errors = validate(values)) and then
use errors[k] when iterating Object.keys(initialValues) (keep casting key as
keyof T and checking values[k].length > 0 && !errors[k]) so validation runs once
and isValid uses the cached result.
- Line 10: The generic constraint on useForm is redundant—remove the duplicated
equivalent constraint and simplify it to a single constraint (e.g., change the
generic declaration on function useForm to use one constraint such as T extends
Record<string, string>) while keeping the existing UseFormOptions<T> usage;
update the useForm<T ...> signature to the simplified generic so the function,
its parameters, and callers compile the same but with a clearer single type
constraint.

In `@Week04/SJ01-max/Mission2/src/lib/authApi.ts`:
- Around line 21-24: saveTokens currently stores accessToken and refreshToken
under separate keys which is inconsistent with Mission3's apiClient.ts that
expects a single 'auth_tokens' JSON object; update the saveTokens function to
serialize both tokens into one object and save it under the 'auth_tokens' key
(JSON.stringify({ accessToken, refreshToken })) so reads by apiClient.ts (which
parses 'auth_tokens') will work uniformly across missions.

In `@Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx`:
- Around line 11-21: The validate function currently only checks email/password
when they are truthy; update validate (function name: validate, params: values:
LoginValues) to also set errors.email and errors.password when values.email or
values.password are empty or missing (e.g., === '' or falsy), using appropriate
messages like "필수 입력 항목입니다." and keep the existing format checks
(emailRegex.test and password length) so both required and format/length
validations run and populate the errors object returned.
- Around line 52-54: Replace the hardcoded Google login URL in handleGoogleLogin
with a URL built from an environment variable (e.g., API base like
REACT_APP_API_BASE or NEXT_PUBLIC_API_BASE); update the handleGoogleLogin
function to read the env var (fall back to a sensible default if needed) and
call window.location.href = `${API_BASE}/v1/auth/google/login`; also add/update
the .env entry for the chosen variable and document its expected value.

In `@Week04/SJ01-max/Mission3/src/hooks/useForm.ts`:
- Line 10: The generic constraint on useForm is redundant: change the signature
of function useForm<T extends Record<string, string>>({...}) by removing the
duplicate & { [key: string]: string } part so only a single indexable
string-record constraint remains; update any references to the useForm generic
if necessary to match the simplified T constraint.
- Around line 39-42: The isValid computation currently calls validate(values) on
every render; change it to use the existing errors state or memoize the
validation result to avoid repeated work: refer to the isValid constant and the
values, initialValues, validate and errors symbols — either compute isValid from
errors (e.g., check Object.keys(initialValues).every(k => values[k].length>0 &&
!errors[k])) or wrap the validation call in useMemo (depend on [values,
validate, initialValues]) and use that memoized validationErrors when
calculating isValid.
- Around line 18-28: handleChange closes over values causing unnecessary
re-creations; switch to a functional state update: inside handleChange compute
newValues from prev (e.g., setValues(prev => { const newValues = { ...prev,
[name]: value }; if (touched[name as keyof T]) setErrors(validate(newValues));
return newValues; })), remove values from the useCallback deps array, and keep
touched and validate in deps so handleChange remains stable and uses the latest
state when updating values and errors (refer to handleChange, setValues,
setErrors, validate, touched).

In `@Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts`:
- Around line 27-30: The removeValue function currently calls
setStoredValue(initialValue) and localStorage.removeItem(key) without error
handling; wrap its body in a try/catch (matching the pattern used elsewhere in
this hook) so any localStorage.removeItem or state update errors are caught and
handled (e.g., log the error or silently ignore) and ensure removeValue still
resets stored state on success; locate the removeValue function in
useLocalStorage.ts to apply this change.

In `@Week04/SJ01-max/Mission3/src/lib/authApi.ts`:
- Around line 22-25: The signUp function currently returns ApiResponse<unknown>,
which loses type safety; update the ApiResponse generic to the proper response
type (e.g., ApiResponse<User> or ApiResponse<SignUpResponse>) and change the
signUp signature to return that concrete type (or, if the endpoint returns
nothing useful, explicitly return Promise<void>), ensuring you update the
apiClient.post call generic and the return type of signUp to match (refer to
signUp and apiClient.post<ApiResponse<...>> in this file).

In `@Week04/SJ01-max/Mission3/src/pages/LoginPage.tsx`:
- Around line 47-49: The handleGoogleLogin function hardcodes
'http://localhost:8000/v1/auth/google/login'; change it to build the URL from an
environment variable (e.g. REACT_APP_API_BASE_URL or VITE_API_BASE_URL depending
on your build) so the app works in prod; update handleGoogleLogin to read the
base URL from process.env.REACT_APP_API_BASE_URL or
import.meta.env.VITE_API_BASE_URL with a safe fallback, append
'/v1/auth/google/login', and replace the hardcoded string; also add/update your
.env/.env.production entries for that variable.
🪄 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: c8d029e4-f9e7-4767-9cbf-23a890630663

📥 Commits

Reviewing files that changed from the base of the PR and between b0ad89d and 0104ec4.

⛔ Files ignored due to path filters (8)
  • Week04/SJ01-max/Mission1/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week04/SJ01-max/Mission1/public/favicon.svg is excluded by !**/*.svg
  • Week04/SJ01-max/Mission1/public/icons.svg is excluded by !**/*.svg
  • Week04/SJ01-max/Mission1/src/assets/hero.png is excluded by !**/*.png
  • Week04/SJ01-max/Mission1/src/assets/react.svg is excluded by !**/*.svg
  • Week04/SJ01-max/Mission1/src/assets/vite.svg is excluded by !**/*.svg
  • Week04/SJ01-max/Mission2/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week04/SJ01-max/Mission3/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (60)
  • Week04/SJ01-max/Mission1/.gitignore
  • Week04/SJ01-max/Mission1/README.md
  • Week04/SJ01-max/Mission1/eslint.config.js
  • Week04/SJ01-max/Mission1/index.html
  • Week04/SJ01-max/Mission1/package.json
  • Week04/SJ01-max/Mission1/src/App.css
  • Week04/SJ01-max/Mission1/src/App.tsx
  • Week04/SJ01-max/Mission1/src/components/ErrorMessage.tsx
  • Week04/SJ01-max/Mission1/src/components/LoadingSpinner.tsx
  • Week04/SJ01-max/Mission1/src/components/MovieCard.tsx
  • Week04/SJ01-max/Mission1/src/hooks/useCustomFetch.ts
  • Week04/SJ01-max/Mission1/src/index.css
  • Week04/SJ01-max/Mission1/src/lib/tmdbClient.ts
  • Week04/SJ01-max/Mission1/src/main.tsx
  • Week04/SJ01-max/Mission1/src/pages/MovieDetailPage.tsx
  • Week04/SJ01-max/Mission1/src/pages/MovieListPage.tsx
  • Week04/SJ01-max/Mission1/src/types/movie.ts
  • Week04/SJ01-max/Mission1/tsconfig.app.json
  • Week04/SJ01-max/Mission1/tsconfig.json
  • Week04/SJ01-max/Mission1/tsconfig.node.json
  • Week04/SJ01-max/Mission1/vite.config.ts
  • Week04/SJ01-max/Mission2/UMC-8th-BE
  • Week04/SJ01-max/Mission2/index.html
  • Week04/SJ01-max/Mission2/package.json
  • Week04/SJ01-max/Mission2/src/App.tsx
  • Week04/SJ01-max/Mission2/src/components/Navbar.tsx
  • Week04/SJ01-max/Mission2/src/hooks/useForm.ts
  • Week04/SJ01-max/Mission2/src/index.css
  • Week04/SJ01-max/Mission2/src/lib/apiClient.ts
  • Week04/SJ01-max/Mission2/src/lib/authApi.ts
  • Week04/SJ01-max/Mission2/src/main.tsx
  • Week04/SJ01-max/Mission2/src/pages/GoogleCallbackPage.tsx
  • Week04/SJ01-max/Mission2/src/pages/HomePage.tsx
  • Week04/SJ01-max/Mission2/src/pages/LoginPage.tsx
  • Week04/SJ01-max/Mission2/tsconfig.app.json
  • Week04/SJ01-max/Mission2/tsconfig.json
  • Week04/SJ01-max/Mission2/tsconfig.node.json
  • Week04/SJ01-max/Mission2/vite.config.ts
  • Week04/SJ01-max/Mission3/index.html
  • Week04/SJ01-max/Mission3/package.json
  • Week04/SJ01-max/Mission3/src/App.tsx
  • Week04/SJ01-max/Mission3/src/components/Navbar.tsx
  • Week04/SJ01-max/Mission3/src/components/signup/Step1Email.tsx
  • Week04/SJ01-max/Mission3/src/components/signup/Step2Password.tsx
  • Week04/SJ01-max/Mission3/src/components/signup/Step3Nickname.tsx
  • Week04/SJ01-max/Mission3/src/hooks/useForm.ts
  • Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts
  • Week04/SJ01-max/Mission3/src/index.css
  • Week04/SJ01-max/Mission3/src/lib/apiClient.ts
  • Week04/SJ01-max/Mission3/src/lib/authApi.ts
  • Week04/SJ01-max/Mission3/src/lib/schemas.ts
  • Week04/SJ01-max/Mission3/src/main.tsx
  • Week04/SJ01-max/Mission3/src/pages/GoogleCallbackPage.tsx
  • Week04/SJ01-max/Mission3/src/pages/HomePage.tsx
  • Week04/SJ01-max/Mission3/src/pages/LoginPage.tsx
  • Week04/SJ01-max/Mission3/src/pages/SignupPage.tsx
  • Week04/SJ01-max/Mission3/tsconfig.app.json
  • Week04/SJ01-max/Mission3/tsconfig.json
  • Week04/SJ01-max/Mission3/tsconfig.node.json
  • Week04/SJ01-max/Mission3/vite.config.ts

Comment on lines +1 to +184
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;

&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}

.hero {
position: relative;

.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}

.base {
width: 170px;
position: relative;
z-index: 0;
}

.framework,
.vite {
position: absolute;
}

.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}

.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}

#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;

@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}

#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;

& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}

.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}

@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}

#docs {
border-right: 1px solid var(--border);

@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}

#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;

.logo {
height: 18px;
}

a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;

&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}

@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;

li {
flex: 1 1 calc(50% - 8px);
}

a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}

#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}

.ticks {
position: relative;
width: 100%;

&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}

&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
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: App.css의 CSS 클래스들이 실제로 사용되는지 확인

echo "=== Checking usage of CSS classes from App.css ==="

# Check for .counter usage
echo -e "\n--- .counter ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'className.*counter' Week04/SJ01-max/Mission1/

# Check for .hero usage
echo -e "\n--- .hero ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'className.*hero' Week04/SJ01-max/Mission1/

# Check for .framework usage
echo -e "\n--- .framework ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'className.*framework' Week04/SJ01-max/Mission1/

# Check for .vite usage
echo -e "\n--- .vite ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'className.*vite' Week04/SJ01-max/Mission1/

# Check for `#next-steps` usage
echo -e "\n--- `#next-steps` ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'id.*next-steps' Week04/SJ01-max/Mission1/

# Check for `#docs` usage
echo -e "\n--- `#docs` ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'id.*docs' Week04/SJ01-max/Mission1/

# Check for .ticks usage
echo -e "\n--- .ticks ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'className.*ticks' Week04/SJ01-max/Mission1/

# Check for `#center` usage
echo -e "\n--- `#center` ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'id.*center' Week04/SJ01-max/Mission1/

# Check for `#spacer` usage
echo -e "\n--- `#spacer` ---"
rg -n --type=tsx --type=ts --type=jsx --type=js 'id.*spacer' Week04/SJ01-max/Mission1/

echo -e "\n=== If no matches found, this CSS file can be removed ==="

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

Length of output: 605


🏁 Script executed:

#!/bin/bash
# Improved search without problematic file type restrictions

echo "=== Searching for CSS class usage in App.css ==="

# Search for all occurrences in component files (broader approach)
echo -e "\n--- Searching in component files ---"
cd Week04/SJ01-max/Mission1

# List all component files first
echo "Component files found:"
find . -name "*.jsx" -o -name "*.js" -o -name "*.tsx" -o -name "*.ts" | head -20

echo -e "\n--- Searching for CSS class names (broader patterns) ---"
rg "(counter|hero|framework|vite|next-steps|docs|ticks|center|spacer)" . 2>/dev/null | grep -E "\.(jsx|js|tsx|ts):.*className|id" || echo "No matches found in component files"

echo -e "\n--- Checking if App.css is imported ---"
rg "App\.css|from.*css|import.*css" . 2>/dev/null || echo "No CSS imports found"

echo -e "\n--- File structure ---"
tree -L 3 . 2>/dev/null || find . -type f -name "*.jsx" -o -name "*.js" | head -15

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

Length of output: 11665


사용되지 않는 Vite 템플릿 보일러플레이트 CSS 파일입니다. 삭제하세요.

이 파일의 모든 CSS 클래스(.counter, .hero, .framework, .vite, #next-steps, #docs, .ticks 등)가 코드베이스 어디서도 사용되지 않고 있습니다. App.css는 main.tsx에서 임포트되지 않으며, 프로젝트는 Tailwind CSS(index.css를 통해 임포트)만 사용하고 있습니다. 이 파일은 Vite 초기 템플릿의 미사용 보일러플레이트 코드이므로 삭제해야 합니다.

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

In `@Week04/SJ01-max/Mission1/src/App.css` around lines 1 - 184, App.css contains
unused Vite template boilerplate selectors (.counter, .hero, .framework, .vite,
`#next-steps`, `#docs`, .ticks, etc.) and should be removed; delete the App.css file
and ensure there are no imports referencing it (check main.tsx and any other
entry files for an import of App.css and remove that import), and run a quick
global search for the listed selectors to confirm nothing else depends on them.

Comment on lines +18 to +50
useEffect(() => {
if (!url) return;

const controller = new AbortController();

const fetchData = async () => {
setIsLoading(true);
setError(null);

try {
const response = await tmdbClient.get<T>(url, {
params,
signal: controller.signal,
});
setData(response.data);
} catch (err: unknown) {
if ((err as { name?: string }).name === 'CanceledError') return;

if (err instanceof Error) {
setError(err.message);
} else {
setError('데이터를 불러오는 중 오류가 발생했습니다.');
}
} finally {
setIsLoading(false);
}
};

fetchData();

return () => {
controller.abort();
};
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 41-42의 finally는 cleanup에서 abort된 요청에도 실행됩니다. 카테고리나 페이지를 빠르게 바꾸면 이전 요청이 isLoadingfalse로 내려서, 새 요청이 진행 중인데도 스피너가 먼저 사라질 수 있습니다.

제안 코드
   useEffect(() => {
     if (!url) return;
 
     const controller = new AbortController();
+    let active = true;
 
     const fetchData = async () => {
       setIsLoading(true);
       setError(null);
 
       try {
         const response = await tmdbClient.get<T>(url, {
           params,
           signal: controller.signal,
         });
-        setData(response.data);
+        if (active) {
+          setData(response.data);
+        }
       } catch (err: unknown) {
         if ((err as { name?: string }).name === 'CanceledError') return;
 
-        if (err instanceof Error) {
+        if (!active) return;
+
+        if (err instanceof Error) {
           setError(err.message);
         } else {
           setError('데이터를 불러오는 중 오류가 발생했습니다.');
         }
       } finally {
-        setIsLoading(false);
+        if (active) {
+          setIsLoading(false);
+        }
       }
     };
 
     fetchData();
 
     return () => {
+      active = false;
       controller.abort();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission1/src/hooks/useCustomFetch.ts` around lines 18 - 50,
The current finally block in useEffect always calls setIsLoading(false) even
when the request was aborted, causing a canceled request to clear loading for a
subsequent request; modify fetchData/useEffect to avoid clearing loading for
aborted requests by tracking request liveness: create a local let isCurrent =
true (or check controller.signal.aborted) at the top of useEffect, call
controller.abort() and set isCurrent = false in the cleanup, and then in the
finally of fetchData only call setIsLoading(false) if isCurrent &&
!controller.signal.aborted; reference symbols: useEffect, fetchData, controller,
controller.abort, setIsLoading.

Comment on lines +69 to +79
<div className="flex-shrink-0 mx-auto md:mx-0">
{movie.poster_path ? (
<img
src={`${POSTER_BASE}${movie.poster_path}`}
alt={movie.title}
className="w-52 rounded-xl shadow-2xl shadow-black/60 -mt-24 relative z-10 border border-gray-700"
/>
) : (
<div className="w-52 aspect-[2/3] rounded-xl bg-gray-800 flex items-center justify-center -mt-24 relative z-10 border border-gray-700">
<span className="text-5xl">🎬</span>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

배경 이미지가 없을 때 포스터가 위로 겹칩니다.

Line 74와 Line 77의 -mt-24가 항상 적용돼서 movie.backdrop_path가 없는 영화는 포스터/플레이스홀더가 헤더 쪽으로 당겨집니다. 배경이 있을 때만 offset을 주는 편이 안전합니다.

제안 코드
                 {movie.poster_path ? (
                   <img
                     src={`${POSTER_BASE}${movie.poster_path}`}
                     alt={movie.title}
-                    className="w-52 rounded-xl shadow-2xl shadow-black/60 -mt-24 relative z-10 border border-gray-700"
+                    className={`w-52 rounded-xl shadow-2xl shadow-black/60 relative z-10 border border-gray-700 ${
+                      movie.backdrop_path ? '-mt-24' : ''
+                    }`}
                   />
                 ) : (
-                  <div className="w-52 aspect-[2/3] rounded-xl bg-gray-800 flex items-center justify-center -mt-24 relative z-10 border border-gray-700">
+                  <div
+                    className={`w-52 aspect-[2/3] rounded-xl bg-gray-800 flex items-center justify-center relative z-10 border border-gray-700 ${
+                      movie.backdrop_path ? '-mt-24' : ''
+                    }`}
+                  >
                     <span className="text-5xl">🎬</span>
                   </div>
                 )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission1/src/pages/MovieDetailPage.tsx` around lines 69 - 79,
The poster/placeholder always has the '-mt-24' offset causing it to overlap the
header when no backdrop exists; update the MovieDetailPage rendering for the
poster (<img> and the placeholder <div>) to apply the '-mt-24' class only when
movie.backdrop_path is truthy (e.g., compute a conditional className or template
string inside the component), leaving it out when movie.backdrop_path is falsy
so the poster doesn't shift upward; locate the poster markup inside the
MovieDetailPage component where movie.poster_path and the placeholder are
rendered and adjust the className to conditionally include '-mt-24'.

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

문서 언어 설정이 실제 서비스 언어와 불일치합니다.

Line 2의 lang="en"은 한국어 중심 UI와 맞지 않아 접근성/언어 인식 품질이 떨어질 수 있습니다. lang="ko"로 맞춰 주세요.

수정 제안
-<html lang="en">
+<html lang="ko">
📝 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
<html lang="en">
<html lang="ko">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/index.html` at line 2, The HTML root element
currently has lang="en" which mismatches the Korean UI; update the html
element's lang attribute (the <html lang="en"> declaration) to lang="ko" so the
document language correctly reflects Korean for accessibility and language
detection.

@@ -0,0 +1,34 @@
{
"name": "mission1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

패키지명이 프로젝트 디렉터리와 불일치합니다.

Mission2 프로젝트인데 "name": "mission1"로 되어 있어 패키지 식별/배포 시 혼동을 유발할 수 있습니다.

🔧 제안 수정
-  "name": "mission1",
+  "name": "mission2",
📝 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
"name": "mission1",
"name": "mission2",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission2/package.json` at line 2, 패키지 메타정보의 "name" 값이 프로젝트
디렉터리와 일치하지 않습니다; package.json의 "name" 필드("name": "mission1")를 프로젝트 폴더명에 맞게 예:
"mission2"로 변경하여 패키지 식별/배포 혼동을 제거하세요.

Comment on lines +79 to +85
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
>
<EyeIcon open={showPassword} />
</button>
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

비밀번호 토글 버튼에 접근성 이름이 없습니다.

아이콘 버튼에 aria-label/aria-pressed를 추가해 스크린리더 사용자도 기능을 식별할 수 있게 해주세요.

🔧 제안 수정
           <button
             type="button"
             onClick={() => setShowPassword((v) => !v)}
             className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
+            aria-label={showPassword ? '비밀번호 숨기기' : '비밀번호 보기'}
+            aria-pressed={showPassword}
           >
             <EyeIcon open={showPassword} />
           </button>
@@
           <button
             type="button"
             onClick={() => setShowConfirm((v) => !v)}
             className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
+            aria-label={showConfirm ? '비밀번호 확인 숨기기' : '비밀번호 확인 보기'}
+            aria-pressed={showConfirm}
           >
             <EyeIcon open={showConfirm} />
           </button>

Also applies to: 103-109

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

In `@Week04/SJ01-max/Mission3/src/components/signup/Step2Password.tsx` around
lines 79 - 85, The password visibility toggle button lacks accessible labeling;
update the button (the element wrapping EyeIcon, which uses showPassword and
setShowPassword) to include an aria-label that reflects action/state (e.g.,
"Show password" when showPassword is false and "Hide password" when true) and
add aria-pressed={showPassword} so screen readers know the toggle state; apply
the same changes to the other toggle button instance as well.

Comment on lines +13 to +21
const setValue = (value: T | ((prev: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (valueToStore === null || valueToStore === undefined) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(valueToStore));
}
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
# 함수형 업데이트가 stale 클로저(storedValue)를 사용하는지 확인
rg -n -C2 "value instanceof Function \? value\(storedValue\) : value"

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

Length of output: 605


🏁 Script executed:

cat -n Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts

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

Length of output: 1317


함수형 업데이트 시 stale 클로저로 인한 값 유실 문제가 있습니다.

15번 줄에서 value(storedValue)는 최신 상태가 아니라 클로저로 캡처된 값을 참조하므로, 연속 업데이트 시 이전 업데이트가 손실될 수 있습니다. 예를 들어 setValue(prev => prev.count + 1)을 빠르게 두 번 호출해도 storedValue는 첫 번째 렌더링 시점의 값으로 고정되어 두 번째 호출도 같은 값을 기반으로 계산합니다.

🔧 제안 수정
  const setValue = (value: T | ((prev: T) => T)) => {
    try {
-     const valueToStore = value instanceof Function ? value(storedValue) : value;
-     setStoredValue(valueToStore);
-     if (valueToStore === null || valueToStore === undefined) {
-       localStorage.removeItem(key);
-     } else {
-       localStorage.setItem(key, JSON.stringify(valueToStore));
-     }
+     setStoredValue((prev) => {
+       const valueToStore = value instanceof Function ? value(prev) : value;
+       if (valueToStore === null || valueToStore === undefined) {
+         localStorage.removeItem(key);
+       } else {
+         localStorage.setItem(key, JSON.stringify(valueToStore));
+       }
+       return valueToStore;
+     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/hooks/useLocalStorage.ts` around lines 13 - 21,
setValue currently computes valueToStore using the stale closed-over
storedValue; change it to use the functional updater of setStoredValue to get
the latest prev state. In other words, inside setValue call setStoredValue(prev
=> { const valueToStore = value instanceof Function ? value(prev) : value; if
(valueToStore == null) localStorage.removeItem(key); else
localStorage.setItem(key, JSON.stringify(valueToStore)); return valueToStore; })
so updates use the freshest state; keep references to setValue, storedValue,
setStoredValue, localStorage and key.

Comment on lines +4 to +7
const apiClient = axios.create({
baseURL: 'http://localhost:8000/v1',
withCredentials: true,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

API 클라이언트 기본 설정이 운영 환경에서 깨질 가능성이 큽니다.

Line 5의 하드코딩된 http://localhost:8000/v1와 Line 6의 withCredentials: true 조합은 배포 환경/CORS에서 실패하기 쉽습니다. 현재 인증 흐름은 쿠키가 아니라 Bearer 토큰 기반이므로, baseURL은 환경변수로 분리하고 credential 전송은 명시적으로 필요한 경우에만 켜는 편이 안전합니다.

제안 수정안
 const apiClient = axios.create({
-  baseURL: 'http://localhost:8000/v1',
-  withCredentials: true,
+  baseURL: import.meta.env.VITE_API_BASE_URL,
+  withCredentials: false,
 });
📝 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 apiClient = axios.create({
baseURL: 'http://localhost:8000/v1',
withCredentials: true,
});
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: false,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/lib/apiClient.ts` around lines 4 - 7, Replace
the hardcoded axios.create config so apiClient uses a baseURL taken from an
environment variable (e.g., process.env.API_BASE_URL) with a safe fallback to
the localhost URL, and do not set withCredentials: true by default; remove or
set withCredentials: false in the apiClient creation and enable credentials only
on requests that explicitly need cookie auth (set per-request or via a separate
axios instance). Update the axios.create call (apiClient) to read baseURL from
env and avoid global withCredentials to prevent CORS/auth issues.

Comment on lines +13 to +16
if (stored) {
const tokens: AuthTokens = JSON.parse(stored);
config.headers.Authorization = `Bearer ${tokens.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 | 🟡 Minor

토큰 형태 검증 없이 Authorization을 설정하면 잘못된 헤더가 전송될 수 있습니다.

Line 15에서 tokens.accessToken 유효성 확인 없이 헤더를 세팅해 Bearer undefined가 전송될 수 있습니다. 최소한 문자열/빈값 체크를 추가해 주세요.

제안 수정안
     if (stored) {
       const tokens: AuthTokens = JSON.parse(stored);
-      config.headers.Authorization = `Bearer ${tokens.accessToken}`;
+      if (typeof tokens.accessToken === 'string' && tokens.accessToken.length > 0) {
+        config.headers.Authorization = `Bearer ${tokens.accessToken}`;
+      }
     }
📝 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
if (stored) {
const tokens: AuthTokens = JSON.parse(stored);
config.headers.Authorization = `Bearer ${tokens.accessToken}`;
}
if (stored) {
const tokens: AuthTokens = JSON.parse(stored);
if (typeof tokens.accessToken === 'string' && tokens.accessToken.length > 0) {
config.headers.Authorization = `Bearer ${tokens.accessToken}`;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/lib/apiClient.ts` around lines 13 - 16, 현재
stored에서 파싱한 AuthTokens를 검증하지 않고 config.headers.Authorization에 바로 설정해 Bearer
undefined가 전송될 수 있습니다; 수정 방법은 JSON.parse(stored)로 얻은 tokens와 tokens.accessToken이
존재하는지, 문자열인지, 비어있지 않은지 확인한 뒤에만 config.headers.Authorization = `Bearer
${tokens.accessToken}`을 설정하고 그렇지 않으면 Authorization 헤더를 제거하거나 설정하지 않도록 변경하세요 (참고
식별자: stored, AuthTokens, tokens.accessToken, config.headers.Authorization).

Comment on lines +15 to +20
if (accessToken && refreshToken) {
setTokens({ accessToken, refreshToken });
navigate('/');
} else {
navigate('/login');
}
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, Line 19는 replace 없이 이동해서 ?accessToken=...&refreshToken=... URL이 히스토리에 남습니다. 콜백 이동은 navigate(..., { replace: true })로 처리해 토큰 노출면을 줄여주세요.

제안 수정안
     if (accessToken && refreshToken) {
       setTokens({ accessToken, refreshToken });
-      navigate('/');
+      navigate('/', { replace: true });
     } else {
-      navigate('/login');
+      navigate('/login', { replace: true });
     }
📝 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
if (accessToken && refreshToken) {
setTokens({ accessToken, refreshToken });
navigate('/');
} else {
navigate('/login');
}
if (accessToken && refreshToken) {
setTokens({ accessToken, refreshToken });
navigate('/', { replace: true });
} else {
navigate('/login', { replace: true });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Week04/SJ01-max/Mission3/src/pages/GoogleCallbackPage.tsx` around lines 15 -
20, The callback handler in GoogleCallbackPage navigates using navigate('/') and
navigate('/login') which leaves the callback URL (with
?accessToken=...&refreshToken=...) in browser history; update the navigate calls
in the component (where setTokens and navigate are used) to include the replace
option (navigate('/', { replace: true }) and navigate('/login', { replace: true
})) so the callback URL is not retained in history after processing the tokens.

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