Conversation
📝 WalkthroughWalkthroughWeek04/SJ01-max 디렉토리에 세 개의 React + TypeScript + Vite 프로젝트를 추가합니다. Mission1은 TMDB API를 통한 영화 검색 및 상세 조회 기능을 구현하고, Mission2는 이메일/비밀번호 및 Google OAuth 기반 로그인을 제공합니다. Mission3는 Mission2를 확장하여 3단계 회원가입 폼 입력을 추가합니다. Changes
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: 영화 그리드 표시
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: 홈페이지로 리디렉트
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: 로그인 페이지로 리디렉트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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.email과values.password가 truthy일 때만 검증하므로, 빈 필드는 에러를 표시하지 않습니다.useForm의isValid가 비어있지 않은 값을 확인하므로 기능적으로는 동작하지만, 사용자가 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.ts는auth_tokens키에서 JSON 객체로 토큰을 읽지만, 여기서는accessToken과refreshToken을 별도의 키로 저장합니다. 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
⛔ Files ignored due to path filters (8)
Week04/SJ01-max/Mission1/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlWeek04/SJ01-max/Mission1/public/favicon.svgis excluded by!**/*.svgWeek04/SJ01-max/Mission1/public/icons.svgis excluded by!**/*.svgWeek04/SJ01-max/Mission1/src/assets/hero.pngis excluded by!**/*.pngWeek04/SJ01-max/Mission1/src/assets/react.svgis excluded by!**/*.svgWeek04/SJ01-max/Mission1/src/assets/vite.svgis excluded by!**/*.svgWeek04/SJ01-max/Mission2/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlWeek04/SJ01-max/Mission3/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (60)
Week04/SJ01-max/Mission1/.gitignoreWeek04/SJ01-max/Mission1/README.mdWeek04/SJ01-max/Mission1/eslint.config.jsWeek04/SJ01-max/Mission1/index.htmlWeek04/SJ01-max/Mission1/package.jsonWeek04/SJ01-max/Mission1/src/App.cssWeek04/SJ01-max/Mission1/src/App.tsxWeek04/SJ01-max/Mission1/src/components/ErrorMessage.tsxWeek04/SJ01-max/Mission1/src/components/LoadingSpinner.tsxWeek04/SJ01-max/Mission1/src/components/MovieCard.tsxWeek04/SJ01-max/Mission1/src/hooks/useCustomFetch.tsWeek04/SJ01-max/Mission1/src/index.cssWeek04/SJ01-max/Mission1/src/lib/tmdbClient.tsWeek04/SJ01-max/Mission1/src/main.tsxWeek04/SJ01-max/Mission1/src/pages/MovieDetailPage.tsxWeek04/SJ01-max/Mission1/src/pages/MovieListPage.tsxWeek04/SJ01-max/Mission1/src/types/movie.tsWeek04/SJ01-max/Mission1/tsconfig.app.jsonWeek04/SJ01-max/Mission1/tsconfig.jsonWeek04/SJ01-max/Mission1/tsconfig.node.jsonWeek04/SJ01-max/Mission1/vite.config.tsWeek04/SJ01-max/Mission2/UMC-8th-BEWeek04/SJ01-max/Mission2/index.htmlWeek04/SJ01-max/Mission2/package.jsonWeek04/SJ01-max/Mission2/src/App.tsxWeek04/SJ01-max/Mission2/src/components/Navbar.tsxWeek04/SJ01-max/Mission2/src/hooks/useForm.tsWeek04/SJ01-max/Mission2/src/index.cssWeek04/SJ01-max/Mission2/src/lib/apiClient.tsWeek04/SJ01-max/Mission2/src/lib/authApi.tsWeek04/SJ01-max/Mission2/src/main.tsxWeek04/SJ01-max/Mission2/src/pages/GoogleCallbackPage.tsxWeek04/SJ01-max/Mission2/src/pages/HomePage.tsxWeek04/SJ01-max/Mission2/src/pages/LoginPage.tsxWeek04/SJ01-max/Mission2/tsconfig.app.jsonWeek04/SJ01-max/Mission2/tsconfig.jsonWeek04/SJ01-max/Mission2/tsconfig.node.jsonWeek04/SJ01-max/Mission2/vite.config.tsWeek04/SJ01-max/Mission3/index.htmlWeek04/SJ01-max/Mission3/package.jsonWeek04/SJ01-max/Mission3/src/App.tsxWeek04/SJ01-max/Mission3/src/components/Navbar.tsxWeek04/SJ01-max/Mission3/src/components/signup/Step1Email.tsxWeek04/SJ01-max/Mission3/src/components/signup/Step2Password.tsxWeek04/SJ01-max/Mission3/src/components/signup/Step3Nickname.tsxWeek04/SJ01-max/Mission3/src/hooks/useForm.tsWeek04/SJ01-max/Mission3/src/hooks/useLocalStorage.tsWeek04/SJ01-max/Mission3/src/index.cssWeek04/SJ01-max/Mission3/src/lib/apiClient.tsWeek04/SJ01-max/Mission3/src/lib/authApi.tsWeek04/SJ01-max/Mission3/src/lib/schemas.tsWeek04/SJ01-max/Mission3/src/main.tsxWeek04/SJ01-max/Mission3/src/pages/GoogleCallbackPage.tsxWeek04/SJ01-max/Mission3/src/pages/HomePage.tsxWeek04/SJ01-max/Mission3/src/pages/LoginPage.tsxWeek04/SJ01-max/Mission3/src/pages/SignupPage.tsxWeek04/SJ01-max/Mission3/tsconfig.app.jsonWeek04/SJ01-max/Mission3/tsconfig.jsonWeek04/SJ01-max/Mission3/tsconfig.node.jsonWeek04/SJ01-max/Mission3/vite.config.ts
| .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); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 -15Repository: 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.
| 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(); | ||
| }; |
There was a problem hiding this comment.
취소된 이전 요청이 현재 로딩 상태를 덮어쓰고 있습니다.
Line 41-42의 finally는 cleanup에서 abort된 요청에도 실행됩니다. 카테고리나 페이지를 빠르게 바꾸면 이전 요청이 isLoading을 false로 내려서, 새 요청이 진행 중인데도 스피너가 먼저 사라질 수 있습니다.
제안 코드
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.
| <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> |
There was a problem hiding this comment.
배경 이미지가 없을 때 포스터가 위로 겹칩니다.
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"> | |||
There was a problem hiding this comment.
문서 언어 설정이 실제 서비스 언어와 불일치합니다.
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.
| <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", | |||
There was a problem hiding this comment.
패키지명이 프로젝트 디렉터리와 불일치합니다.
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.
| "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"로 변경하여 패키지 식별/배포 혼동을 제거하세요.
| <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> |
There was a problem hiding this comment.
비밀번호 토글 버튼에 접근성 이름이 없습니다.
아이콘 버튼에 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.
| 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)); | ||
| } |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.
| const apiClient = axios.create({ | ||
| baseURL: 'http://localhost:8000/v1', | ||
| withCredentials: true, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| if (stored) { | ||
| const tokens: AuthTokens = JSON.parse(stored); | ||
| config.headers.Authorization = `Bearer ${tokens.accessToken}`; | ||
| } |
There was a problem hiding this comment.
토큰 형태 검증 없이 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.
| 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).
| if (accessToken && refreshToken) { | ||
| setTokens({ accessToken, refreshToken }); | ||
| navigate('/'); | ||
| } else { | ||
| navigate('/login'); | ||
| } |
There was a problem hiding this comment.
콜백 토큰이 브라우저 히스토리에 남습니다.
현재 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.
| 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.
📝 미션 번호
4주차 Misson 1
📋 구현 사항
📎 스크린샷
📝 미션 번호
4주차 Misson 2
📋 구현 사항
📎 스크린샷
📝 미션 번호
4주차 Misson 3
📋 구현 사항
📎 스크린샷
✅ 체크리스트
🤔 질문 사항
Summary by CodeRabbit
릴리스 노트
New Features
Documentation