feat: 8주차 미션 완료#70
Conversation
📝 Walkthrough개요이 PR은 Week08 미션 3을 위해 React + TypeScript + Vite 스택 기반의 완전한 프론트엔드 애플리케이션을 처음부터 구축합니다. 인증 시스템, 무한 스크롤 LP 피드, 좋아요 기능, 사용자 프로필 관리를 포함하는 종합적인 애플리케이션입니다. 변경 사항애플리케이션 초기화 및 인프라 구축
예상 코드 리뷰 난이도🎯 4 (복잡함) | ⏱️ ~75분 이 PR은 다음 이유로 상당한 리뷰 노력을 요구합니다:
관련 PR
제안 리뷰어
시
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 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: 8
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (24)
Week08/hyunbin5921/misson3/src/components/CreateLpModal.tsx-37-44 (1)
37-44:⚠️ Potential issue | 🟠 Major | ⚡ Quick win제출 시 LP 생성이 실행되지 않아 기능이 완성되지 않았습니다.
현재
handleSubmit은 모달만 닫고 실제 생성 요청이 없어, 사용자 입장에서 “Add LP”가 동작하지 않습니다. 최소한 mutate 호출 + 성공/실패 처리까지 연결이 필요합니다.예시 수정안
const handleSubmit = (e: FormEvent) => { e.preventDefault(); - - // TODO: 실제 LP 생성 API에 맞게 body 만들어서 mutate 호출 - // 예: createLpMutate({ title: lpName, content: lpContent, tags, thumbnailFile }) - - handleClose(); + if (!lpName.trim() || !lpContent.trim()) return; + createLpMutate( + { title: lpName.trim(), content: lpContent.trim(), tags, thumbnailFile }, + { + onSuccess: () => { + handleClose(); + }, + } + ); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/CreateLpModal.tsx` around lines 37 - 44, handleSubmit currently only closes the modal; call the LP creation mutation inside it (use the existing createLpMutate / createLp.mutate) with the body built from lpName, lpContent, tags, thumbnailFile (e.g., createLpMutate({ title: lpName, content: lpContent, tags, thumbnailFile })), wire up onSuccess to call handleClose() (and optionally reset form) and onError to show a user-facing error (toast or set an error state), and ensure you prevent duplicate submissions (disable submit while mutation.isLoading) and still call e.preventDefault() at the top of handleSubmit.Week08/hyunbin5921/misson3/src/pages/GoogleLoginRedirectPage.tsx-15-19 (1)
15-19:⚠️ Potential issue | 🟠 Major | ⚡ Quick win액세스 토큰만으로 로그인 완료 처리하면 인증 상태가 깨질 수 있습니다.
현재는
accessToken만 있으면refreshToken이 없어도 저장 후/my로 이동합니다. 두 토큰이 모두 있을 때만 저장/리다이렉트하도록 조건을 묶는 게 안전합니다.수정 예시
- if (accessToken) { - setAccessToken(accessToken) - setRefreshToken(refreshToken) - window.location.href = "/my" - } + if (accessToken && refreshToken) { + setAccessToken(accessToken) + setRefreshToken(refreshToken) + window.location.href = "/my" + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/GoogleLoginRedirectPage.tsx` around lines 15 - 19, The current redirect logic checks only accessToken and may store tokens and redirect even when refreshToken is missing; update the conditional in GoogleLoginRedirectPage to require both accessToken and refreshToken before calling setAccessToken, setRefreshToken and setting window.location.href = "/my" (i.e., change the if(accessToken) check to if(accessToken && refreshToken) and keep the same calls to setAccessToken, setRefreshToken, and window.location.href inside that block).Week08/hyunbin5921/misson3/src/pages/ThrottlePage.tsx-14-14 (1)
14-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick win이벤트 정리(cleanup) 코드가 반대로 작성되었습니다.
언마운트 시 리스너를 제거해야 하는데 다시 등록하고 있습니다.
수정 예시
- return () => window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/ThrottlePage.tsx` at line 14, The cleanup in the effect is registering the scroll listener instead of removing it; update the cleanup returned from the useEffect in ThrottlePage so it calls window.removeEventListener("scroll", handleScroll) (matching the original addEventListener signature) rather than addEventListener, ensuring the handleScroll listener is properly detached on unmount.Week08/hyunbin5921/misson3/.gitignore-10-14 (1)
10-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick win환경변수 파일 무시 규칙을 추가하세요.
현재 규칙에는
.env/.env.*가 없어 API 키 같은 민감정보가 실수로 커밋될 수 있습니다.🔧 제안 패치
node_modules dist dist-ssr *.local +.env +.env.* +!.env.example🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/.gitignore` around lines 10 - 14, Update the .gitignore to ignore environment variable files by adding patterns for .env and .env.* so API keys and other secrets are not committed; specifically add entries for ".env" and ".env.*" alongside the existing patterns like "node_modules" and "dist" to ensure any environment-specific files are excluded from source control.Week08/hyunbin5921/misson3/src/context/AuthContext.tsx-31-36 (1)
31-36:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftrefresh token을 localStorage에 저장하는 방식은 보안 리스크가 큽니다.
Line 33-35, Line 53에서 refresh token을 localStorage에 보관하고 있어 XSS 시 장기 세션 탈취로 이어질 수 있습니다. refresh token은 HttpOnly/Secure 쿠키로 이관하는 쪽이 안전합니다.
Also applies to: 52-53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/context/AuthContext.tsx` around lines 31 - 36, The code stores refresh tokens in localStorage using useLocalStorage (functions getRefreshTokeninStorage, setRefreshTokeninStorage, removeRefreshTokenFromStorage and LOCAL_STORAGE_KEY.refreshToken) which is a security risk; refactor AuthContext to stop saving/reading the refresh token from localStorage and instead send/receive the refresh token via HttpOnly, Secure cookies from the server (remove setRefreshTokeninStorage/getRefreshTokeninStorage/removeRefreshTokenFromStorage usage and any logic that expects local access to the refresh token), update token refresh and logout flows to rely on cookie-based refresh endpoints, and ensure any client-side code uses only the access token in memory (or transient storage) while the server sets/deletes the HttpOnly cookie.Week08/hyunbin5921/misson3/src/layouts/HomeLayout.tsx-6-6 (1)
6-6:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
lucide-react에서 잘못된 Sidebar를 import하고 있으며, 커스텀 Sidebar 컴포넌트도 필수 props가 누락되어 있습니다.Line 6의 import는 아이콘 라이브러리
lucide-react에서 Sidebar를 가져오고 있어, Line 16에서 네비게이션 사이드바 대신 아이콘을 렌더링하거나 오류가 발생합니다. 실제 의도는src/components/Sidebar.tsx의 커스텀 컴포넌트를 사용하는 것이어야 하며, 이 컴포넌트는isOpen과onCloseprops를 필수로 요구합니다.🔧 수정 사항
-import { Sidebar } from "lucide-react"; +import Sidebar from "../components/Sidebar";그리고 Line 16의
<Sidebar />호출 시 상태 관리를 위해isOpen과onCloseprops를 전달해야 합니다:-<Sidebar /> +<Sidebar isOpen={isOpen} onClose={onClose} />이를 위해 HomeLayout 컴포넌트에서 상태를 관리하는 로직이 추가되어야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/layouts/HomeLayout.tsx` at line 6, The code imports Sidebar from the lucide-react icon library instead of the local component and fails to pass the required props; update the import to import Sidebar from the local component file (src/components/Sidebar.tsx), add state in HomeLayout (useState boolean, e.g., isOpen and setIsOpen), implement an onClose handler that calls setIsOpen(false), and pass both isOpen={isOpen} and onClose={onClose} into the <Sidebar /> usage so the custom Sidebar receives its required props.Week08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsx-7-7 (1)
7-7:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Sidebarimport 대상이 잘못되어 실제 사이드바 UI가 누락될 수 있습니다.Line 7은
lucide-react아이콘을 가져오고 있어, Line 22에서 내비게이션 사이드바 대신 아이콘만 렌더링됩니다. 의도대로라면../components/Sidebar컴포넌트를 import해야 합니다.수정 예시
-import { Sidebar } from "lucide-react"; +import Sidebar from "../components/Sidebar";Also applies to: 22-22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsx` at line 7, The import for Sidebar is wrong: replace the lucide-react icon import with the local Sidebar component import so the actual navigation UI is rendered; in ProtectedLayout.tsx update the import that currently reads importing Sidebar from "lucide-react" to import the Sidebar component from "../components/Sidebar" (or the correct relative path) and confirm the JSX that renders <Sidebar /> (in the render block where Sidebar is used) now references this component.Week08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsx-12-14 (1)
12-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick win렌더 단계에서
alert부수효과를 제거하고 올바른 Sidebar 컴포넌트를 import하세요.Line 13의
alert는 렌더링 중 실행되어 Strict Mode에서 중복 호출될 수 있습니다. 가드 분기에서는 리다이렉트만 수행하고, 안내는 로그인 페이지 토스트/배너로 처리하는 편이 안전합니다.또한 Line 7에서
lucide-react의Sidebar아이콘을 import하여 Line 22에서 렌더링하고 있으나, 실제 네비게이션 사이드바 컴포넌트(src/components/Sidebar.tsx)가 이미 존재합니다. 올바른 컴포넌트를 import하여isOpen과onCloseprops와 함께 사용해야 합니다.수정 예시
- import { Sidebar } from "lucide-react"; + import Sidebar from "../components/Sidebar";if (!accessToken) { - alert("로그인이 필요합니다!") return <Navigate to={"/login"} replace />; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsx` around lines 12 - 14, Remove the alert side-effect from the render guard in ProtectedLayout (when accessToken is falsy) so the branch only returns <Navigate to={"/login"} replace /> and move any user messaging to the login page (toast/banner). Replace the incorrect Sidebar import from 'lucide-react' with the actual Sidebar component (src/components/Sidebar.tsx) and render that Sidebar component with the expected props (pass isOpen and onClose where Sidebar is used) so the layout uses the real navigation sidebar instead of the icon.Week08/hyunbin5921/misson3/src/hooks/useThrottle.ts-24-29 (1)
24-29:⚠️ Potential issue | 🟠 Major | ⚡ Quick win지연 실행 시간이 잘못 계산되어 스로틀 간격이 과도하게 늘어납니다.
현재는 남은 시간이 아니라 항상
delay전체를 다시 기다려서, 특정 구간에서 의도보다 늦게 갱신됩니다. 남은 시간 기준으로 타이머를 잡아야 합니다.제안 diff
- const timeId = setTimeout(() => { + const remaining = Math.max(0, lastExecuted.current + delay - Date.now()); + const timeId = setTimeout(() => { // 타이머 만료 시 lastExecuted.current = Date.now(); // 최신 value를 throttledValue에 저장해서 컴포넌트로 리렌더링 setThrottledValue(value); - }, delay); + }, remaining);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/hooks/useThrottle.ts` around lines 24 - 29, The timeout in useThrottle incorrectly always waits the full delay, causing excessive intervals; change the timer to use the remaining time by computing remaining = delay - (Date.now() - lastExecuted.current) (clamp to >=0) and pass that to setTimeout instead of delay, so setTimeout(() => { lastExecuted.current = Date.now(); setThrottledValue(value); }, Math.max(remaining, 0)); locate this logic around the timeId / setTimeout usage in useThrottle and update the calculation there.Week08/hyunbin5921/misson3/src/pages/SignupPage.tsx-42-47 (1)
42-47:⚠️ Potential issue | 🟠 Major | ⚡ Quick win회원가입 API 호출 실패 처리가 없어 사용자 피드백이 누락됩니다.
postSignup실패 시 예외 처리를 추가해 오류 메시지/재시도 흐름을 제공하는 편이 안전합니다.제안 diff
- const onSubmit: SubmitHandler<formField> = async(data) => { - const {passwordCheck, ...rest} = data - - const response = await postSignup(rest) - console.log(rest); - }; + const onSubmit: SubmitHandler<formField> = async (data) => { + const { passwordCheck, ...rest } = data; + try { + await postSignup(rest); + } catch (error) { + console.error("회원가입 오류", error); + alert("회원가입에 실패했습니다."); + } + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/SignupPage.tsx` around lines 42 - 47, The onSubmit handler currently calls postSignup(rest) without error handling, so wrap the call inside a try/catch in the onSubmit function (reference onSubmit, postSignup, formField, passwordCheck) and handle failures by showing user feedback (e.g., set form errors or a toast/alert) and optionally offering retry logic; in the catch block log the error, display a user-friendly message, and ensure any loading state is cleared so the UI reflects the failure.Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx-28-31 (1)
28-31:⚠️ Potential issue | 🟠 Major | ⚡ Quick win로그인 값(이메일/비밀번호) 콘솔 출력은 민감정보 노출 위험이 있습니다.
성공/실패 로그에서 자격 증명 데이터 출력은 제거하는 것이 안전합니다.
제안 diff
- console.log(values); ... - console.log("로그인 오류", error); + console.error("로그인 오류", error);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx` around lines 28 - 31, The console log in LoginPage.tsx is printing sensitive credentials (console.log(values)) and must be removed; inside the login handler (the function handling form submission in LoginPage.tsx) delete any logging of the full values object and instead log only non-sensitive status messages (e.g., "login request sent" or the user id/email masked) or remove the log entirely; also ensure the catch block's console.log("로그인 오류", error) does not accidentally include sensitive data by only logging the error message/stack or a generic failure message before showing alert("로그인 실패").Week08/hyunbin5921/misson3/src/pages/SignupPage.tsx-101-105 (1)
101-105:⚠️ Potential issue | 🟠 Major | ⚡ Quick win버튼이
type="button"이라 Enter 키 제출 흐름이 깨집니다.폼 제출은
<form onSubmit={handleSubmit(onSubmit)}>+type="submit"으로 연결해야 키보드 접근성과 기본 제출 동작이 보장됩니다.제안 diff
- <div className="flex flex-col gap-3"> + <form className="flex flex-col gap-3" onSubmit={handleSubmit(onSubmit)}> ... - <button + <button className="w-full bg-blue-600 text-white py-3 rounded-md text-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer disabled:bg-gray-300" - type="button" - onClick={handleSubmit(onSubmit)} + type="submit" disabled={isSubmitting} > 회원가입 </button> - </div> + </form>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/SignupPage.tsx` around lines 101 - 105, The button currently uses type="button" and calls handleSubmit(onSubmit) onClick which breaks Enter-key submission; change the form element to use onSubmit={handleSubmit(onSubmit)} (if not already) and set the button to type="submit", removing the onClick handler on the button; reference the existing handleSubmit and onSubmit functions and the isSubmitting prop so the button remains disabled during submission.Week08/hyunbin5921/misson3/src/utils/validate.ts-13-17 (1)
13-17:⚠️ Potential issue | 🟠 Major | ⚡ Quick win이메일 정규식이 과도하게 제한적이라 정상 계정을 막을 수 있습니다.
현재 패턴은 유효한 이메일(긴 TLD,
+포함 로컬파트 등)을 거부할 수 있어 로그인 UX를 깨뜨릴 가능성이 큽니다.제안 diff
- !/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test( + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( values.email )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/utils/validate.ts` around lines 13 - 17, The current email regex in validate.ts (the test used before setting errors.email in the validation logic) is too restrictive and blocks valid addresses (e.g., plus signs and longer TLDs); update the regex used in that test to a more permissive, widely-used pattern (for example a simple RFC-like check such as non-whitespace local-part + '@' + domain with at least one dot and no spaces) or replace it with a call to a trusted validator utility (e.g., validator.isEmail) to allow '+' and longer TLDs; ensure the change is made where the code tests values.email and still sets errors.email = "올바른 이메일 형식이 아닙니다." when the check fails.Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx-16-21 (1)
16-21:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGoogle 로그인 URL이 localhost 하드코딩이라 배포 환경에서 깨집니다.
환경변수 기반 URL을 사용해야 환경별 동작이 보장됩니다.
제안 diff
- const handleGoogleLogin = () => { - window.location.href = "http://localhost:8000/v1/auth/google/login" - - // window.location.href = import.meta.env.VITE_SERVER_API_URL+ "/v1/auth/google/login" - console.log("SERVER_API_URL:", import.meta.env.VITE_SERVER_API_URL) - - } + const handleGoogleLogin = () => { + window.location.href = `${import.meta.env.VITE_SERVER_API_URL}/v1/auth/google/login`; + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx` around lines 16 - 21, The handleGoogleLogin function currently hardcodes window.location.href to "http://localhost:8000/v1/auth/google/login"; change it to build the redirect URL from the environment variable import.meta.env.VITE_SERVER_API_URL (e.g., `${import.meta.env.VITE_SERVER_API_URL}/v1/auth/google/login`) and assign that to window.location.href so deployed environments use the configured server URL; include a sensible fallback (or throw/log) if VITE_SERVER_API_URL is missing to avoid silent failures and keep the existing console.log of the env var for debugging.Week08/hyunbin5921/misson3/src/types/auth.ts-13-21 (1)
13-21:⚠️ Potential issue | 🟠 Major | ⚡ Quick win응답 DTO의 날짜 필드는
Date가 아니라string으로 선언해야 합니다.HTTP(JSON) 응답은
Date객체로 들어오지 않아서 현재 타입 계약이 런타임 데이터와 어긋납니다.🔧 제안 수정안
export type ResponseSignupDto = CommonResponse<{ @@ - createdAt: Date; - updateAt: Date; + createdAt: string; + updateAt: string; }> @@ export type ResponseMyInfoDto = CommonResponse<{ @@ - createdAt: Date; - updateAt: Date; + createdAt: string; + updateAt: string; }>Also applies to: 37-45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/types/auth.ts` around lines 13 - 21, The response DTOs currently declare timestamp fields as Date which mismatches JSON responses; update ResponseSignupDto (and the other affected DTOs like the ones defined later around lines 37-45) to use string for createdAt and updateAt (and any other date-like fields) instead of Date so the TypeScript types reflect the JSON payload; search for CommonResponse<{ ... createdAt: Date; updateAt: Date; }> and replace those Date types with string across the file.Week08/hyunbin5921/misson3/src/types/lp.ts-14-25 (1)
14-25:⚠️ Potential issue | 🟠 Major | ⚡ Quick winLP DTO의 날짜 타입도
Date대신string으로 맞춰야 합니다.JSON 역직렬화 결과와 타입 선언이 달라 후속 처리에서 오류를 유발할 수 있습니다.
🔧 제안 수정안
export type Lp = { @@ - createdAt: Date; - updateAt: Date; + createdAt: string; + updateAt: string; @@ };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/types/lp.ts` around lines 14 - 25, The Lp type's date fields use Date which mismatches JSON deserialization; update the Lp type by changing the createdAt and updateAt properties from Date to string so they align with incoming JSON (locate the Lp type declaration and modify createdAt: Date and updateAt: Date to createdAt: string and updateAt: string).Week08/hyunbin5921/misson3/src/apis/axios.ts-54-94 (1)
54-94:⚠️ Potential issue | 🟠 Major | ⚡ Quick win토큰 갱신 실패 시 에러를 제대로 전파하지 않아 잘못된 재시도가 발생합니다.
현재
catch블록(76-85줄)이Promise.reject를 반환하지 않아,refreshPromise는undefined로 resolve됩니다. 이후 90-94줄에서newAccessToken이undefined가 되어'Bearer undefined'헤더로 재시도되고,originalRequest.headers가 존재하지 않으면originalRequest.headers.Authorization할당 시 에러가 발생합니다.두 가지를 수정하세요:
- catch 블록에서
return Promise.reject(error)추가- 91줄 앞에
originalRequest.headers = originalRequest.headers ?? {}추가수정안
if (!refreshPromise) { refreshPromise = (async () => { @@ return data.data.accessToken; })() .catch((error) => { const { removeItem: removeAccessToken } = useLocalStorage( LOCAL_STORAGE_KEY.accessToken ); const { removeItem: removeRefreshToken } = useLocalStorage( LOCAL_STORAGE_KEY.refreshToken ); removeAccessToken(); removeRefreshToken(); + return Promise.reject(error); }) .finally(() => { refreshPromise = null; }); } return refreshPromise.then((newAccessToken) => { + originalRequest.headers = originalRequest.headers ?? {}; originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return axiosInstance.request(originalRequest); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/apis/axios.ts` around lines 54 - 94, The refresh flow sets refreshPromise but its catch handler swallows errors and the then callback assumes originalRequest.headers exists; update the catch on refreshPromise (the async IIFE that uses useLocalStorage and axiosInstance.post) to rethrow the caught error by returning Promise.reject(error) so failures propagate, and in the .then handler before assigning Authorization ensure originalRequest.headers is initialized (e.g., originalRequest.headers = originalRequest.headers ?? {}) so setting originalRequest.headers.Authorization won't throw when headers is undefined.Week08/hyunbin5921/misson3/src/pages/MyPage.tsx-14-14 (1)
14-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick win사용자 응답 전문 로그 출력은 PII 노출 위험이 있습니다.
Line 14의
console.log(response)는 이메일/프로필 데이터 노출 위험이 있으니 제거하거나 비식별 정보만 로깅하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/MyPage.tsx` at line 14, The console.log(response) in MyPage.tsx exposes full user response (potential PII); remove that call or replace it with safe, non-identifying diagnostics (e.g., log only status codes, lengths, or a masked subset such as response.status or response.data?.id with email/profile fields redacted). Locate the console.log(response) statement and either delete it or change it to a minimal safe log (or gated behind a debug flag) so no raw email/profile data is printed.Week08/hyunbin5921/misson3/src/pages/LpDetailPage.tsx-14-24 (1)
14-24:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
lpid유효성 검증 없이 상세 조회가 실행됩니다.Line 24에서
numericId를 그대로 조회 훅에 전달하고 있어, 잘못된 URL 파라미터 시 불필요/잘못된 요청이 발생할 수 있습니다. 조회 호출 전에Number.isInteger(numericId) && numericId > 0조건으로 유효 ID만 조회되도록 막아주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/LpDetailPage.tsx` around lines 14 - 24, Check lpid before triggering the detail fetch: compute const numericId = Number(lpid); const isValidId = Number.isInteger(numericId) && numericId > 0; then prevent useGetLpDetail from executing for invalid IDs by either passing an enabled flag (e.g. useGetLpDetail({ lpid: numericId }, { enabled: isValidId })) or, if the hook signature doesn't support options, pass a safe value when invalid (e.g. useGetLpDetail({ lpid: isValidId ? numericId : undefined })); reference lpid, numericId and useGetLpDetail when making the change.Week08/hyunbin5921/misson3/src/pages/MyPage.tsx-11-19 (1)
11-19:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
getMyInfo()호출에 대한 예외 처리를 추가하세요.현재 코드에서
getMyInfo()가 실패하면 unhandled promise rejection이 발생합니다.try/catch로 에러를 처리하고 사용자에게 오류 메시지를 표시해야 합니다. 프로젝트의 다른 비동기 작업들(login,logout)처럼 일관된 에러 처리 패턴을 적용하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/MyPage.tsx` around lines 11 - 19, The effect's async fetch (getData) calls getMyInfo() without error handling, causing unhandled promise rejections; wrap the await getMyInfo() call inside a try/catch in the getData function, in the catch log the error (console.error) and surface a user-facing message (e.g., set an error state or call the same notification/alert helper used by login/logout) and avoid calling setData on failure; keep the rest of the useEffect/getData structure intact and follow the project's existing login/logout error-handling pattern.Week08/hyunbin5921/misson3/src/pages/HomePage.tsx-28-50 (1)
28-50:⚠️ Potential issue | 🟠 Major | ⚡ Quick win전역 wheel 차단 로직이 스크롤 UX를 심각하게 훼손합니다.
3초 동안 휠 입력을 막는 방식은 네트워크 절감보다 사용자 조작 차단 부작용이 큽니다.
fetchNextPage호출만 제어하고 입력 이벤트는 막지 않는 방향으로 바꾸는 게 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/HomePage.tsx` around lines 28 - 50, The global wheel-blocking in useEffect (handleWheel) is breaking UX; instead of preventing scrolling inside handleWheel, remove the e.preventDefault()/e.stopPropagation() and stop blocking the wheel event, and implement rate-limiting around the data fetch: create a timestamp guard (e.g., lastFetchTime) and call fetchNextPage only if Date.now() - lastFetchTime > 3000, updating lastFetchTime when you trigger fetchNextPage; keep the wheel listener in useEffect but let scrolling behave normally and only gate calls to fetchNextPage in handleWheel (or a renamed onWheelFetch) so you control network requests without blocking user input.Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeletonList.tsx-8-11 (1)
8-11:⚠️ Potential issue | 🟠 Major | ⚡ Quick win그리드 레이아웃이 깨질 수 있는 래퍼
<div>구조입니다.현재는 스켈레톤 전체가 하나의 grid item으로 들어갑니다. 리스트 컴포넌트는 Fragment를 반환해 부모 grid에 직접 아이템이 배치되게 해주세요.
수정 제안
return ( - <div>{new Array(count).fill(0).map((_,idx) => - (<LpCardSkeleton key={idx}/>))}</div> + <> + {new Array(count).fill(0).map((_, idx) => ( + <LpCardSkeleton key={idx} /> + ))} + </> )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeletonList.tsx` around lines 8 - 11, LpCardSkeletonList currently wraps all skeletons in a single <div>, causing the entire list to be treated as one grid item; change the component to return a Fragment (React.Fragment or <></>) that maps new Array(count).fill(0).map((_, idx) => <LpCardSkeleton key={idx} />) so each LpCardSkeleton becomes a direct child of the parent grid; keep the key on LpCardSkeleton and use the existing count prop to generate items.Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsx-5-5 (1)
5-5:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTailwind CSS v4에서
bg-opacity-75문법을bg-black/75로 변경하세요.
bg-opacity-*유틸리티는 v4에서 제거되었으며, 슬래시 투명도 수정자(color/opacity)가 표준 문법입니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsx` at line 5, In LpCardSkeleton.tsx update the Tailwind class on the overlay div (the element with className starting "absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 p-2") to use the v4 opacity syntax by replacing "bg-black bg-opacity-75" with "bg-black/75" inside the className string so the component's overlay uses the new color/opacity modifier.Week08/hyunbin5921/misson3/src/components/Sidebar.tsx-15-15 (1)
15-15:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTailwind v4에서
bg-opacity-50클래스는 제거되었으므로bg-black/30만 적용됩니다.
bg-black/30과bg-opacity-50을 동시에 사용 중인데,bg-opacity-50은 v4에서 작동하지 않습니다. 불투명도 값을bg-black/50형태로 통일해서 정리하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/Sidebar.tsx` at line 15, In Sidebar.tsx update the Tailwind classes on the overlay element (the className that currently includes "fixed inset-0 bg-black/30 bg-opacity-50 z-40") to remove the deprecated bg-opacity-50 and use a single opacity-aware color class such as "bg-black/50" (so the className becomes "fixed inset-0 bg-black/50 z-40"); this edits the overlay JSX in the Sidebar component to use the Tailwind v4-compliant background opacity syntax.
🟡 Minor comments (10)
Week08/hyunbin5921/misson3/src/components/Footer.tsx-11-13 (1)
11-13:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win법적/문의 링크가 실제 목적지로 연결되지 않습니다.
to="#"는 사용자 관점에서 사실상 동작하지 않는 링크입니다. 최소한 실제 라우트(/privacy,/terms,/contact) 또는 외부 URL로 연결해 주세요.예시 수정안
- <Link to="#">- Privacy Policy - </Link> - <Link to="#">- Terms of Service -</Link> - <Link to="#">- Contact -</Link> + <Link to="/privacy">- Privacy Policy -</Link> + <Link to="/terms">- Terms of Service -</Link> + <Link to="/contact">- Contact -</Link>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/Footer.tsx` around lines 11 - 13, In the Footer component replace the dummy anchor targets (currently Link to="#" instances) with real routes or URLs so the links work for users: change the three Link elements inside Footer (the Privacy Policy, Terms of Service, and Contact links) to point to actual routes like "/privacy", "/terms", and "/contact" (or external URLs if these pages live elsewhere), and ensure you keep using the react-router-dom Link component (or switch to an <a> with target="_blank" for external links) so navigation behaves correctly.Week08/hyunbin5921/misson3/src/components/CreateLpModal.tsx-89-137 (1)
89-137:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win라벨과 입력 요소 연결(
htmlFor/id)이 없어 접근성이 떨어집니다.스크린리더 사용 시 필드 맥락 전달이 약해집니다. 각
label과input/textarea를 명시적으로 연결해 주세요.예시 수정안
-<label className="text-xs text-gray-300">LP Name</label> +<label htmlFor="lp-name" className="text-xs text-gray-300">LP Name</label> <input + id="lp-name" type="text" value={lpName} onChange={(e) => setLpName(e.target.value)}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/CreateLpModal.tsx` around lines 89 - 137, In CreateLpModal.tsx, many labels (e.g., the "LP Thumbnail", "LP Name", "LP Content", "LP Tag") are not associated with their inputs which hurts accessibility; add unique id attributes to the corresponding inputs/textareas/file input (for example ids like lpThumbnailInput, lpNameInput, lpContentInput, lpTagInput) and set the matching label htmlFor to those ids, ensuring the file input handled by handleFileChange and state variables thumbnailFile, lpName, lpContent, tagInput remain wired to the same elements so behavior doesn't change while improving screen-reader accessibility.Week08/hyunbin5921/misson3/src/hooks/useLocalStorage.tsx-10-19 (1)
10-19:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
getItem의 에러 경로 반환값을null로 고정해 주세요.현재 Line 16-18 catch에서 값을 반환하지 않아
undefined가 반환됩니다. 정상 경로의null정책과 달라 호출부 상태 분기를 깨뜨릴 수 있습니다.🔧 제안 수정안
const getItem = () => { try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : null } catch (e) { console.log(e) + return null } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/hooks/useLocalStorage.tsx` around lines 10 - 19, The getItem function's catch block currently doesn't return a value causing undefined on errors; change the catch in getItem (the function using window.localStorage.getItem and JSON.parse(key) logic) to return null on error (and optionally still console.log the error) so the error path matches the normal null contract expected by callers.Week08/hyunbin5921/misson3/src/context/AuthContext.tsx-19-24 (1)
19-24:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
useAuth의 Provider 누락 방어가 현재 동작하지 않습니다.Line 19의
createContext가 기본 객체를 가지므로 Line 91의if (!context)체크는 절대 트리거되지 않습니다. Provider 누락 시 에러를 발생시키는 대신 조용히 no-op 함수들이 호출되어 버그를 숨깁니다.🔧 제안 수정안
-export const AuthContext = createContext<AuthContextType>({ - accessToken: null, - refreshToken: null, - login: async () => {}, - logout: async () => {}, -}); +export const AuthContext = createContext<AuthContextType | null>(null);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/context/AuthContext.tsx` around lines 19 - 24, The AuthContext default value hides missing Provider errors because createContext is initialized with a non-null object; change the context to be nullable by creating AuthContext with type AuthContextType | null and an initial value of null (i.e., createContext<AuthContextType | null>(null)), then ensure the consumer hook (useAuth) checks the context and throws when it is null so missing Provider raises an error; update any type annotations that expect AuthContextType to accept the nullable context in the provider lookup path.Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx-85-85 (1)
85-85:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
public리소스 경로를 상대경로로 참조하면 라우트에 따라 이미지가 깨질 수 있습니다.Vite에서는
/images/google.png같은 절대 public 경로(또는 import)를 권장합니다.제안 diff
- <img className="w-5" src="../../public/images/google.png" alt="구글 로고 이미지" /> + <img className="w-5" src="/images/google.png" alt="구글 로고 이미지" />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx` at line 85, The img in LoginPage.tsx uses a relative public path which breaks on different routes; update the <img> src to use an absolute public path (e.g. start with "/images/google.png") or import the asset at the top of the file and reference the imported identifier instead so Vite serves the image reliably across routes.Week08/hyunbin5921/misson3/src/hooks/useForm.ts-3-25 (1)
3-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
useForm제네릭 계약과 상태 업데이트 방식이 현재 구현과 맞지 않습니다.지금 구현은 사실상 “문자열 필드 폼” 전용인데
T가 제한되지 않아 타입 계약이 불명확하고,setValues/setTouched도 함수형 업데이트가 아니라 빠른 연속 입력에서 최신 상태 보장이 약해집니다.제안 diff
-interface useFormProps<T> { +interface useFormProps<T extends Record<string, string>> { initialValue: T; - //값이 올바른지 검증하는 함수. - validate: (values: T) => Record<keyof T, string>; + validate: (values: T) => Record<keyof T, string>; } -function useForm<T>({ initialValue, validate }: useFormProps<T>) { +function useForm<T extends Record<string, string>>({ + initialValue, + validate, +}: useFormProps<T>) { const [values, setValues] = useState(initialValue); - const [touched, setTouched] = useState<Record<string, boolean>>(); - - const [errors, setErrors] = useState<Record<string, string>>(); + const [touched, setTouched] = useState<Record<keyof T, boolean>>({} as Record<keyof T, boolean>); + const [errors, setErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>); // 사용자가 입력값을 바꿀 때 실행되는 함수. const handleChange = (name: keyof T, text: string) => { - setValues({ - ...values, + setValues((prev) => ({ + ...prev, [name]: text, - }); + })); }; const handleBlur = (name: keyof T) => { - setTouched({ - ...touched, + setTouched((prev) => ({ + ...prev, [name]: true, - }); + })); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/hooks/useForm.ts` around lines 3 - 25, The useForm generic should be constrained and state updates must use functional updates and correct typings: constrain T to Record<string, string> (or a suitable mapped type) so values, touched, and errors types align with initialValue; initialize touched and errors as empty objects of type Record<keyof T, boolean> and Record<keyof T, string> (or Partial variants) instead of undefined; update setValues, setTouched and setErrors to use functional updates (prev => ({ ...prev, [name]: ... })) inside handleChange and handleBlur to avoid stale state; adjust handleChange signature and usage to accept value typed as T[keyof T] or string per the chosen constraint and ensure validate is called against the latest functional-updated values.Week08/hyunbin5921/misson3/src/components/NavBar.tsx-37-43 (1)
37-43:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win아이콘 전용 버튼에 접근 가능한 이름이 필요합니다.
현재 아이콘 버튼에
aria-label이 없어 보조기기 사용자에게 기능이 전달되지 않습니다.수정 제안
<button type="button" onClick={toggle} + aria-label="사이드바 열기" className="w-8 h-8 flex items-center justify-center rounded hover:bg-gray-800" > @@ <button type="button" onClick={toggleSearch} + aria-label="검색창 토글" className="p-2 rounded-full hover:bg-gray-800" >Also applies to: 67-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/NavBar.tsx` around lines 37 - 43, The icon-only <button> elements in NavBar.tsx (the button that calls toggle and the similar button later) lack accessible names; update those buttons (the one with onClick={toggle} and the other icon-only button) to include meaningful aria-label attributes (e.g., aria-label="Open menu" / "Close menu" or "Toggle navigation") so screen readers receive the button purpose, ensuring the labels match the button action/state.Week08/hyunbin5921/misson3/src/components/Sidebar.tsx-27-32 (1)
27-32:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win닫기 버튼에
type과 접근성 라벨을 추가해 주세요.아이콘 문자(✖)만으로는 의미 전달이 약하고,
type미지정은 재사용 시 의도치 않은 submit을 유발할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/Sidebar.tsx` around lines 27 - 32, The close button in Sidebar.tsx (inside the Sidebar component) needs an explicit type and an accessibility label: change the button that calls onClose (currently rendering the ✖ icon) to include type="button" to avoid accidental form submits and add an accessible name such as aria-label="Close sidebar" (or aria-labelledby pointing to a visible label) so screen readers can convey its purpose.Week08/hyunbin5921/misson3/src/pages/HomePage.tsx-17-17 (1)
17-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win스로틀 간격과 주석이 불일치합니다.
현재 값은
3000ms인데 주석은300ms입니다. 의도대로라면 값/주석을 일치시켜야 하고, 무한스크롤 체감상 3초는 너무 깁니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/pages/HomePage.tsx` at line 17, The throttling interval for throttledScrollY is inconsistent with its comment: useThrottle(scrollY, 3000) sets 3000ms while the comment says 300ms and 3s is too long for infinite scroll; change the numeric argument in the useThrottle call (throttledScrollY) from 3000 to 300 to match the comment and improve UX, or alternatively update the comment if you intend a different interval—make the change beside the throttledScrollY definition so useThrottle(scrollY, ...) and its inline comment agree.Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsx-3-3 (1)
3-3:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
animate-purse를animate-pulse로 수정하세요.
animate-purse는 Tailwind CSS 표준 유틸리티가 아니며 프로젝트에 커스텀 정의도 없어서 스켈레톤 애니메이션이 작동하지 않습니다. 스켈레톤 로딩 효과를 위해animate-pulse를 사용해야 합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsx` at line 3, In LpCardSkeleton.tsx (component LpCardSkeleton) the div's className uses the non-existent Tailwind utility "animate-purse"; replace "animate-purse" with the correct "animate-pulse" in the className string on the outer div to enable the skeleton loading animation (i.e., update the className value where "animate-purse" appears).
🧹 Nitpick comments (2)
Week08/hyunbin5921/misson3/src/apis/auth.ts (1)
27-30: ⚡ Quick win
postLogout도 반환 타입을 명시해서 타입 계약을 일관화해 주세요.현재 함수만 타입이 비어 있어 호출부에서 추론 품질이 떨어집니다.
🔧 제안 수정안
+import type { CommonResponse } from "../types/common"; @@ -export const postLogout = async () => { - const {data} = await axiosInstance.post("/v1/auth/signout") - return data -} +export const postLogout = async (): Promise<CommonResponse<null>> => { + const { data } = await axiosInstance.post<CommonResponse<null>>("/v1/auth/signout"); + return data; +};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/apis/auth.ts` around lines 27 - 30, The postLogout function lacks an explicit return type; add a concrete Promise return type (e.g., Promise<PostLogoutResponse> or Promise<void> depending on the API contract) to the function signature (export const postLogout = async (): Promise<PostLogoutResponse> => { ... }) and define or import the PostLogoutResponse interface used across the auth API functions so callers have a consistent, explicit type contract; update any related type file or reuse an existing auth response type (e.g., AuthResponse) if one exists.Week08/hyunbin5921/misson3/src/apis/lp.ts (1)
38-42: ⚡ Quick win
deleteLike반환 타입을ResponseLikeLpDto로 명시해 타입 일관성을 맞춰 주세요.동일 도메인 API(
postLike)와 계약을 맞추면 뮤테이션 훅에서 안전하게 재사용할 수 있습니다.🔧 제안 수정안
-export const deleteLike = async ({ lpid }: RequestLpDto) => { - const { data } = await axiosInstance.delete(`v1/lps/${lpid}/likes`); +export const deleteLike = async ({ + lpid, +}: RequestLpDto): Promise<ResponseLikeLpDto> => { + const { data } = await axiosInstance.delete<ResponseLikeLpDto>(`v1/lps/${lpid}/likes`); return data; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Week08/hyunbin5921/misson3/src/apis/lp.ts` around lines 38 - 42, The deleteLike function should declare its return type as ResponseLikeLpDto to match postLike and keep type consistency; update the function signature export const deleteLike = async ({ lpid }: RequestLpDto): Promise<ResponseLikeLpDto> => and ensure the axiosInstance.delete call is typed (e.g., axiosInstance.delete<ResponseLikeLpDto>(...)) so the returned data conforms to ResponseLikeLpDto and the mutation hooks can safely reuse it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cf851653-b1f2-477c-8b2d-e475c9809996
⛔ Files ignored due to path filters (9)
Week08/hyunbin5921/misson3/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlWeek08/hyunbin5921/misson3/public/images/gar.pngis excluded by!**/*.pngWeek08/hyunbin5921/misson3/public/images/google.pngis excluded by!**/*.pngWeek08/hyunbin5921/misson3/public/images/hamburger.svgis excluded by!**/*.svgWeek08/hyunbin5921/misson3/public/images/hamburger1.pngis excluded by!**/*.pngWeek08/hyunbin5921/misson3/public/images/look.pngis excluded by!**/*.pngWeek08/hyunbin5921/misson3/public/images/mod.pngis excluded by!**/*.pngWeek08/hyunbin5921/misson3/public/vite.svgis excluded by!**/*.svgWeek08/hyunbin5921/misson3/src/assets/react.svgis excluded by!**/*.svg
📒 Files selected for processing (53)
Week08/hyunbin5921/misson3/.gitignoreWeek08/hyunbin5921/misson3/README.mdWeek08/hyunbin5921/misson3/eslint.config.jsWeek08/hyunbin5921/misson3/index.htmlWeek08/hyunbin5921/misson3/package.jsonWeek08/hyunbin5921/misson3/src/App.cssWeek08/hyunbin5921/misson3/src/App.tsxWeek08/hyunbin5921/misson3/src/apis/auth.tsWeek08/hyunbin5921/misson3/src/apis/axios.tsWeek08/hyunbin5921/misson3/src/apis/lp.tsWeek08/hyunbin5921/misson3/src/components/CreateLpModal.tsxWeek08/hyunbin5921/misson3/src/components/Footer.tsxWeek08/hyunbin5921/misson3/src/components/LpCard/LpCard.tsxWeek08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsxWeek08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeletonList.tsxWeek08/hyunbin5921/misson3/src/components/NavBar.tsxWeek08/hyunbin5921/misson3/src/components/Sidebar.tsxWeek08/hyunbin5921/misson3/src/constants/delay.tsWeek08/hyunbin5921/misson3/src/constants/key.tsWeek08/hyunbin5921/misson3/src/context/AuthContext.tsxWeek08/hyunbin5921/misson3/src/context/SearchContext.tsxWeek08/hyunbin5921/misson3/src/hooks/mutations/useDeleteLike.tsWeek08/hyunbin5921/misson3/src/hooks/mutations/usePostLike.tsWeek08/hyunbin5921/misson3/src/hooks/queries/useGetInfiniteLpList.tsWeek08/hyunbin5921/misson3/src/hooks/queries/useGetLpDetail.tsWeek08/hyunbin5921/misson3/src/hooks/queries/useGetLpList.tsWeek08/hyunbin5921/misson3/src/hooks/queries/useGetMyInfo.tsWeek08/hyunbin5921/misson3/src/hooks/useDebounce.tsWeek08/hyunbin5921/misson3/src/hooks/useForm.tsWeek08/hyunbin5921/misson3/src/hooks/useLocalStorage.tsxWeek08/hyunbin5921/misson3/src/hooks/useSidebar.tsWeek08/hyunbin5921/misson3/src/hooks/useThrottle.tsWeek08/hyunbin5921/misson3/src/index.cssWeek08/hyunbin5921/misson3/src/layouts/HomeLayout.tsxWeek08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsxWeek08/hyunbin5921/misson3/src/main.tsxWeek08/hyunbin5921/misson3/src/pages/GoogleLoginRedirectPage.tsxWeek08/hyunbin5921/misson3/src/pages/HomePage.tsxWeek08/hyunbin5921/misson3/src/pages/LoginPage.tsxWeek08/hyunbin5921/misson3/src/pages/LpDetailPage.tsxWeek08/hyunbin5921/misson3/src/pages/MyPage.tsxWeek08/hyunbin5921/misson3/src/pages/NotFoundPage.tsxWeek08/hyunbin5921/misson3/src/pages/SignupPage.tsxWeek08/hyunbin5921/misson3/src/pages/ThrottlePage.tsxWeek08/hyunbin5921/misson3/src/types/auth.tsWeek08/hyunbin5921/misson3/src/types/common.tsWeek08/hyunbin5921/misson3/src/types/lp.tsWeek08/hyunbin5921/misson3/src/utils/validate.tsWeek08/hyunbin5921/misson3/src/vite-env.d.tsWeek08/hyunbin5921/misson3/tsconfig.app.jsonWeek08/hyunbin5921/misson3/tsconfig.jsonWeek08/hyunbin5921/misson3/tsconfig.node.jsonWeek08/hyunbin5921/misson3/vite.config.ts
| <span>❤</span> | ||
| <span>{}</span> | ||
| </div> |
There was a problem hiding this comment.
빈 JSX 표현식으로 컴파일이 실패합니다.
Line 48의 {}는 유효한 표현식이 아니어서 빌드가 깨집니다. 값이 준비되지 않았다면 해당 노드를 제거하거나 명시적인 값을 넣어야 합니다.
수정 제안
<div className="flex items-center gap-1">
<span>❤</span>
- <span>{}</span>
+ {/* 좋아요 수 바인딩 전 임시 처리 */}
+ <span>0</span>
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span>❤</span> | |
| <span>{}</span> | |
| </div> | |
| <span>❤</span> | |
| {/* 좋아요 수 바인딩 전 임시 처리 */} | |
| <span>0</span> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/components/LpCard/LpCard.tsx` around lines 47
- 49, In LpCard component update the empty JSX expression in the second <span>
(the "<span>{}</span>" in the render) — remove the empty "{}" or replace it with
a valid value (e.g., a prop/state like likes, likeCount, or a fallback such as 0
or an empty string) so the JSX compiles; locate the empty expression in the
LpCard render return and either delete the whole <span> or bind it to the
intended variable (e.g., props.likes or state.likeCount) with an explicit
fallback.
| import useGetMyInfo from "../hooks/queries/useGetMyInfo"; | ||
| import { useSearch as useSearchContext } from "../context/SearchContext"; | ||
|
|
||
| import Sidebar from "./SideBar"; |
There was a problem hiding this comment.
대소문자 불일치 import로 빌드가 깨질 수 있습니다.
Line 9의 ./SideBar는 실제 파일명 Sidebar.tsx와 케이스가 달라서, Linux 환경에서 모듈을 찾지 못할 수 있습니다.
수정 제안
-import Sidebar from "./SideBar";
+import Sidebar from "./Sidebar";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/components/NavBar.tsx` at line 9, The import
in NavBar.tsx uses the wrong case ("./SideBar") which will fail on
case-sensitive filesystems; update the import to exactly match the actual
filename (Sidebar.tsx) so the Sidebar component import in NavBar.tsx resolves
correctly (ensure the import string matches the "Sidebar" filename and symbol).
| const newLpPost = { ...previousLpPost }; | ||
|
|
||
| // 게시글에 저장된 좋아요 목록에서 현재 내가 눌렀던 좋아요의 위치 찾기 | ||
| const me = queryClient.getQueryData<ResponseMyInfoDto>([ | ||
| QUERY_KEY.myInfo, | ||
| ]); | ||
| const userId = Number(me?.data.id); | ||
|
|
||
| const likedIndex = | ||
| previousLpPost?.data.likes.findIndex( | ||
| (like) => like.userId === userId | ||
| ) ?? -1; | ||
|
|
||
| if (likedIndex >= 0) { | ||
| previousLpPost?.data.likes.splice(likedIndex, 1); | ||
| } else { | ||
| const newLike = { userId, lpId: lp.lpid } as Likes; | ||
| previousLpPost?.data.likes.push(newLike); | ||
| } | ||
|
|
||
| // 업데이트된 게시글 캐시에 저장. -> 바로 UI 업뎃 | ||
| queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost); | ||
|
|
There was a problem hiding this comment.
낙관적 업데이트에서 원본 캐시 데이터가 직접 변경됨 (rollback 불가)
{ ...previousLpPost }는 shallow copy이므로 previousLpPost.data.likes와 newLpPost.data.likes가 동일한 배열을 참조합니다. Line 38-42에서 splice/push로 배열을 직접 수정하면 롤백용 원본 데이터도 함께 변경되어, onError에서 복구가 불가능합니다.
Deep copy를 사용하여 새 객체를 생성해야 합니다.
🐛 제안 수정
- // 게시글 데이터 복사 -> NewLpPost에 새로운 객체 생성
- const newLpPost = { ...previousLpPost };
-
- // 게시글에 저장된 좋아요 목록에서 현재 내가 눌렀던 좋아요의 위치 찾기
- const me = queryClient.getQueryData<ResponseMyInfoDto>([
- QUERY_KEY.myInfo,
- ]);
- const userId = Number(me?.data.id);
-
- const likedIndex =
- previousLpPost?.data.likes.findIndex(
- (like) => like.userId === userId
- ) ?? -1;
-
- if (likedIndex >= 0) {
- previousLpPost?.data.likes.splice(likedIndex, 1);
- } else {
- const newLike = { userId, lpId: lp.lpid } as Likes;
- previousLpPost?.data.likes.push(newLike);
- }
-
- // 업데이트된 게시글 캐시에 저장. -> 바로 UI 업뎃
- queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost);
+ const me = queryClient.getQueryData<ResponseMyInfoDto>([
+ QUERY_KEY.myInfo,
+ ]);
+ const userId = Number(me?.data.id);
+
+ // Deep copy로 새 객체 생성
+ const newLpPost = previousLpPost
+ ? {
+ ...previousLpPost,
+ data: {
+ ...previousLpPost.data,
+ likes: previousLpPost.data.likes.filter(
+ (like) => like.userId !== userId
+ ),
+ },
+ }
+ : previousLpPost;
+
+ queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/hooks/mutations/useDeleteLike.ts` around lines
24 - 46, The optimistic update mutates the original cache because newLpPost is
only a shallow copy and previousLpPost.data.likes is modified in-place; instead
create a deep copy of the post and its nested likes array before modifying:
build a newLpPost that copies previousLpPost and previousLpPost.data (e.g., new
data object) and sets data.likes to a new array (use filter to remove or a new
array with the new like), then call queryClient.setQueryData([QUERY_KEY.lps,
lp.lpid], newLpPost); avoid using splice/push on previousLpPost.data.likes so
rollback in onError can restore the original previousLpPost.
| onError: (err, newLp, context) => { | ||
| console.log(err, newLp); | ||
| queryClient.setQueryData( | ||
| [QUERY_KEY.lps, newLp.lpid], | ||
| context?.previousLpPost?.data.id | ||
| ); | ||
| }, |
There was a problem hiding this comment.
onError 롤백 로직이 잘못됨 — 캐시 복구 실패
context?.previousLpPost?.data.id는 숫자(ID 값)를 반환합니다. 캐시를 복구하려면 전체 객체인 context?.previousLpPost를 설정해야 합니다. 현재 코드는 에러 발생 시 캐시에 숫자값이 저장되어 앱이 정상 동작하지 않습니다.
🐛 제안 수정
onError: (err, newLp, context) => {
console.log(err, newLp);
queryClient.setQueryData(
[QUERY_KEY.lps, newLp.lpid],
- context?.previousLpPost?.data.id
+ context?.previousLpPost
);
},📝 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.
| onError: (err, newLp, context) => { | |
| console.log(err, newLp); | |
| queryClient.setQueryData( | |
| [QUERY_KEY.lps, newLp.lpid], | |
| context?.previousLpPost?.data.id | |
| ); | |
| }, | |
| onError: (err, newLp, context) => { | |
| console.log(err, newLp); | |
| queryClient.setQueryData( | |
| [QUERY_KEY.lps, newLp.lpid], | |
| context?.previousLpPost | |
| ); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/hooks/mutations/useDeleteLike.ts` around lines
49 - 55, The onError rollback in useDeleteLike.ts is restoring the wrong value
into the cache: queryClient.setQueryData([QUERY_KEY.lps, newLp.lpid],
context?.previousLpPost?.data.id) writes a numeric id instead of the full LP
object; change the rollback to re-set the entire previous snapshot
(context?.previousLpPost) into the cache for the key [QUERY_KEY.lps, newLp.lpid]
so the cache contains the original object shape (update the onError handler that
references newLp, context, and QUERY_KEY.lps accordingly).
| const newLpPost = { ...previousLpPost }; | ||
|
|
||
| // 게시글에 저장된 좋아요 목록에서 현재 내가 눌렀던 좋아요의 위치 찾기 | ||
| const me = queryClient.getQueryData<ResponseMyInfoDto>([ | ||
| QUERY_KEY.myInfo, | ||
| ]); | ||
| const userId = Number(me?.data.id); | ||
|
|
||
| const likedIndex = | ||
| previousLpPost?.data.likes.findIndex( | ||
| (like) => like.userId === userId | ||
| ) ?? -1; | ||
|
|
||
| if (likedIndex >= 0) { | ||
| previousLpPost?.data.likes.splice(likedIndex, 1); | ||
| } else { | ||
| const newLike = { userId, lpId: lp.lpid } as Likes; | ||
| previousLpPost?.data.likes.push(newLike); | ||
| } | ||
|
|
||
| // 업데이트된 게시글 캐시에 저장. -> 바로 UI 업뎃 | ||
| queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost); |
There was a problem hiding this comment.
낙관적 업데이트에서 원본 캐시 데이터가 직접 변경됨 (rollback 불가)
useDeleteLike와 동일한 문제입니다. { ...previousLpPost }는 shallow copy이므로 likes 배열을 splice/push로 수정하면 원본 데이터도 변경됩니다. Deep copy를 사용해야 합니다.
🐛 제안 수정
- // 게시글 데이터 복사 -> NewLpPost에 새로운 객체 생성
- const newLpPost = { ...previousLpPost };
-
- // 게시글에 저장된 좋아요 목록에서 현재 내가 눌렀던 좋아요의 위치 찾기
- const me = queryClient.getQueryData<ResponseMyInfoDto>([
- QUERY_KEY.myInfo,
- ]);
- const userId = Number(me?.data.id);
-
- const likedIndex =
- previousLpPost?.data.likes.findIndex(
- (like) => like.userId === userId
- ) ?? -1;
-
- if (likedIndex >= 0) {
- previousLpPost?.data.likes.splice(likedIndex, 1);
- } else {
- const newLike = { userId, lpId: lp.lpid } as Likes;
- previousLpPost?.data.likes.push(newLike);
- }
-
- // 업데이트된 게시글 캐시에 저장. -> 바로 UI 업뎃
- queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost);
+ const me = queryClient.getQueryData<ResponseMyInfoDto>([
+ QUERY_KEY.myInfo,
+ ]);
+ const userId = Number(me?.data.id);
+
+ // Deep copy로 새 객체 생성 및 좋아요 추가
+ const newLpPost = previousLpPost
+ ? {
+ ...previousLpPost,
+ data: {
+ ...previousLpPost.data,
+ likes: [
+ ...previousLpPost.data.likes,
+ { userId, lpId: lp.lpid } as Likes,
+ ],
+ },
+ }
+ : previousLpPost;
+
+ queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], newLpPost);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/hooks/mutations/usePostLike.ts` around lines
25 - 46, The optimistic-update mutates the original cache because newLpPost is a
shallow copy and likes is modified in place; in usePostLike, deep-clone
previousLpPost (e.g., structuredClone or a deep clone utility) before
splicing/pushing the likes array, update the cloned object, and pass that clone
to queryClient.setQueryData([QUERY_KEY.lps, lp.lpid], clonedPost) so the
original cache remains unchanged and rollback via the saved previousLpPost still
works.
| onError: (err, newLp, context) => { | ||
| console.log(err, newLp); | ||
| queryClient.setQueryData( | ||
| [QUERY_KEY.lps, newLp.lpid], | ||
| context?.previousLpPost?.data.id | ||
| ); |
There was a problem hiding this comment.
onError 롤백 로직이 잘못됨 — 캐시 복구 실패
useDeleteLike와 동일한 버그입니다. context?.previousLpPost?.data.id는 숫자를 반환하므로, 전체 객체인 context?.previousLpPost를 설정해야 합니다.
🐛 제안 수정
onError: (err, newLp, context) => {
console.log(err, newLp);
queryClient.setQueryData(
[QUERY_KEY.lps, newLp.lpid],
- context?.previousLpPost?.data.id
+ context?.previousLpPost
);
},📝 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.
| onError: (err, newLp, context) => { | |
| console.log(err, newLp); | |
| queryClient.setQueryData( | |
| [QUERY_KEY.lps, newLp.lpid], | |
| context?.previousLpPost?.data.id | |
| ); | |
| onError: (err, newLp, context) => { | |
| console.log(err, newLp); | |
| queryClient.setQueryData( | |
| [QUERY_KEY.lps, newLp.lpid], | |
| context?.previousLpPost | |
| ); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/hooks/mutations/usePostLike.ts` around lines
50 - 55, The onError rollback in usePostLike is restoring the wrong value — it
currently writes context?.previousLpPost?.data.id (a number) back into the
cache; change the rollback to restore the full previous object by calling
queryClient.setQueryData([QUERY_KEY.lps, newLp.lpid], context?.previousLpPost)
so the cache receives the original object shape; update the onError handler in
usePostLike to use context?.previousLpPost (not .data.id) and keep the same
query key ([QUERY_KEY.lps, newLp.lpid]) to locate the cached entry.
| }); | ||
|
|
||
| }; |
There was a problem hiding this comment.
구문 오류 — 불필요한 };
Line 63에서 useMutation 호출이 완료되었으나, Line 65에 불필요한 };가 있습니다. 이 코드는 트랜스파일러나 린터에서 에러를 발생시킬 수 있습니다.
🐛 제안 수정
});
-
- };
+}📝 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.
| }); | |
| }; | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/hooks/mutations/usePostLike.ts` around lines
63 - 65, The file has a stray closing token after the mutation definition:
remove the extraneous "};" that follows the useMutation block in usePostLike
(the extra brace/semicolon after the useMutation call) so the function exports
cleanly; ensure only the correct closing braces for the useMutation callback and
the usePostLike function remain.
| <img src={data?.data?.avatar as string} alt="프로필 이미지" /> | ||
| <h1>{data?.data?.email}</h1> | ||
|
|
||
| <button className='cursor-pointer bg-blue-300 rounded-sm p-3 hover:scale-95'onClick={handleLogout}>로그아웃</button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "MyPage.tsx" -type fRepository: HSU-Makeus-Challenge-10th/Web
Length of output: 874
🏁 Script executed:
cat -n ./Week08/hyunbin5921/misson3/src/pages/MyPage.tsx | sed -n '25,35p'Repository: HSU-Makeus-Challenge-10th/Web
Length of output: 456
JSX 속성 구분 공백 누락으로 문법 오류가 발생합니다.
Line 31에서 className 속성의 종료 따옴표와 onClick 속성 사이에 공백이 없어 JSX 파싱 오류가 발생합니다.
수정 예시
- <button className='cursor-pointer bg-blue-300 rounded-sm p-3 hover:scale-95'onClick={handleLogout}>로그아웃</button>
+ <button className='cursor-pointer bg-blue-300 rounded-sm p-3 hover:scale-95' onClick={handleLogout}>로그아웃</button>📝 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.
| <button className='cursor-pointer bg-blue-300 rounded-sm p-3 hover:scale-95'onClick={handleLogout}>로그아웃</button> | |
| <button className='cursor-pointer bg-blue-300 rounded-sm p-3 hover:scale-95' onClick={handleLogout}>로그아웃</button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Week08/hyunbin5921/misson3/src/pages/MyPage.tsx` at line 31, JSX 구문 오류는
<button> 엘리먼트의 className 속성 끝따옴표와 onClick 속성 사이에 공백이 빠져 발생합니다; MyPage.tsx에서 해당
<button> (className='...')을 찾아 className의 끝따옴표 뒤에 공백을 추가하여
onClick={handleLogout}과 분리하고 JSX 속성 구분 규칙을 준수하도록 수정하세요 (참조: className, onClick,
handleLogout).
📝 미션 번호
8주차 Misson 1, 2, 3
📋 구현 사항
📎 스크린샷
2025-11-24.190129.mp4
2025-11-24.194205.mp4
2025-11-24.194205.mp4
✅ 체크리스트
🤔 질문 사항
없음
Summary by CodeRabbit
릴리스 노트
New Features
Chores