Skip to content

feat: 8주차 미션 완료#70

Open
guieuna3-lab wants to merge 1 commit into
mainfrom
hyunbin5921/week08
Open

feat: 8주차 미션 완료#70
guieuna3-lab wants to merge 1 commit into
mainfrom
hyunbin5921/week08

Conversation

@guieuna3-lab
Copy link
Copy Markdown
Contributor

@guieuna3-lab guieuna3-lab commented May 19, 2026

📝 미션 번호

8주차 Misson 1, 2, 3

📋 구현 사항

  • 디바운스, 트로틀링으로 서버와 클라이언트의 네트워크 비용 저하
  • 사이드 바 로직 업데이트로 인한 사용자 UI 개선

📎 스크린샷

2025-11-24.190129.mp4
2025-11-24.194205.mp4
2025-11-24.194205.mp4

✅ 체크리스트

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

🤔 질문 사항

없음

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 사용자 인증 기능 (회원가입, 로그인, 로그아웃)
    • 구글 로그인 통합
    • LP 목록 조회 및 상세 페이지
    • 무한 스크롤 기반 게시물 로딩
    • 게시물 좋아요 기능
    • 검색 및 정렬 기능
    • 사용자 프로필 페이지
  • Chores

    • React + TypeScript + Vite 기반 프로젝트 초기 설정
    • ESLint, Tailwind CSS 개발 환경 구성

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

개요

이 PR은 Week08 미션 3을 위해 React + TypeScript + Vite 스택 기반의 완전한 프론트엔드 애플리케이션을 처음부터 구축합니다. 인증 시스템, 무한 스크롤 LP 피드, 좋아요 기능, 사용자 프로필 관리를 포함하는 종합적인 애플리케이션입니다.

변경 사항

애플리케이션 초기화 및 인프라 구축

레이어 / 파일 요약
프로젝트 설정
.gitignore, README.md, package.json, tsconfig.*, vite.config.ts, eslint.config.js, index.html, src/index.css, src/main.tsx
Vite + React + TypeScript + Tailwind CSS + ESLint 구성을 완성하고, HTML 엔트리포인트 및 패키지 의존성을 정의합니다.
타입 정의 및 API 계층
src/types/common.ts, src/types/auth.ts, src/types/lp.ts, src/apis/axios.ts, src/apis/auth.ts, src/apis/lp.ts
CommonResponse/CursorBasedResponse 제네릭, 인증/LP 도메인 DTO를 정의하고, axiosInstance 토큰 주입/401 갱신 로직 및 API 래퍼 함수를 구현합니다.
인증 상태 관리 및 저장소
src/context/AuthContext.tsx, src/hooks/useLocalStorage.tsx, src/constants/key.ts, src/constants/delay.ts
AuthProvider/useAuth로 로그인/로그아웃을 제어하고, useLocalStorage로 토큰을 브라우저에 유지하며, 쿼리/저장소 키를 상수로 관리합니다.
검색 상태 및 디바운싱
src/context/SearchContext.tsx, src/hooks/useDebounce.ts
SearchProvider로 검색어와 디바운스된 값을 전역 상태로 공유합니다.
커스텀 훅 및 유틸
src/hooks/useForm.ts, src/hooks/useSidebar.ts, src/hooks/useThrottle.ts, src/utils/validate.ts
useForm으로 폼 상태/검증을, useSidebar로 모바일 네비게이션 토글을, useThrottle로 스크롤 이벤트를 제어하고, 로그인 검증 규칙을 정의합니다.
React Query 데이터 페칭
src/hooks/queries/useGetMyInfo.ts, src/hooks/queries/useGetLpList.ts, src/hooks/queries/useGetLpDetail.ts, src/hooks/queries/useGetInfiniteLpList.ts
사용자 정보, LP 목록(페이징/무한 스크롤), LP 상세를 React Query로 조회합니다.
React Query 뮤테이션
src/hooks/mutations/usePostLike.ts, src/hooks/mutations/useDeleteLike.ts
좋아요 추가/삭제를 낙관적 업데이트와 캐시 무효화로 처리합니다.
레이아웃 및 라우팅
src/layouts/HomeLayout.tsx, src/layouts/ProtectedLayout.tsx
HomeLayout은 공개 페이지용으로, ProtectedLayout은 인증 토큰 유무에 따라 접근을 제어합니다.
네비게이션 및 UI 컴포넌트
src/components/NavBar.tsx, src/components/Sidebar.tsx, src/components/Footer.tsx, src/components/CreateLpModal.tsx
NavBar는 검색/사용자 메뉴를, Sidebar는 모바일 네비게이션을, Footer는 법적 링크를, CreateLpModal은 LP 작성 인터페이스를 제공합니다.
카드 및 스켈레톤
src/components/LpCard/LpCard.tsx, src/components/LpCard/LpCardSkeleton.tsx, src/components/LpCard/LpCardSkeletonList.tsx
LpCard는 목록 아이템 렌더링을, 스켈레톤은 로딩 상태 시각화를 담당합니다.
페이지: 홈 및 무한 스크롤
src/pages/HomePage.tsx
무한 스크롤 기반 LP 피드를 구현하고, 휠 이벤트 스로틀링 및 하단 감지로 자동 페이징을 제어합니다.
페이지: 인증
src/pages/LoginPage.tsx, src/pages/SignupPage.tsx
로그인 폼(구글 OAuth 포함), 회원가입 폼(Zod 검증)을 구현합니다.
페이지: 상세 및 프로필
src/pages/LpDetailPage.tsx, src/pages/MyPage.tsx
LP 상세 페이지에서 좋아요 토글을 처리하고, MyPage에서 사용자 정보를 표시합니다.
페이지: 특수 용도
src/pages/GoogleLoginRedirectPage.tsx, src/pages/ThrottlePage.tsx, src/pages/NotFoundPage.tsx
OAuth 콜백 처리, 스로틀 데모, 404 페이지를 제공합니다.
앱 통합 및 라우팅
src/App.tsx, src/vite-env.d.ts
React Router, React Query, AuthContext, SearchProvider를 조합하여 public/protected 라우트를 통합합니다.

예상 코드 리뷰 난이도

🎯 4 (복잡함) | ⏱️ ~75분

이 PR은 다음 이유로 상당한 리뷰 노력을 요구합니다:

  • 범위: 80개 이상 파일에 걸친 완전한 애플리케이션 초기화
  • 복잡성: Axios 401 갱신 로직(refreshPromise 동시화), React Query 낙관적 업데이트, 무한 스크롤 구현
  • 다양성: 인증, 라우팅, 폼 검증, 상태 관리, UI 컴포넌트, 페이징 등 여러 패턴이 혼재
  • 상호 의존성: 각 레이어가 이전 레이어에 의존하므로 순차적 검토 필요

💡 리뷰 제안: 레이어 순서대로 진행하되, API 인터셉터 로직(특히 401 재시도)과 React Query 캐시 전략에 특별한 주의를 기울이세요.

관련 PR

제안 리뷰어

  • wantkdd

🐰 아, 얼마나 멋진 여행이었던가!
React에 TypeScript를 곁들이고,
무한 스크롤과 좋아요 기능을 더하니—
완성된 애플리케이션의 탄생이로다!
🚀✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PR 제목이 "feat: 8주차 미션 완료"로 작성되었으나, 저장소 템플릿에서 요구하는 "Create Week{주차} Mission{번호}" 형식(예: Create Week8 Mission1)과 다릅니다. PR 제목을 "feat: Create Week8 Mission 1, 2, 3" 또는 "feat: Week8 Mission 1, 2, 3 완료" 형식으로 변경하여 템플릿 가이드라인을 따르도록 권장합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 대부분의 필수 섹션(미션 번호, 구현 사항, 스크린샷, 체크리스트)을 포함하고 있으며 실제 구현 내용을 잘 설명하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 lift

refresh 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의 커스텀 컴포넌트를 사용하는 것이어야 하며, 이 컴포넌트는 isOpenonClose props를 필수로 요구합니다.

🔧 수정 사항
-import { Sidebar } from "lucide-react";
+import Sidebar from "../components/Sidebar";

그리고 Line 16의 <Sidebar /> 호출 시 상태 관리를 위해 isOpenonClose props를 전달해야 합니다:

-<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

Sidebar import 대상이 잘못되어 실제 사이드바 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-reactSidebar 아이콘을 import하여 Line 22에서 렌더링하고 있으나, 실제 네비게이션 사이드바 컴포넌트(src/components/Sidebar.tsx)가 이미 존재합니다. 올바른 컴포넌트를 import하여 isOpenonClose props와 함께 사용해야 합니다.

수정 예시
- 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 win

Google 로그인 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 win

LP 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를 반환하지 않아, refreshPromiseundefined로 resolve됩니다. 이후 90-94줄에서 newAccessTokenundefined가 되어 'Bearer undefined' 헤더로 재시도되고, originalRequest.headers가 존재하지 않으면 originalRequest.headers.Authorization 할당 시 에러가 발생합니다.

두 가지를 수정하세요:

  1. catch 블록에서 return Promise.reject(error) 추가
  2. 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 win

Tailwind 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 win

Tailwind v4에서 bg-opacity-50 클래스는 제거되었으므로 bg-black/30만 적용됩니다.

bg-black/30bg-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)이 없어 접근성이 떨어집니다.

스크린리더 사용 시 필드 맥락 전달이 약해집니다. 각 labelinput/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-purseanimate-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

📥 Commits

Reviewing files that changed from the base of the PR and between b47368a and c9441ad.

⛔ Files ignored due to path filters (9)
  • Week08/hyunbin5921/misson3/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week08/hyunbin5921/misson3/public/images/gar.png is excluded by !**/*.png
  • Week08/hyunbin5921/misson3/public/images/google.png is excluded by !**/*.png
  • Week08/hyunbin5921/misson3/public/images/hamburger.svg is excluded by !**/*.svg
  • Week08/hyunbin5921/misson3/public/images/hamburger1.png is excluded by !**/*.png
  • Week08/hyunbin5921/misson3/public/images/look.png is excluded by !**/*.png
  • Week08/hyunbin5921/misson3/public/images/mod.png is excluded by !**/*.png
  • Week08/hyunbin5921/misson3/public/vite.svg is excluded by !**/*.svg
  • Week08/hyunbin5921/misson3/src/assets/react.svg is excluded by !**/*.svg
📒 Files selected for processing (53)
  • Week08/hyunbin5921/misson3/.gitignore
  • Week08/hyunbin5921/misson3/README.md
  • Week08/hyunbin5921/misson3/eslint.config.js
  • Week08/hyunbin5921/misson3/index.html
  • Week08/hyunbin5921/misson3/package.json
  • Week08/hyunbin5921/misson3/src/App.css
  • Week08/hyunbin5921/misson3/src/App.tsx
  • Week08/hyunbin5921/misson3/src/apis/auth.ts
  • Week08/hyunbin5921/misson3/src/apis/axios.ts
  • Week08/hyunbin5921/misson3/src/apis/lp.ts
  • Week08/hyunbin5921/misson3/src/components/CreateLpModal.tsx
  • Week08/hyunbin5921/misson3/src/components/Footer.tsx
  • Week08/hyunbin5921/misson3/src/components/LpCard/LpCard.tsx
  • Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeleton.tsx
  • Week08/hyunbin5921/misson3/src/components/LpCard/LpCardSkeletonList.tsx
  • Week08/hyunbin5921/misson3/src/components/NavBar.tsx
  • Week08/hyunbin5921/misson3/src/components/Sidebar.tsx
  • Week08/hyunbin5921/misson3/src/constants/delay.ts
  • Week08/hyunbin5921/misson3/src/constants/key.ts
  • Week08/hyunbin5921/misson3/src/context/AuthContext.tsx
  • Week08/hyunbin5921/misson3/src/context/SearchContext.tsx
  • Week08/hyunbin5921/misson3/src/hooks/mutations/useDeleteLike.ts
  • Week08/hyunbin5921/misson3/src/hooks/mutations/usePostLike.ts
  • Week08/hyunbin5921/misson3/src/hooks/queries/useGetInfiniteLpList.ts
  • Week08/hyunbin5921/misson3/src/hooks/queries/useGetLpDetail.ts
  • Week08/hyunbin5921/misson3/src/hooks/queries/useGetLpList.ts
  • Week08/hyunbin5921/misson3/src/hooks/queries/useGetMyInfo.ts
  • Week08/hyunbin5921/misson3/src/hooks/useDebounce.ts
  • Week08/hyunbin5921/misson3/src/hooks/useForm.ts
  • Week08/hyunbin5921/misson3/src/hooks/useLocalStorage.tsx
  • Week08/hyunbin5921/misson3/src/hooks/useSidebar.ts
  • Week08/hyunbin5921/misson3/src/hooks/useThrottle.ts
  • Week08/hyunbin5921/misson3/src/index.css
  • Week08/hyunbin5921/misson3/src/layouts/HomeLayout.tsx
  • Week08/hyunbin5921/misson3/src/layouts/ProtectedLayout.tsx
  • Week08/hyunbin5921/misson3/src/main.tsx
  • Week08/hyunbin5921/misson3/src/pages/GoogleLoginRedirectPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/HomePage.tsx
  • Week08/hyunbin5921/misson3/src/pages/LoginPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/LpDetailPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/MyPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/NotFoundPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/SignupPage.tsx
  • Week08/hyunbin5921/misson3/src/pages/ThrottlePage.tsx
  • Week08/hyunbin5921/misson3/src/types/auth.ts
  • Week08/hyunbin5921/misson3/src/types/common.ts
  • Week08/hyunbin5921/misson3/src/types/lp.ts
  • Week08/hyunbin5921/misson3/src/utils/validate.ts
  • Week08/hyunbin5921/misson3/src/vite-env.d.ts
  • Week08/hyunbin5921/misson3/tsconfig.app.json
  • Week08/hyunbin5921/misson3/tsconfig.json
  • Week08/hyunbin5921/misson3/tsconfig.node.json
  • Week08/hyunbin5921/misson3/vite.config.ts

Comment on lines +47 to +49
<span>❤</span>
<span>{}</span>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

빈 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.

Suggested change
<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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

대소문자 불일치 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).

Comment on lines +24 to +46
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

낙관적 업데이트에서 원본 캐시 데이터가 직접 변경됨 (rollback 불가)

{ ...previousLpPost }는 shallow copy이므로 previousLpPost.data.likesnewLpPost.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.

Comment on lines +49 to +55
onError: (err, newLp, context) => {
console.log(err, newLp);
queryClient.setQueryData(
[QUERY_KEY.lps, newLp.lpid],
context?.previousLpPost?.data.id
);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Suggested change
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).

Comment on lines +25 to +46
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

낙관적 업데이트에서 원본 캐시 데이터가 직접 변경됨 (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.

Comment on lines +50 to +55
onError: (err, newLp, context) => {
console.log(err, newLp);
queryClient.setQueryData(
[QUERY_KEY.lps, newLp.lpid],
context?.previousLpPost?.data.id
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +63 to +65
});

};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

구문 오류 — 불필요한 };

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.

Suggested change
});
};
});
}
🤖 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "MyPage.tsx" -type f

Repository: 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.

Suggested change
<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).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant