Skip to content

Create Week08 Mission01, 02, 03#72

Open
librelldd wants to merge 1 commit into
mainfrom
Week08
Open

Create Week08 Mission01, 02, 03#72
librelldd wants to merge 1 commit into
mainfrom
Week08

Conversation

@librelldd
Copy link
Copy Markdown
Contributor

@librelldd librelldd commented May 23, 2026

📝 미션 번호

8주차 미션01,02,03

📋 구현 사항

useDebounce 구현 및 적용
Generic을 사용한 useDebounce(value, delay) 훅 직접 구현
검색 입력에 적용하여 300ms 지연된 debouncedQuery 생성
빈 문자열 및 공백 입력 시 enabled 옵션으로 API 요청 차단

📎 스크린샷

Week08_mission01.mp4
Week08_mission02_e.mp4
Week08_mission03.mp4

✅ 체크리스트

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

🤔 질문 사항

Summary by CodeRabbit

릴리스 노트

  • 새 기능
    • 사용자 인증 시스템 (회원가입, 로그인, 로그아웃)
    • 사용자 프로필 관리 및 수정
    • LP 작성, 조회, 수정, 삭제 기능
    • 댓글 작성, 수정, 삭제 기능
    • 좋아요 기능
    • 검색 및 필터링 기능
    • 다크 모드 지원
    • 반응형 사이드바 네비게이션

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

📝 Walkthrough

작업 내용

이 PR은 Week08 mission01에서 React + TypeScript + Vite 기반의 완전한 싱글페이지 애플리케이션(SPA)을 구현합니다. 인증 토큰 자동 관리, 무한 스크롤 LP(빌보드) 목록, 댓글 시스템, 사용자 프로필 관리, 다크 모드 등의 기능을 포함합니다.

변경 사항 요약

LP 플랫폼 완전 애플리케이션 구현 (130+ 파일, ~3,800줄)

레이어 / 파일 요약
프로젝트 설정
.gitignore, package.json, tsconfig.*, vite.config.ts, eslint.config.js, postcss.config.js, index.html
Vite 프로젝트 구조, 의존성, TypeScript/ESLint/PostCSS 설정, HTML 진입점
API 계층 & 토큰 관리
src/apis/axios.ts, src/apis/auth.ts, src/apis/lp.ts
Axios 인스턴스 with 401 refresh-token 재시도 큐, 인증/LP/댓글 API 래퍼
타입 정의
src/types/auth.ts, src/types/common.ts, src/types/lp.ts, src/enums/common.ts, src/vite-env.d.ts
요청/응답 DTO, 페이지네이션, 환경 변수 타입
인증 상태 관리
src/context/AuthContext.tsx
로그인/로그아웃/회원탈퇴, 사용자 정보 동기화, 토큰 저장/삭제
폼 & 커스텀 훅
src/hooks/useForm.ts, useDebounce.ts, useThrottle.ts, useDarkMode.ts, useLocalStorage.ts, useSidebar.ts
폼 검증, 검색 지연, 스크롤 이벤트 제한, 테마 토글, 스토리지 API
React Query 훅
src/hooks/queries/useGetInfinite*.ts, useGetLpList.ts, useCreateLp.ts, useUpdateProfile.ts
무한 스크롤, 커서 기반 페이징, 낙관적 캐시 업데이트
UI 컴포넌트
src/components/{Navbar, Sidebar, Modal, LpCard, LpSkeleton, ErrorRetry, Footer, CreateLpModal}.tsx
네비게이션, 메뉴, 모달, 카드, 로딩 상태, 에러 처리
레이아웃 & 라우팅
src/layouts/{HomeLayout, ProtectedLayout}.tsx, src/App.tsx
메인 레이아웃, 인증 보호 라우팅, React Router v6 설정
페이지
src/pages/{LoginPage, SignupPage, HomePage, LpDetailPage, MyPage, GoogleLoginRedirectPage, ThrottlePage, NotFoundPage}.tsx
로그인/가입, LP 목록, 상세, 프로필, 404
검증 & 스타일
src/utils/validate.ts, src/index.css, src/main.tsx
입력 검증, Tailwind 커스텀 애니메이션, 부트스트랩

Sequence Diagram

sequenceDiagram
  participant User
  participant Browser
  participant App as App/Router
  participant Auth as AuthContext
  participant API as Axios + API
  participant Server
  
  User->>Browser: 애플리케이션 접속
  Browser->>App: 앱 초기화
  App->>Auth: AuthProvider 마운트
  Auth->>API: localStorage 토큰 읽기
  Auth->>API: getMyInfo() 호출
  API->>Server: GET /v1/users/me (with access token)
  Server->>API: 200 + user 정보
  API->>Auth: 사용자 정보 반환
  Auth->>App: auth 상태 제공
  
  User->>Browser: 로그인 페이지 방문
  Browser->>App: /login 라우팅
  App->>User: 로그인 폼 렌더링
  
  User->>Browser: 이메일/비밀번호 입력 & 제출
  Browser->>API: postSignin(email, password)
  API->>Server: POST /v1/auth/signin
  Server->>API: 200 + accessToken + refreshToken
  API->>Auth: handleLoginSuccess() 호출
  Auth->>Auth: localStorage에 토큰 저장
  Auth->>API: getMyInfo() 재조회
  API->>Server: GET /v1/users/me
  Server->>API: user 정보
  Auth->>App: user 상태 업데이트
  App->>Browser: /my 또는 이전 경로로 리다이렉트
  
  User->>Browser: LP 목록 페이지 방문
  Browser->>App: / 라우팅
  App->>API: useGetInfiniteLpList(limit=10, search='', order='desc')
  API->>Server: GET /v1/lps?limit=10&order=desc
  Server->>API: cursor 기반 LP 페이지 반환
  API->>App: LpCard 목록 렌더링
  
  User->>Browser: 스크롤 하단 도달
  Browser->>API: fetchNextPage()
  API->>Server: GET /v1/lps?cursor=<nextCursor>&limit=10
  Server->>API: 다음 페이지 반환
  App->>Browser: 추가 LP 카드 렌더링
Loading

추정 코드 검토 시간

🎯 4 (복잡) | ⏱️ ~60분

이유:

  • 높은 복잡도: Axios 토큰/refresh 재시도 큐 로직(196줄), LpDetailPage 댓글/수정/삭제 낙관적 업데이트(529줄)
  • 광범위한 변경: 130+ 파일, API 계층부터 페이지까지 다층 구조
  • 다양한 패턴: React Query, Context API, 커스텀 훅, 무한 스크롤, 인증 보호 라우팅
  • 상호 의존성: 각 페이지가 API/Auth/Query 훅에 의존

관련 PR

제안 검토자

  • wantkdd

🐰 축하 시

🎪 한 주간의 코딩 마술이 완성되었네,
토큰 리프레시는 무한 무한 스크롤하고,
댓글도 무한, 좋아요도 낙관적 업데이트,
다크 모드는 야행성 개발자의 친구!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Create Week08 Mission01, 02, 03'은 변경사항의 핵심 내용인 8주차 미션 구현을 명확하게 요약하고 있습니다.
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.
Description check ✅ Passed PR 설명이 필수 섹션을 모두 포함하고 있으며, 구현 사항을 상세하게 작성했고 스크린샷을 첨부했습니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 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.

@librelldd librelldd marked this pull request as draft May 23, 2026 09:40
@librelldd librelldd requested a review from wantkdd May 23, 2026 09:40
@librelldd librelldd self-assigned this May 23, 2026
@librelldd librelldd marked this pull request as ready for review May 23, 2026 09:40
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: 20

Note

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

🟡 Minor comments (13)
Week08/librelldd/mission01/src/layouts/HomeLayout.tsx-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

비표준 Tailwind 간격 유틸리티를 확인하세요.

pt-13 클래스는 Tailwind CSS의 기본 간격 스케일에 포함되지 않습니다. 기본 스케일은 4의 배수를 사용합니다 (예: pt-12=3rem, pt-16=4rem). 커스텀 테마에 정의되지 않았다면 이 클래스가 적용되지 않을 수 있습니다.

🔧 표준 간격 클래스 사용 제안
-      <div className="pt-13">
+      <div className="pt-12">
        {/* 또는 */}
+      <div className="pt-16">

정확히 52px(13×4)가 필요하다면, 커스텀 값을 사용하세요:

-      <div className="pt-13">
+      <div className="pt-[52px]">
🤖 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/librelldd/mission01/src/layouts/HomeLayout.tsx` at line 15, The div in
HomeLayout.tsx uses a nonstandard Tailwind utility "pt-13"; replace it with a
valid spacing token or an explicit value: either use the nearest standard class
(e.g., "pt-12" or "pt-16"), add a custom spacing key in your tailwind.config.js
and use that name, or use an arbitrary value like "pt-[52px]" to get exactly
52px; update the className on the <div> accordingly.
Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteLpList.ts-20-20 (1)

20-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

쿼리 키에 limit을 포함해 캐시 경계를 명확히 하세요.

현재 queryKey[QUERY_KEY.lps, search, order]로 고정돼 limit이 달라질 경우(향후 가변/다른 호출부 추가 시) 캐시가 섞일 여지가 있습니다. 호출부는 현재 useGetInfiniteLpList(10, ...)limit이 고정이어서 당장 재현 가능성은 낮지만, 안전하게 limit을 키에 포함하는 편이 좋습니다.

🔧 제안 수정안
-        queryKey: [QUERY_KEY.lps, search, order],
+        queryKey: [QUERY_KEY.lps, limit, search, order],
🤖 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/librelldd/mission01/src/hooks/queries/useGetInfiniteLpList.ts` at line
20, The queryKey for useGetInfiniteLpList currently omits the limit which can
cause cache collisions when limit varies; update the queryKey in
useGetInfiniteLpList (the array currently [QUERY_KEY.lps, search, order]) to
include the limit parameter (e.g., [QUERY_KEY.lps, search, order, limit]) so
each distinct page-size gets its own cache entry, and ensure any place that
constructs or compares this key (the useGetInfiniteLpList hook and its
useInfiniteQuery invocation) uses the same key shape.
Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteComments.ts-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

디버그 로그는 제거하거나 개발 환경으로 제한하세요.

페이지 데이터 전체를 매 요청마다 콘솔에 출력하면 운영 환경 노이즈가 커집니다.

🤖 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/librelldd/mission01/src/hooks/queries/useGetInfiniteComments.ts` at
line 19, Remove the stray debug console.log in useGetInfiniteComments (the
"[useGetInfiniteComments] lastPage:" log) or wrap it so it only runs in
development: locate the console.log in the useGetInfiniteComments hook and
either delete the statement or guard it with an environment check (e.g.,
process.env.NODE_ENV === 'development' or an isDev flag) so that full page data
is not logged in production.
Week08/librelldd/mission01/src/pages/ThrottlePage.tsx-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

디버깅용 console.log를 제거하세요.

리렌더링 추적을 위한 콘솔 출력이 남아있습니다. 프로덕션 배포 전에 제거해야 합니다.

🧹 제안하는 수정
-    console.log("리렌더링");
🤖 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/librelldd/mission01/src/pages/ThrottlePage.tsx` at line 19, Remove the
leftover debug console output by deleting the console.log("리렌더링"); statement
inside the ThrottlePage component (or replace it with a conditional/debug-only
logger if runtime debugging is required); ensure no other dev-only console.log
calls remain in the component so production builds have no debug prints.
Week08/librelldd/mission01/src/hooks/useThrottle.ts-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

변수명의 오타를 수정하세요.

lastExcecutedlastExecuted의 오타입니다. 파일 전체에서 일관되게 수정이 필요합니다.

📝 제안하는 수정
-   const lastExcecuted = useRef<number>(Date.now());
+   const lastExecuted = useRef<number>(Date.now());

그리고 이후 모든 참조 위치도 수정:

  • Line 18, 20, 23: lastExcecutedlastExecuted
🤖 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/librelldd/mission01/src/hooks/useThrottle.ts` at line 14, Rename the
misspelled ref variable lastExcecuted to lastExecuted throughout the useThrottle
hook: update the declaration (const lastExcecuted = useRef<number>(Date.now()) →
lastExecuted) and replace every usage/reference of lastExcecuted inside
useThrottle (including in the timeout/check logic and any set or read accesses)
so the identifier is consistent.
Week08/librelldd/mission01/src/components/Footer.tsx-9-10 (1)

9-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

저작권 텍스트의 형식을 수정하세요.

텍스트에 공백이 누락되어 있습니다. &copy: 뒤에 공백이 필요하고, Dolimpan.allrights 사이에도 공백이 필요합니다.

📝 제안하는 수정
                <p>
-                   &copy:{new Date().getFullYear()}SpinningSpinning Dolimpan.all
-                   rights reserved.
+                   &copy; {new Date().getFullYear()} SpinningSpinning Dolimpan. All rights reserved.
                </p>
🤖 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/librelldd/mission01/src/components/Footer.tsx` around lines 9 - 10, In
Footer.tsx update the copyright JSX string (&copy:{new
Date().getFullYear()}SpinningSpinning Dolimpan.all rights reserved.) to use the
correct HTML entity and spacing: replace with "&copy; {new Date().getFullYear()}
SpinningSpinning Dolimpan. All rights reserved." so there is a space after the
entity and between the year, site name, and "All rights reserved." and fix
"Dolimpan.all" to "Dolimpan. All".
Week08/librelldd/mission01/src/pages/SignupPage.tsx-59-59 (1)

59-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

프로덕션 코드에서 디버깅용 console.log를 제거하세요.

디버깅용 콘솔 출력이 남아있습니다. 배포 전에 제거해야 합니다.

🧹 제안하는 수정
-      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/librelldd/mission01/src/pages/SignupPage.tsx` at line 59, Remove the
debug console.log in the SignupPage component (the "console.log(\" 회원가입 완료:\",
response)" call) from the signup/submit handler (e.g., the function handling
form submission such as handleSignup or onSubmit); either delete the statement
or replace it with the app's proper logging/notification mechanism (e.g., call
the user-facing success notifier or centralized logger with the response) so no
debugging console output remains in production.
Week08/librelldd/mission01/src/pages/SignupPage.tsx-157-163 (1)

157-163: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

비밀번호 확인 토글 버튼에도 아이콘이나 텍스트를 추가하세요.

위와 동일하게 버튼 내부가 비어있습니다.

👁️ 제안하는 수정
                  <button
                    type="button"
                    onClick={() => setShowConfirm(!showConfirm)}
                    className="absolute right-4 top-4 text-gray-500"
                  >
-
+                   {showConfirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
                  </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/librelldd/mission01/src/pages/SignupPage.tsx` around lines 157 - 163,
The close button inside SignupPage that toggles password confirmation visibility
(onClick={() => setShowConfirm(!showConfirm)} using setShowConfirm and
showConfirm) is empty and needs visible content and accessibility text; add an
eye/eye-off icon or simple text that reflects the current state (e.g., "Show" /
"Hide" or an SVG icon) inside the button, update its aria-label to something
like aria-label={showConfirm ? "Hide password confirmation" : "Show password
confirmation"}, and ensure the visual label toggles based on showConfirm so
users can tell the current state.
Week08/librelldd/mission01/src/pages/SignupPage.tsx-140-146 (1)

140-146: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

비밀번호 표시 토글 버튼에 아이콘이나 텍스트를 추가하세요.

버튼 내부가 비어있어 사용자에게 아무것도 표시되지 않습니다. 눈 아이콘(예: lucide-react의 Eye/EyeOff)을 추가하는 것이 좋습니다.

👁️ 제안하는 수정

파일 상단에 아이콘 import 추가:

+import { Eye, EyeOff } from "lucide-react";

버튼 내부 수정:

                  <button
                    type="button"
                    onClick={() => setShowPassword(!showPassword)}
                    className="absolute right-4 top-4 text-gray-500"
                  >
-
+                   {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
                  </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/librelldd/mission01/src/pages/SignupPage.tsx` around lines 140 - 146,
버튼 내부가 비어 있어 토글 상태를 사용자에게 보여주지 않으므로 SignupPage.tsx에서 보여지는 비밀번호 토글 버튼에 아이콘을
렌더링하도록 수정하세요: 파일 상단에 lucide-react의 Eye와 EyeOff를 import하고, 해당 버튼(클릭 핸들러
setShowPassword, 상태 showPassword 사용) 내부에 showPassword 상태에 따라 Eye 또는 EyeOff 아이콘을
조건부로 렌더링하며 접근성을 위해 적절한 aria-label 또는 title을 추가하세요.
Week08/librelldd/mission01/src/pages/LoginPage.tsx-109-109 (1)

109-109: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

구글 아이콘 이미지에 대체 텍스트가 없습니다.

Line 109의 <img>alt가 없어 보조기기 접근성이 떨어집니다. 장식용이면 alt=""로 명시해 주세요.

🔧 제안 수정
-              <img src={"/images/google.svg"} />
+              <img src={"/images/google.svg"} alt="" aria-hidden="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/librelldd/mission01/src/pages/LoginPage.tsx` at line 109, LoginPage의
`<img src="/images/google.svg" />`에 접근성용 대체 텍스트를 추가하세요; 장식용이라면 `alt=""`로 명시하고
콘텐츠를 전달하는 이미지라면 의미있는 텍스트(예: `alt="구글로 로그인"`, `alt="Google 로고"` 등)로 설정하여
LoginPage 컴포넌트 내 해당 `<img>` 요소를 수정하세요.
Week08/librelldd/mission01/src/pages/HomePage.tsx-170-188 (1)

170-188: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

초기 로딩 중 빈 상태 문구가 먼저 노출될 수 있습니다.

isPending일 때도 빈 목록 분기로 내려가서 "등록된 LP가 없습니다."가 잠깐 보일 수 있습니다.

🔧 제안 수정
   if (isError) {
     return <ErrorRetry onRetry={() => refetch()} />;
   }
+
+  if (isPending) {
+    return <LpListSkeleton />;
+  }
🤖 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/librelldd/mission01/src/pages/HomePage.tsx` around lines 170 - 188,
The empty-state message can flash during initial loading because the component
treats isPending as not-loading; update the rendering logic in HomePage.tsx to
guard the empty-list branch with the loading flag (isPending) so that while
isPending is true you render a loading state (e.g., LpListSkeleton or other
loader) instead of the "등록된 LP가 없습니다." message; specifically change the
condition around allLps and the empty fallback to first check isPending (or
combine into if (isPending || allLps.length > 0) show list/skeleton else show
empty message) and ensure the ref, isFetchingNextPage, LpCard, debouncedQuery
logic remains unchanged.
Week08/librelldd/mission01/src/pages/LpDetailPage.tsx-426-428 (1)

426-428: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

댓글 유효성 검증에 trim 기준이 빠져 있습니다.

Line 427은 길이만 검사해서 " " 같은 공백 입력이 등록될 수 있습니다. commentInput.trim().length 기준으로 검증해 주세요.

🤖 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/librelldd/mission01/src/pages/LpDetailPage.tsx` around lines 426 -
428, The button's disabled check uses commentInput.length which allows
whitespace-only comments; update the validation to use
commentInput.trim().length (e.g., replace the disabled condition using
commentInput.length < 2 with commentInput.trim().length < 2) and ensure any
related check in handleCreateComment or comment submission logic also uses
trimmed input (referencing commentInput and handleCreateComment) to prevent
whitespace-only comments.
Week08/librelldd/mission01/src/components/CreateLpModal.tsx-25-34 (1)

25-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

파일 업로드 검증이 안내 문구와 불일치합니다.

Line 179에서 최대 10MB를 안내하지만, Line 25-34에는 파일 크기/타입 검증이 없습니다. 프론트에서 선검증을 추가해 불필요한 업로드 요청을 막는 게 좋습니다.

Also applies to: 179-180

🤖 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/librelldd/mission01/src/components/CreateLpModal.tsx` around lines 25
- 34, handleFileChange currently accepts any file; add client-side validation to
match the UI text (max 10MB and allowed image types) before calling
setThumbnail/setThumbnailPreview: in handleFileChange check if file.size <= 10 *
1024 * 1024 and that file.type matches allowed patterns (e.g. image/png,
image/jpeg, image/gif or image/* as desired), and only then create FileReader
and setThumbnail/setThumbnailPreview; on validation failure clear
thumbnail/preview and set a validation error state (create/use a state like
setThumbnailError or setErrorMessage) so the UI shows the same "max 10MB" / type
message. Ensure the logic lives inside handleFileChange and references
setThumbnail and setThumbnailPreview.
🧹 Nitpick comments (8)
Week08/librelldd/mission01/src/layouts/ProtectedLayout.tsx (1)

1-1: ⚡ Quick win

.tsx 확장자 포함 import로 인한 빌드 실패 리스크는 낮음
Week08/librelldd/mission01/tsconfig.app.jsontsconfig.node.json"allowImportingTsExtensions": true가 있어 ProtectedLayout.tsx../context/AuthContext.tsx import는 설정 문제로 인한 빌드 실패 사유가 아닙니다. 다만 import 스타일을 ../context/AuthContext처럼 확장자 없이 통일하는 건 개선 여지가 있습니다.

🤖 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/librelldd/mission01/src/layouts/ProtectedLayout.tsx` at line 1, Change
the import in ProtectedLayout.tsx to remove the .tsx extension to maintain
consistent import style; replace import { useAuth } from
"../context/AuthContext.tsx" with an extensionless import
(../context/AuthContext) so the useAuth symbol resolves the same without
embedding file extensions and avoids inconsistent import style across the
codebase.
Week08/librelldd/mission01/src/hooks/useCreateLp.ts (1)

13-15: ⚡ Quick win

useCreateLponError: (error: any) 타입 제거 권장

Week08/librelldd/mission01/src/hooks/useCreateLp.ts에서 onErrorany로 고정되어(13-15) 타입 안전성이 떨어집니다. useMutationTError 제네릭을 AxiosError로 지정하고(import type { AxiosError } from "axios";), 필요 시 error.response?.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/librelldd/mission01/src/hooks/useCreateLp.ts` around lines 13 - 15,
Replace the loose any error typing in useCreateLp by importing and using
AxiosError: add `import type { AxiosError } from "axios"` and specify the
useMutation TError generic as AxiosError (so the mutation signature uses
AxiosError for errors), then change the onError parameter from (error: any) to
(error: AxiosError) and handle details via AxiosError properties (e.g.,
error.response?.data) inside onError; update references in useCreateLp and the
useMutation call accordingly.
Week08/librelldd/mission01/src/hooks/useDarkMode.ts (2)

13-24: 💤 Low value

classList 조작을 단순화할 수 있습니다.

현재 구현은 정확하지만, classList.toggle() 메서드를 사용하면 더 간결하게 작성할 수 있습니다.

♻️ 간결한 구현 제안
  useEffect(() => {
    const root = window.document.documentElement;
-   if (isDark) {
-     root.classList.add("dark");
-     root.classList.remove("light");
-     localStorage.setItem("theme", "dark");
-   } else {
-     root.classList.remove("dark");
-     root.classList.add("light");
-     localStorage.setItem("theme", "light");
-   }
+   root.classList.toggle("dark", isDark);
+   root.classList.toggle("light", !isDark);
+   localStorage.setItem("theme", isDark ? "dark" : "light");
  }, [isDark]);
🤖 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/librelldd/mission01/src/hooks/useDarkMode.ts` around lines 13 - 24,
Simplify the classList manipulation in the useEffect inside useDarkMode by
replacing the explicit add/remove calls with classList.toggle using the boolean
force argument (e.g., root.classList.toggle("dark", isDark) and
root.classList.toggle("light", !isDark)); keep the localStorage updates for
"theme" based on isDark and preserve the existing dependency on isDark so the
behavior remains identical but more concise.

4-11: ⚡ Quick win

localStorage null 처리를 명시적으로 개선하세요.

savedTheme !== "light" 로직은 savedThemenull일 때 다크 모드를 기본값으로 사용합니다. 사용자가 테마를 설정한 적이 없는 경우의 동작을 더 명확하게 표현하는 것이 좋습니다.

♻️ 명시적 null 체크 제안
  const [isDark, setIsDark] = useState(() => {
    if (typeof window !== "undefined") {
      const savedTheme = localStorage.getItem("theme");
-     
-     return savedTheme !== "light";
+     // 저장된 값이 없으면 다크 모드를 기본값으로 사용
+     return savedTheme === null ? true : savedTheme === "dark";
    }
    return 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/librelldd/mission01/src/hooks/useDarkMode.ts` around lines 4 - 11, The
state initializer in useDark (useDark, isDark, setIsDark) should explicitly
handle null from localStorage.getItem("theme") instead of relying on savedTheme
!== "light"; update the initializer to check if savedTheme === null and return
the intended default (e.g., true) in that case, otherwise return savedTheme !==
"light" so the behavior is explicit when the user has never set a theme.
Week08/librelldd/mission01/src/pages/NotFoundPage.tsx (1)

1-7: ⚡ Quick win

404 페이지 UX를 개선하세요.

현재 구현은 단순한 텍스트만 표시합니다. 사용자가 홈으로 돌아갈 수 있는 링크와 적절한 스타일링을 추가하면 사용자 경험이 향상됩니다.

♻️ 개선된 404 페이지 예시
+import { Link } from "react-router-dom";
+
 const NotFoundPage = () => {
   return (
-    <div>NotFoundPage</div>
+    <div className="min-h-screen flex flex-col items-center justify-center">
+      <h1 className="text-4xl font-bold mb-4">404</h1>
+      <p className="text-xl mb-8">페이지를 찾을 수 없습니다</p>
+      <Link to="/" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
+        홈으로 돌아가기
+      </Link>
+    </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/librelldd/mission01/src/pages/NotFoundPage.tsx` around lines 1 - 7,
NotFoundPage currently renders only plain text; update the NotFoundPage
component to present a user-friendly 404 view by adding a clear heading and
descriptive message, a prominent link or button that navigates back to the home
route (use your router's Link or history push), center and style the container
(apply a scoped CSS module/className or inline styles) and include accessible
attributes (role="alert" or aria-live as appropriate) so it is both visually
improved and keyboard/screen-reader friendly.
Week08/librelldd/mission01/src/pages/SignupPage.tsx (1)

31-31: ⚡ Quick win

타입 안전성을 위해 any 타입 대신 명시적 타입을 사용하세요.

errors 객체를 any로 선언하면 타입 체크의 이점을 잃게 됩니다. Partial<Record<keyof SignupFormValues, string>>와 같이 명시적으로 타이핑하는 것을 권장합니다.

♻️ 제안하는 수정
    validate: (values) => {
-     const errors: any = {};
+     const errors: Partial<Record<keyof SignupFormValues, string>> = {};
      if (!values.email.includes("@")) errors.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/librelldd/mission01/src/pages/SignupPage.tsx` at line 31, Replace the
loose any typing on the local errors object with an explicit type to restore
type safety: declare errors as Partial<Record<keyof SignupFormValues, string>>
(or the appropriate mapped type if your form type is named differently) in the
SignupPage component so usages of errors (e.g., setting errors.email or
iterating keys) are type-checked; ensure SignupFormValues is imported or defined
in scope and adjust any assignments to match string values.
Week08/librelldd/mission01/src/constants/key.ts (1)

1-4: 💤 Low value

중복된 키-값 구조를 단순화하는 것을 고려하세요.

LOCAL_STORAGE_KEY의 각 속성에서 키와 값이 동일합니다. 이는 중복이며, 직접 문자열 상수로 내보내거나 다른 패턴을 사용하는 것이 더 명확할 수 있습니다.

♻️ 대안 패턴

옵션 1: as const 사용

export const LOCAL_STORAGE_KEY = {
    accessToken: 'accessToken',
    refreshToken: 'refreshToken',
} as const;

옵션 2: 직접 상수 export

export const ACCESS_TOKEN_KEY = 'accessToken';
export const REFRESH_TOKEN_KEY = 'refreshToken';
🤖 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/librelldd/mission01/src/constants/key.ts` around lines 1 - 4,
LOCAL_STORAGE_KEY currently repeats identical keys and values; replace the
object with a non-redundant pattern—either mark the object as readonly using
"LOCAL_STORAGE_KEY" with as const (retain access via
LOCAL_STORAGE_KEY.accessToken / .refreshToken) or export individual string
constants (e.g., ACCESS_TOKEN_KEY and REFRESH_TOKEN_KEY) and update all usages
to reference the chosen symbols; ensure types remain compatible where
LOCAL_STORAGE_KEY was used.
Week08/librelldd/mission01/src/components/Modal.tsx (1)

14-77: ⚡ Quick win

접근성 향상을 위해 키보드 지원과 포커스 관리를 추가하는 것을 권장합니다.

현재 모달은 Escape 키 처리, 포커스 트랩, ARIA 속성이 없습니다. 이는 키보드 사용자와 스크린 리더 사용자의 경험을 저하시킵니다.

♿ 제안하는 개선사항

1. Escape 키 지원 추가:

useEffect(() => {
  if (!isOpen) return;
  
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === 'Escape') onClose();
  };
  
  document.addEventListener('keydown', handleEscape);
  return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);

2. ARIA 속성 추가:

-<div className="relative bg-white dark:bg-gray-900 rounded-3xl...">
+<div 
+  role="dialog"
+  aria-modal="true"
+  aria-labelledby="modal-title"
+  className="relative bg-white dark:bg-gray-900 rounded-3xl..."
+>
   <div className="p-6">
     <div className="flex items-center justify-between mb-6">
       <div className="flex items-center space-x-3">
         <div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-xl">
           <AlertTriangle className="w-6 h-6 text-amber-500" />
         </div>
-        <h3 className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
+        <h3 id="modal-title" className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
           {title}
         </h3>

3. 포커스 관리:
추가적으로 react-focus-lock이나 수동 포커스 트랩 구현을 고려하세요.

🤖 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/librelldd/mission01/src/components/Modal.tsx` around lines 14 - 77,
The Modal component lacks keyboard accessibility and focus management; add
Escape key handling, ARIA attributes, and a focus trap so keyboard and assistive
tech users can interact properly. In the Modal component (Modal props: isOpen,
onClose, onConfirm, title), add a useEffect to listen for 'Escape' and call
onClose when open, set proper ARIA attributes on the dialog wrapper
(role="dialog", aria-modal="true", aria-labelledby pointing to the title
element), and implement focus management (either integrate a focus-lock library
like react-focus-lock around the modal content or add logic to save/restore
focus and trap tab/shift+tab within the modal while isOpen). Ensure the close
button remains tabbable and focus is moved into the modal when opened and
returned when closed.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@Week08/librelldd/mission01/package.json`:
- Around line 23-33: Remove the mismatched type package from package.json:
delete the "`@types/react-router-dom`" entry under devDependencies (since your
dependency is "react-router-dom": "^7.14.0"); then run your package manager
(npm/yarn/pnpm) to update node_modules and the lockfile so the change takes
effect and ensure TypeScript uses the correct v7 types.

In `@Week08/librelldd/mission01/src/apis/auth.ts`:
- Around line 20-23: Remove the console.log that prints the entire signin
request body in postSignin; do not log sensitive fields (e.g., password from
RequestLoginDto). If you need to retain a debug log, log only non-sensitive
properties (like username/email) or a masked version (e.g., replace password
with "****") before calling axiosInstance.post("/v1/auth/signin", body) in the
postSignin function and ensure no raw body is written to stdout.

In `@Week08/librelldd/mission01/src/apis/axios.ts`:
- Around line 142-190: The code creates a new Promise with an async executor
(triggering no-async-promise-executor); replace the entire new Promise(async
(resolve, reject) => { ... }) block with an async function that directly uses
await/try/catch/finally and returns or throws—keep the same logic: read
refreshToken via getStorageItem(LOCAL_STORAGE_KEY.refreshToken), call
axiosInstance.post("/v1/auth/refresh"), update tokens via setStorageItem,
compute cleanToken, call processQueue(null, cleanToken), update
originalRequest.headers (use headers.set if available else assign), and return
axiosInstance.request(originalRequest); in the catch call
processQueue(refreshError, null), removeStorageItem for both LOCAL_STORAGE_KEY
keys and set window.location.href = "/login" then re-throw refreshError; ensure
isRefreshing is set to false in a finally block so behavior around isRefreshing,
processQueue, originalRequest, and axiosInstance is preserved.

In `@Week08/librelldd/mission01/src/apis/lp.ts`:
- Around line 101-105: The axiosInstance.post call that sends formData currently
sets "Content-Type": "multipart/form-data" manually which can omit the required
boundary; remove the headers block so the browser/axios can set the proper
Content-Type automatically when calling axiosInstance.post("/v1/uploads/public",
formData, ...). If this code runs in a Node environment using form-data, instead
obtain headers from formData.getHeaders() and pass those to axios; otherwise
simply omit the headers field.

In `@Week08/librelldd/mission01/src/components/CreateLpModal.tsx`:
- Around line 125-126: The backdrop onClick (handleBackdropClick), the close
button handler (handleClose) and any other close/cancel controls must be gated
when an upload/create is in progress; modify those handlers to early-return when
isUploading || createLpMutation.isPending (or compute a single boolean like
isInProgress) and also disable or remove clickability of the UI elements (e.g.,
add disabled/aria-disabled and prevent pointer events) so users cannot trigger
close while uploading; update handleBackdropClick, handleClose and the
cancel/close buttons referenced around lines 125-126, 136-137 and 264-272 to
check that flag and no-op if true.

In `@Week08/librelldd/mission01/src/components/Sidebar.tsx`:
- Around line 63-64: The sidebar currently includes a menu item { name: "검색",
path: "/search", icon: Search, authRequired: true } that points to a
non-existent route and causes a 404; either remove or update that entry in
Sidebar.tsx (the "검색" menu/path "/search") or add a corresponding route handler
in App.tsx (e.g., a /search Route and its component) so the path resolves
correctly when clicked.

In `@Week08/librelldd/mission01/src/context/AuthContext.tsx`:
- Around line 74-76: The exported signup function in AuthContext is currently a
no-op; either implement it to call the real signup API or remove it from the
context contract if unused. If implementing, implement signup(RequestSignupDto)
inside AuthContext to call the backend signup endpoint (using existing
api/client utilities or fetch), handle success by updating auth state (e.g.,
setUser/setAuthTokens) and handle/propagate errors (try/catch and return/throw a
sensible error or result), and update any related types in AuthContext.Provider
and the RequestSignupDto usage points; if unused, remove signup from the context
interface and all consumers (signUp references) to keep the contract consistent.

In `@Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteComments.ts`:
- Around line 16-24: The current useInfiniteQuery() call in
useGetInfiniteComments uses queryKey: ["lpComments", lpId, order] but the
queryFn calls getLPComments(..., { limit, order }), causing cache collisions
when limit changes; update the queryKey to include limit (e.g., ["lpComments",
lpId, order, limit]) inside the useGetInfiniteComments hook so variants of limit
produce separate caches and ensure getNextPageParam and other options remain
unchanged.

In `@Week08/librelldd/mission01/src/hooks/queries/useGetLpList.ts`:
- Around line 15-22: The query uses queryKey: [QUERY_KEY.lps, search, sort] but
the queryFn calls getLPList({ cursor, search, limit }) so sort is never sent and
cursor/limit changes don't invalidate cache; update useGetLpList to (1) pass the
sort/order parameter into getLPList (e.g., map sort → PaginationDto.order)
inside the queryFn and (2) include all request params that affect results
(cursor and limit and sort and search) in the queryKey (e.g., [QUERY_KEY.lps,
search, sort, cursor, limit]) so cache keys match actual requests; modify the
useGetLpList function and its queryFn accordingly.

In `@Week08/librelldd/mission01/src/hooks/useForm.ts`:
- Around line 13-17: handleChange currently merges state using the
possibly-stale closures values and touched (setValues({ ...values, [name]: text
}) and setTouched({ ...touched, [name]: true })), which can lose rapid
consecutive updates; change both updates to function-style updates so React uses
the latest state (use setValues(prev => ({ ...prev, [name]: text })) and
setTouched(prev => ({ ...prev, [name]: true })) inside handleChange).

In `@Week08/librelldd/mission01/src/hooks/useThrottle.ts`:
- Around line 28-31: The setTimeout callback in useThrottle is incomplete: when
the timeout fires it must update lastExcecuted.current and call
setThrottledValue(value) so the throttled value actually changes after delay.
Modify the timeout handler (the one creating timeId) to set
lastExcecuted.current = Date.now() (or appropriate timestamp) and then call
setThrottledValue(value); keep the existing timeout cancellation logic intact.

In `@Week08/librelldd/mission01/src/index.css`:
- Line 3: stylelint will flag Tailwind's `@custom-variant dark (...)` as an
unknown at-rule because `stylelint-config-standard-scss` enables
`scss/at-rule-no-unknown`; update the stylelint config (`.stylelintrc.json`) to
override `scss/at-rule-no-unknown` by adding `ignoreAtRules: ["custom-variant"]`
or alternatively add the at-rule to `languageOptions.syntax.atRules` so
`@custom-variant` is recognized, ensuring the rule name
`scss/at-rule-no-unknown` and the at-rule string `custom-variant` are used in
the override.

In `@Week08/librelldd/mission01/src/pages/GoogleLoginRedirectPage.tsx`:
- Around line 10-17: The page currently reads access/refresh tokens from
URLSearchParams(window.location.search) and writes them via
setAccessToken/setRefreshToken, which exposes tokens in the address bar; change
GoogleLoginRedirectPage to only read an authorization "code" query param
(instead of LOCAL_STORAGE_KEY.accessToken/refreshToken), POST that code to your
backend exchange endpoint (server performs Authorization Code + PKCE token
exchange) and then accept tokens from the server response to call
setAccessToken/setRefreshToken; remove any logic that parses or persists tokens
from the URL and ensure you redirect (window.location.href = "/my") only after
the backend returns tokens so sensitive tokens never appear in the address bar
or browser history.
- Around line 14-17: 현재 GoogleLoginRedirectPage의 token 처리에서 accessToken만 있으면
refreshToken이 null이어도 setAccessToken/setRefreshToken 호출 후 리다이렉트 하고 있으므로,
accessToken과 refreshToken 둘 다 존재할 때만 상태를 설정하고 window.location.href = "/my"로
이동하도록 조건을 바꿔주세요 (예: if (accessToken && refreshToken) {
setAccessToken(accessToken); setRefreshToken(refreshToken); window.location.href
= "/my"; } else { window.location.href = "/login"; }). 또한 부분적으로 상태가 설정되는 상황을
방지하려면 setAccessToken/setRefreshToken 호출을 둘 다 한 곳에 묶고, else 분기에서는 기존 토큰을 클리어하거나
로그인으로 되돌리는 처리를 추가해 주세요.

In `@Week08/librelldd/mission01/src/pages/HomePage.tsx`:
- Around line 15-17: The selected search type state (searchType) is not being
sent to the fetch/query, so changing the dropdown doesn't change results; update
the code that builds the fetch/query parameters and the query key to include
searchType (where the request is made—the function calling the API or the
useQuery/useEffect that currently uses only the search text/page), e.g., add
searchType to the params object passed to the API call and include searchType in
the query key so caching/invalidations react to it; ensure the dropdown handler
still updates setSearchType and that any hook dependencies (the fetch function,
useEffect, or useQuery) list searchType so the results re-run when it changes.

In `@Week08/librelldd/mission01/src/pages/LpDetailPage.tsx`:
- Around line 153-155: The like-toggle mutation (mutate alias handleToggleLike
used with mutationFn calling postLpLike/deleteLpLike) allows repeated clicks
while a request is pending; add a pending guard by using the useMutation's
isLoading (or isPending) return value to disable the like button and prevent
re-entry (early return) when a mutation is in-flight, and update the button's
disabled prop accordingly; apply the same pattern to the other toggle mutation
referenced around lines 273-275 (the other mutate handler) so both mutations
check their respective isLoading flags before invoking mutate/mutateAsync and
disable the related UI while pending.

In `@Week08/librelldd/mission01/src/pages/MyPage.tsx`:
- Around line 145-157: The modal root div (the container with classes "relative
bg-white ...") in MyPage.tsx lacks accessibility attributes; add role="dialog"
and aria-modal="true" to that root, give the heading element (the <h3> "프로필 수정")
a unique id (e.g., "editProfileTitle") and set aria-labelledby on the modal root
to that id, and ensure the close button (closeEditModal) remains
keyboard-focusable; update the JSX attributes accordingly so assistive tech can
identify the dialog.

In `@Week08/librelldd/mission01/src/src/components/Navbar.tsx`:
- Around line 48-53: The icon-only theme toggle button in Navbar.tsx lacks an
accessible name; update the button that calls toggleDarkMode (uses isDark to
render Sun/Moon) to include a descriptive aria-label (e.g., aria-label="Toggle
dark mode" or aria-label based on isDark state) so screen readers convey its
purpose, and apply the same fix to the other icon-only button at the referenced
block (lines 63-68) so all icon-only buttons have appropriate aria-labels.

In `@Week08/librelldd/mission01/src/types/lp.ts`:
- Around line 21-22: The createdAt and updatedAt fields on the Lp type are
declared as Date but API responses are ISO date strings; change createdAt and
updatedAt in the Lp interface to string to match runtime data, and add a
dedicated parser (e.g., parseLp) that returns an LpWithDates variant converting
those strings to Date objects via new Date(...) when you need Date semantics;
update any code that assumed Date to call parseLp or use LpWithDates instead.

In `@Week08/librelldd/mission01/src/utils/validate.ts`:
- Around line 11-17: The email regex in validate.ts (the test against
values.email that sets errors.email) is too restrictive and rejects many valid
addresses; replace the current pattern with a more permissive, well-known simple
validator (for example use a non-empty local and domain check like
/^[^\s@]+@[^\s@]+\.[^\s@]+$/) or delegate to a standard email validation
utility, then update the test that assigns errors.email accordingly so valid
addresses are not falsely rejected.

---

Minor comments:
In `@Week08/librelldd/mission01/src/components/CreateLpModal.tsx`:
- Around line 25-34: handleFileChange currently accepts any file; add
client-side validation to match the UI text (max 10MB and allowed image types)
before calling setThumbnail/setThumbnailPreview: in handleFileChange check if
file.size <= 10 * 1024 * 1024 and that file.type matches allowed patterns (e.g.
image/png, image/jpeg, image/gif or image/* as desired), and only then create
FileReader and setThumbnail/setThumbnailPreview; on validation failure clear
thumbnail/preview and set a validation error state (create/use a state like
setThumbnailError or setErrorMessage) so the UI shows the same "max 10MB" / type
message. Ensure the logic lives inside handleFileChange and references
setThumbnail and setThumbnailPreview.

In `@Week08/librelldd/mission01/src/components/Footer.tsx`:
- Around line 9-10: In Footer.tsx update the copyright JSX string (&copy:{new
Date().getFullYear()}SpinningSpinning Dolimpan.all rights reserved.) to use the
correct HTML entity and spacing: replace with "&copy; {new Date().getFullYear()}
SpinningSpinning Dolimpan. All rights reserved." so there is a space after the
entity and between the year, site name, and "All rights reserved." and fix
"Dolimpan.all" to "Dolimpan. All".

In `@Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteComments.ts`:
- Line 19: Remove the stray debug console.log in useGetInfiniteComments (the
"[useGetInfiniteComments] lastPage:" log) or wrap it so it only runs in
development: locate the console.log in the useGetInfiniteComments hook and
either delete the statement or guard it with an environment check (e.g.,
process.env.NODE_ENV === 'development' or an isDev flag) so that full page data
is not logged in production.

In `@Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteLpList.ts`:
- Line 20: The queryKey for useGetInfiniteLpList currently omits the limit which
can cause cache collisions when limit varies; update the queryKey in
useGetInfiniteLpList (the array currently [QUERY_KEY.lps, search, order]) to
include the limit parameter (e.g., [QUERY_KEY.lps, search, order, limit]) so
each distinct page-size gets its own cache entry, and ensure any place that
constructs or compares this key (the useGetInfiniteLpList hook and its
useInfiniteQuery invocation) uses the same key shape.

In `@Week08/librelldd/mission01/src/hooks/useThrottle.ts`:
- Line 14: Rename the misspelled ref variable lastExcecuted to lastExecuted
throughout the useThrottle hook: update the declaration (const lastExcecuted =
useRef<number>(Date.now()) → lastExecuted) and replace every usage/reference of
lastExcecuted inside useThrottle (including in the timeout/check logic and any
set or read accesses) so the identifier is consistent.

In `@Week08/librelldd/mission01/src/layouts/HomeLayout.tsx`:
- Line 15: The div in HomeLayout.tsx uses a nonstandard Tailwind utility
"pt-13"; replace it with a valid spacing token or an explicit value: either use
the nearest standard class (e.g., "pt-12" or "pt-16"), add a custom spacing key
in your tailwind.config.js and use that name, or use an arbitrary value like
"pt-[52px]" to get exactly 52px; update the className on the <div> accordingly.

In `@Week08/librelldd/mission01/src/pages/HomePage.tsx`:
- Around line 170-188: The empty-state message can flash during initial loading
because the component treats isPending as not-loading; update the rendering
logic in HomePage.tsx to guard the empty-list branch with the loading flag
(isPending) so that while isPending is true you render a loading state (e.g.,
LpListSkeleton or other loader) instead of the "등록된 LP가 없습니다." message;
specifically change the condition around allLps and the empty fallback to first
check isPending (or combine into if (isPending || allLps.length > 0) show
list/skeleton else show empty message) and ensure the ref, isFetchingNextPage,
LpCard, debouncedQuery logic remains unchanged.

In `@Week08/librelldd/mission01/src/pages/LoginPage.tsx`:
- Line 109: LoginPage의 `<img src="/images/google.svg" />`에 접근성용 대체 텍스트를 추가하세요;
장식용이라면 `alt=""`로 명시하고 콘텐츠를 전달하는 이미지라면 의미있는 텍스트(예: `alt="구글로 로그인"`, `alt="Google
로고"` 등)로 설정하여 LoginPage 컴포넌트 내 해당 `<img>` 요소를 수정하세요.

In `@Week08/librelldd/mission01/src/pages/LpDetailPage.tsx`:
- Around line 426-428: The button's disabled check uses commentInput.length
which allows whitespace-only comments; update the validation to use
commentInput.trim().length (e.g., replace the disabled condition using
commentInput.length < 2 with commentInput.trim().length < 2) and ensure any
related check in handleCreateComment or comment submission logic also uses
trimmed input (referencing commentInput and handleCreateComment) to prevent
whitespace-only comments.

In `@Week08/librelldd/mission01/src/pages/SignupPage.tsx`:
- Line 59: Remove the debug console.log in the SignupPage component (the
"console.log(\" 회원가입 완료:\", response)" call) from the signup/submit handler
(e.g., the function handling form submission such as handleSignup or onSubmit);
either delete the statement or replace it with the app's proper
logging/notification mechanism (e.g., call the user-facing success notifier or
centralized logger with the response) so no debugging console output remains in
production.
- Around line 157-163: The close button inside SignupPage that toggles password
confirmation visibility (onClick={() => setShowConfirm(!showConfirm)} using
setShowConfirm and showConfirm) is empty and needs visible content and
accessibility text; add an eye/eye-off icon or simple text that reflects the
current state (e.g., "Show" / "Hide" or an SVG icon) inside the button, update
its aria-label to something like aria-label={showConfirm ? "Hide password
confirmation" : "Show password confirmation"}, and ensure the visual label
toggles based on showConfirm so users can tell the current state.
- Around line 140-146: 버튼 내부가 비어 있어 토글 상태를 사용자에게 보여주지 않으므로 SignupPage.tsx에서 보여지는
비밀번호 토글 버튼에 아이콘을 렌더링하도록 수정하세요: 파일 상단에 lucide-react의 Eye와 EyeOff를 import하고, 해당
버튼(클릭 핸들러 setShowPassword, 상태 showPassword 사용) 내부에 showPassword 상태에 따라 Eye 또는
EyeOff 아이콘을 조건부로 렌더링하며 접근성을 위해 적절한 aria-label 또는 title을 추가하세요.

In `@Week08/librelldd/mission01/src/pages/ThrottlePage.tsx`:
- Line 19: Remove the leftover debug console output by deleting the
console.log("리렌더링"); statement inside the ThrottlePage component (or replace it
with a conditional/debug-only logger if runtime debugging is required); ensure
no other dev-only console.log calls remain in the component so production builds
have no debug prints.

---

Nitpick comments:
In `@Week08/librelldd/mission01/src/components/Modal.tsx`:
- Around line 14-77: The Modal component lacks keyboard accessibility and focus
management; add Escape key handling, ARIA attributes, and a focus trap so
keyboard and assistive tech users can interact properly. In the Modal component
(Modal props: isOpen, onClose, onConfirm, title), add a useEffect to listen for
'Escape' and call onClose when open, set proper ARIA attributes on the dialog
wrapper (role="dialog", aria-modal="true", aria-labelledby pointing to the title
element), and implement focus management (either integrate a focus-lock library
like react-focus-lock around the modal content or add logic to save/restore
focus and trap tab/shift+tab within the modal while isOpen). Ensure the close
button remains tabbable and focus is moved into the modal when opened and
returned when closed.

In `@Week08/librelldd/mission01/src/constants/key.ts`:
- Around line 1-4: LOCAL_STORAGE_KEY currently repeats identical keys and
values; replace the object with a non-redundant pattern—either mark the object
as readonly using "LOCAL_STORAGE_KEY" with as const (retain access via
LOCAL_STORAGE_KEY.accessToken / .refreshToken) or export individual string
constants (e.g., ACCESS_TOKEN_KEY and REFRESH_TOKEN_KEY) and update all usages
to reference the chosen symbols; ensure types remain compatible where
LOCAL_STORAGE_KEY was used.

In `@Week08/librelldd/mission01/src/hooks/useCreateLp.ts`:
- Around line 13-15: Replace the loose any error typing in useCreateLp by
importing and using AxiosError: add `import type { AxiosError } from "axios"`
and specify the useMutation TError generic as AxiosError (so the mutation
signature uses AxiosError for errors), then change the onError parameter from
(error: any) to (error: AxiosError) and handle details via AxiosError properties
(e.g., error.response?.data) inside onError; update references in useCreateLp
and the useMutation call accordingly.

In `@Week08/librelldd/mission01/src/hooks/useDarkMode.ts`:
- Around line 13-24: Simplify the classList manipulation in the useEffect inside
useDarkMode by replacing the explicit add/remove calls with classList.toggle
using the boolean force argument (e.g., root.classList.toggle("dark", isDark)
and root.classList.toggle("light", !isDark)); keep the localStorage updates for
"theme" based on isDark and preserve the existing dependency on isDark so the
behavior remains identical but more concise.
- Around line 4-11: The state initializer in useDark (useDark, isDark,
setIsDark) should explicitly handle null from localStorage.getItem("theme")
instead of relying on savedTheme !== "light"; update the initializer to check if
savedTheme === null and return the intended default (e.g., true) in that case,
otherwise return savedTheme !== "light" so the behavior is explicit when the
user has never set a theme.

In `@Week08/librelldd/mission01/src/layouts/ProtectedLayout.tsx`:
- Line 1: Change the import in ProtectedLayout.tsx to remove the .tsx extension
to maintain consistent import style; replace import { useAuth } from
"../context/AuthContext.tsx" with an extensionless import
(../context/AuthContext) so the useAuth symbol resolves the same without
embedding file extensions and avoids inconsistent import style across the
codebase.

In `@Week08/librelldd/mission01/src/pages/NotFoundPage.tsx`:
- Around line 1-7: NotFoundPage currently renders only plain text; update the
NotFoundPage component to present a user-friendly 404 view by adding a clear
heading and descriptive message, a prominent link or button that navigates back
to the home route (use your router's Link or history push), center and style the
container (apply a scoped CSS module/className or inline styles) and include
accessible attributes (role="alert" or aria-live as appropriate) so it is both
visually improved and keyboard/screen-reader friendly.

In `@Week08/librelldd/mission01/src/pages/SignupPage.tsx`:
- Line 31: Replace the loose any typing on the local errors object with an
explicit type to restore type safety: declare errors as Partial<Record<keyof
SignupFormValues, string>> (or the appropriate mapped type if your form type is
named differently) in the SignupPage component so usages of errors (e.g.,
setting errors.email or iterating keys) are type-checked; ensure
SignupFormValues is imported or defined in scope and adjust any assignments to
match string values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48434f0e-7254-4b29-9288-bbaf0df30104

📥 Commits

Reviewing files that changed from the base of the PR and between ba77bb2 and 46fb44b.

⛔ Files ignored due to path filters (2)
  • Week08/librelldd/mission01/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • Week08/librelldd/mission01/public/images/google.png is excluded by !**/*.png
📒 Files selected for processing (56)
  • Week08/librelldd/mission01/.gitignore
  • Week08/librelldd/mission01/README.md
  • Week08/librelldd/mission01/eslint.config.js
  • Week08/librelldd/mission01/index.html
  • Week08/librelldd/mission01/package.json
  • Week08/librelldd/mission01/postcss.config.js
  • Week08/librelldd/mission01/src/App.css
  • Week08/librelldd/mission01/src/App.tsx
  • Week08/librelldd/mission01/src/apis/auth.ts
  • Week08/librelldd/mission01/src/apis/axios.ts
  • Week08/librelldd/mission01/src/apis/lp.ts
  • Week08/librelldd/mission01/src/components/CreateLpModal.tsx
  • Week08/librelldd/mission01/src/components/ErrorRetry.tsx
  • Week08/librelldd/mission01/src/components/Footer.tsx
  • Week08/librelldd/mission01/src/components/LpCard.tsx
  • Week08/librelldd/mission01/src/components/LpSkeleton.tsx
  • Week08/librelldd/mission01/src/components/Modal.tsx
  • Week08/librelldd/mission01/src/components/Navbar.tsx
  • Week08/librelldd/mission01/src/components/Sidebar.tsx
  • Week08/librelldd/mission01/src/constants/key.ts
  • Week08/librelldd/mission01/src/context/AuthContext.tsx
  • Week08/librelldd/mission01/src/enums/common.ts
  • Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteComments.ts
  • Week08/librelldd/mission01/src/hooks/queries/useGetInfiniteLpList.ts
  • Week08/librelldd/mission01/src/hooks/queries/useGetLpList.ts
  • Week08/librelldd/mission01/src/hooks/useCreateLp.ts
  • Week08/librelldd/mission01/src/hooks/useDarkMode.ts
  • Week08/librelldd/mission01/src/hooks/useDebounce.ts
  • Week08/librelldd/mission01/src/hooks/useForm.ts
  • Week08/librelldd/mission01/src/hooks/useLocalStorage.ts
  • Week08/librelldd/mission01/src/hooks/useSidebar.ts
  • Week08/librelldd/mission01/src/hooks/useThrottle.ts
  • Week08/librelldd/mission01/src/hooks/useUpdateProfile.ts
  • Week08/librelldd/mission01/src/index.css
  • Week08/librelldd/mission01/src/layouts/HomeLayout.tsx
  • Week08/librelldd/mission01/src/layouts/ProtectedLayout.tsx
  • Week08/librelldd/mission01/src/main.tsx
  • Week08/librelldd/mission01/src/pages/GoogleLoginRedirectPage.tsx
  • Week08/librelldd/mission01/src/pages/HomePage.tsx
  • Week08/librelldd/mission01/src/pages/LoginPage.tsx
  • Week08/librelldd/mission01/src/pages/LpDetailPage.tsx
  • Week08/librelldd/mission01/src/pages/MyPage.tsx
  • Week08/librelldd/mission01/src/pages/NotFoundPage.tsx
  • Week08/librelldd/mission01/src/pages/SignupPage.tsx
  • Week08/librelldd/mission01/src/pages/ThrottlePage.tsx
  • Week08/librelldd/mission01/src/src/components/Navbar.tsx
  • Week08/librelldd/mission01/src/types/auth.ts
  • Week08/librelldd/mission01/src/types/common.ts
  • Week08/librelldd/mission01/src/types/lp.ts
  • Week08/librelldd/mission01/src/utils/validate.ts
  • Week08/librelldd/mission01/src/vite-env.d.ts
  • Week08/librelldd/mission01/tsconfig.app.json
  • Week08/librelldd/mission01/tsconfig.json
  • Week08/librelldd/mission01/tsconfig.node.json
  • Week08/librelldd/mission01/vite
  • Week08/librelldd/mission01/vite.config.ts

Comment on lines +23 to +33
"react-router-dom": "^7.14.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/cli": "^4.3.0",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is @types/react-router-dom@5.3.3compatible withreact-router-dom@7.x, or should it be removed because v7 ships its own types?

💡 Result:

Yes—remove it. Reason: @types/react-router-dom@5.3.3 is the DefinitelyTyped types for React Router v5 (@types/react-router-dom is “type definitions for react-router-dom” and the 5.3.3 version is sourced from the v5-era react-router repository) [1][2]. For React Router v7, the react-router-dom package includes its own TypeScript type definitions (the package is “written in TypeScript” and re-exports from react-router; it also ships .d.ts/.d.mts artifacts) [3][4]. So keeping @types/react-router-dom@5.3.3 on top of react-router-dom@7.x will at best be redundant and at worst can conflict with the types shipped by react-router-dom. Recommendation for a v7 project: - uninstall @types/react-router-dom - rely on types shipped with react-router-dom@7.x (and react-router) [4][5]. Caveat: if you have some code specifically targeting v5 APIs, you may still need v5 compatibility packages or migrations—but that’s a library/code migration issue, not something @types/react-router-dom@5.3.3 should be used for in a v7 app [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  nl -ba "$FILE" | sed -n '1,120p'
else
  echo "File not found: $FILE" >&2
  exit 1
fi

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

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  nl -ba "$FILE" | sed -n '1,120p'
else
  echo "File not found: $FILE" >&2
  exit 1
fi

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

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
nl -ba "$FILE" | sed -n '1,120p'

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

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
nl -ba "$FILE" | sed -n '1,120p'

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

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE" >&2
  exit 1
fi
echo "== $FILE =="
# 번호 포함 출력 (nl 미사용)
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'

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

Length of output: 1541


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Week08/librelldd/mission01/package.json"
if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE" >&2
  exit 1
fi
echo "== $FILE =="
# 번호 포함 출력 (nl 미사용)
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'

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

Length of output: 1541


react-router-dom@7@types/react-router-dom@5를 같이 두는 건 제거 권장

Week08/librelldd/mission01/package.json에서 react-router-dom^7.14.0인데, @types/react-router-dom@^5.3.3(v5용 DefinitelyTyped)를 함께 두면 중복/타입 충돌 위험이 있습니다. devDependencies에서 @types/react-router-dom를 제거하세요.

수정 예시
   "devDependencies": {
@@
-    "`@types/react-router-dom`": "^5.3.3",
📝 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
"react-router-dom": "^7.14.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/cli": "^4.3.0",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
"react-router-dom": "^7.14.0",
"zod": "^4.3.6"
},
"devDependencies": {
"`@eslint/js`": "^9.39.4",
"`@tailwindcss/cli`": "^4.3.0",
"`@tailwindcss/postcss`": "^4.2.2",
"`@types/node`": "^24.12.2",
"`@types/react`": "^19.2.14",
"`@types/react-dom`": "^19.2.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/librelldd/mission01/package.json` around lines 23 - 33, Remove the
mismatched type package from package.json: delete the "`@types/react-router-dom`"
entry under devDependencies (since your dependency is "react-router-dom":
"^7.14.0"); then run your package manager (npm/yarn/pnpm) to update node_modules
and the lockfile so the change takes effect and ensure TypeScript uses the
correct v7 types.

Comment on lines +20 to +23
export const postSignin = async (body: RequestLoginDto): Promise<ResponseLoginDto> => {
console.log("로그인 요청 데이터:", body);
const { data } = await axiosInstance.post("/v1/auth/signin", body);
return data;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

로그인 요청 본문을 콘솔에 출력하면 비밀번호가 노출됩니다.

Line 21에서 요청 body를 그대로 로그로 남기고 있어 민감정보(비밀번호) 유출 위험이 있습니다. 운영 코드에서는 제거해야 합니다.

🔧 제안 수정
 export const postSignin = async (body: RequestLoginDto): Promise<ResponseLoginDto> => {
-    console.log("로그인 요청 데이터:", body);
     const { data } = await axiosInstance.post("/v1/auth/signin", body);
     return data;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const postSignin = async (body: RequestLoginDto): Promise<ResponseLoginDto> => {
console.log("로그인 요청 데이터:", body);
const { data } = await axiosInstance.post("/v1/auth/signin", body);
return data;
export const postSignin = async (body: RequestLoginDto): Promise<ResponseLoginDto> => {
const { data } = await axiosInstance.post("/v1/auth/signin", body);
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/librelldd/mission01/src/apis/auth.ts` around lines 20 - 23, Remove the
console.log that prints the entire signin request body in postSignin; do not log
sensitive fields (e.g., password from RequestLoginDto). If you need to retain a
debug log, log only non-sensitive properties (like username/email) or a masked
version (e.g., replace password with "****") before calling
axiosInstance.post("/v1/auth/signin", body) in the postSignin function and
ensure no raw body is written to stdout.

Comment on lines +142 to +190
return new Promise(async (resolve, reject) => {
try {
const refreshToken = getStorageItem(LOCAL_STORAGE_KEY.refreshToken);
if (!refreshToken) {
throw new Error("No refresh token found");
}

// refresh API 호출
const { data } = await axiosInstance.post("/v1/auth/refresh", {
refresh: refreshToken,
});

const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;

// 새로운 토큰 로컬 스토리지 업데이트
setStorageItem(LOCAL_STORAGE_KEY.accessToken, newAccessToken);
setStorageItem(LOCAL_STORAGE_KEY.refreshToken, newRefreshToken);

const cleanToken = newAccessToken.replace(/^["']|["']$/g, "").trim();

// 큐에 대기 중이던 다른 모든 401 요청들에게 새 토큰을 배포하여 일괄 성공 처리
processQueue(null, cleanToken);

// 현재의 오리지널 요청도 새 토큰으로 헤더를 직접 갱신
if (originalRequest.headers) {
if (typeof originalRequest.headers.set === 'function') {
originalRequest.headers.set('Authorization', `Bearer ${cleanToken}`);
} else {
originalRequest.headers['Authorization'] = `Bearer ${cleanToken}`;
}
}

// 재시도 요청 실행 및 해결
resolve(axiosInstance.request(originalRequest));
} catch (refreshError) {
// 토큰 재발급에 완전히 실패한 경우, 대기 중인 모든 요청들을 일괄 거절 처리
processQueue(refreshError, null);

// 스토리지 비우기 및 로그인 화면 리다이렉트
removeStorageItem(LOCAL_STORAGE_KEY.accessToken);
removeStorageItem(LOCAL_STORAGE_KEY.refreshToken);
window.location.href = "/login";

reject(refreshError);
} finally {
isRefreshing = false;
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# async Promise executor 사용 위치 확인
rg -nP 'new\s+Promise\s*\(\s*async\b' Week08/librelldd/mission01/src/apis/axios.ts

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

Length of output: 131


new Promise(async ...) 패턴 제거 필요 (Week08/librelldd/mission01/src/apis/axios.ts: 142-190)
해당 구간에서 new Promise(async (resolve, reject) => ...)가 사용되어 no-async-promise-executor(ESLint) 위반 원인이 됩니다. async executor를 없애고 async 흐름에서 return/throw로 처리하세요.

🔧 제안 수정
-      return new Promise(async (resolve, reject) =&gt; {
-        try {
+      return (async () =&gt; {
+        try {
           const refreshToken = getStorageItem(LOCAL_STORAGE_KEY.refreshToken);
           if (!refreshToken) {
             throw new Error("No refresh token found");
           }
@@
-          resolve(axiosInstance.request(originalRequest));
+          return axiosInstance.request(originalRequest);
         } catch (refreshError) {
@@
-          reject(refreshError);
+          throw refreshError;
         } finally {
           isRefreshing = false;
         }
-      });
+      })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise(async (resolve, reject) => {
try {
const refreshToken = getStorageItem(LOCAL_STORAGE_KEY.refreshToken);
if (!refreshToken) {
throw new Error("No refresh token found");
}
// refresh API 호출
const { data } = await axiosInstance.post("/v1/auth/refresh", {
refresh: refreshToken,
});
const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;
// 새로운 토큰 로컬 스토리지 업데이트
setStorageItem(LOCAL_STORAGE_KEY.accessToken, newAccessToken);
setStorageItem(LOCAL_STORAGE_KEY.refreshToken, newRefreshToken);
const cleanToken = newAccessToken.replace(/^["']|["']$/g, "").trim();
// 큐에 대기 중이던 다른 모든 401 요청들에게 새 토큰을 배포하여 일괄 성공 처리
processQueue(null, cleanToken);
// 현재의 오리지널 요청도 새 토큰으로 헤더를 직접 갱신
if (originalRequest.headers) {
if (typeof originalRequest.headers.set === 'function') {
originalRequest.headers.set('Authorization', `Bearer ${cleanToken}`);
} else {
originalRequest.headers['Authorization'] = `Bearer ${cleanToken}`;
}
}
// 재시도 요청 실행 및 해결
resolve(axiosInstance.request(originalRequest));
} catch (refreshError) {
// 토큰 재발급에 완전히 실패한 경우, 대기 중인 모든 요청들을 일괄 거절 처리
processQueue(refreshError, null);
// 스토리지 비우기 및 로그인 화면 리다이렉트
removeStorageItem(LOCAL_STORAGE_KEY.accessToken);
removeStorageItem(LOCAL_STORAGE_KEY.refreshToken);
window.location.href = "/login";
reject(refreshError);
} finally {
isRefreshing = false;
}
});
return (async () => {
try {
const refreshToken = getStorageItem(LOCAL_STORAGE_KEY.refreshToken);
if (!refreshToken) {
throw new Error("No refresh token found");
}
// refresh API 호출
const { data } = await axiosInstance.post("/v1/auth/refresh", {
refresh: refreshToken,
});
const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;
// 새로운 토큰 로컬 스토리지 업데이트
setStorageItem(LOCAL_STORAGE_KEY.accessToken, newAccessToken);
setStorageItem(LOCAL_STORAGE_KEY.refreshToken, newRefreshToken);
const cleanToken = newAccessToken.replace(/^["']|["']$/g, "").trim();
// 큐에 대기 중이던 다른 모든 401 요청들에게 새 토큰을 배포하여 일괄 성공 처리
processQueue(null, cleanToken);
// 현재의 오리지널 요청도 새 토큰으로 헤더를 직접 갱신
if (originalRequest.headers) {
if (typeof originalRequest.headers.set === 'function') {
originalRequest.headers.set('Authorization', `Bearer ${cleanToken}`);
} else {
originalRequest.headers['Authorization'] = `Bearer ${cleanToken}`;
}
}
// 재시도 요청 실행 및 해결
return axiosInstance.request(originalRequest);
} catch (refreshError) {
// 토큰 재발급에 완전히 실패한 경우, 대기 중인 모든 요청들을 일괄 거절 처리
processQueue(refreshError, null);
// 스토리지 비우기 및 로그인 화면 리다이렉트
removeStorageItem(LOCAL_STORAGE_KEY.accessToken);
removeStorageItem(LOCAL_STORAGE_KEY.refreshToken);
window.location.href = "/login";
throw refreshError;
} finally {
isRefreshing = false;
}
})();
🧰 Tools
🪛 Biome (2.4.15)

[error] 142-190: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🤖 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/librelldd/mission01/src/apis/axios.ts` around lines 142 - 190, The
code creates a new Promise with an async executor (triggering
no-async-promise-executor); replace the entire new Promise(async (resolve,
reject) => { ... }) block with an async function that directly uses
await/try/catch/finally and returns or throws—keep the same logic: read
refreshToken via getStorageItem(LOCAL_STORAGE_KEY.refreshToken), call
axiosInstance.post("/v1/auth/refresh"), update tokens via setStorageItem,
compute cleanToken, call processQueue(null, cleanToken), update
originalRequest.headers (use headers.set if available else assign), and return
axiosInstance.request(originalRequest); in the catch call
processQueue(refreshError, null), removeStorageItem for both LOCAL_STORAGE_KEY
keys and set window.location.href = "/login" then re-throw refreshError; ensure
isRefreshing is set to false in a finally block so behavior around isRefreshing,
processQueue, originalRequest, and axiosInstance is preserved.

Comment on lines +101 to +105
const { data } = await axiosInstance.post("/v1/uploads/public", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Axios 1.x 기준으로 브라우저에서 FormData 업로드 시 Content-Type: multipart/form-data를 수동으로 지정하는 것이 권장되나요? boundary 처리 관점에서 공식 권장 방식을 알려주세요.

💡 Result:

결론: 브라우저에서 Axios 1.x로 FormData 업로드할 때 Content-Type: multipart/form-data를 수동으로 지정하는 것은 boundary 처리 관점에서 권장되지 않습니다. 공식 권장 방식은 "FormData를 payload로 넘기고, Content-Type 헤더를 명시하지(또는 제거/undefined 처리) 말아 브라우저가 boundary를 포함한 올바른 Content-Type을 자동으로 세팅하게 하는 것"입니다.[1][2] 이유(경계(boundary) 처리 관점): - multipart/form-data는 파트를 구분하기 위해 Content-Typeboundary 파라미터가 필요합니다(규격상 multipart의 파트 구분자는 boundary 값으로 구성).[3] - 그런데 브라우저/Fetch/XHR에서는 FormData를 보낼 때 boundary 포함 Content-Type을 브라우저가 생성해야 합니다. 따라서 Content-Type 헤더를 직접 명시하면, 브라우저가 boundary 표현식을 포함해 헤더를 설정하는 것을 막아 boundary가 누락되거나 불일치할 수 있습니다.[2] Axios 1.x에서의 권장 패턴(브라우저): - dataFormData(또는 파일이 포함된 FormData)를 그대로 전달하세요. - 요청 헤더에 Content-Type: multipart/form-data를 넣지 마세요(넣어야 한다면 최소한 브라우저가 boundary를 붙일 수 있도록 제거/undefined로 두세요). 예시: - 나쁜 예(수동 지정): - headers: { 'Content-Type': 'multipart/form-data' } - 좋은 예(명시하지 않음): - axios.post(url, formData) 처럼 Content-Type을 생략 Axios 문서 관점의 보조 설명: - Axios는 multipart 요청을 위해 payload로 FormData 인스턴스를 넘기면 content-type을 payload 타입에 기반해 추정하며, Content-Type 헤더 설정이 "필수는 아닙니다"라고 안내합니다.[4] 주의: - Node.js 환경의 form-data 폴리필에서는 boundary를 자동으로 생성/부착하는 동작이 달라질 수 있어 수동 boundary 조합이 필요한 케이스가 생깁니다(질문은 '브라우저' 업로드이므로 이 답변의 핵심은 브라우저 기준 권장사항입니다).[5] 요약: 브라우저 + Axios 1.x + FormData 업로드라면 Content-Type: multipart/form-data를 수동 지정하지 말고, FormData를 넘겨 boundary를 브라우저가 자동으로 포함하게 하는 것이 공식적으로 안전한 방식입니다.[2][4]

Citations:


FormData 업로드에서 Content-Type: multipart/form-data 수동 지정 제거 권장 (boundary 누락/불일치 위험)
multipart/form-dataContent-Typeboundary가 포함돼야 하는데, 브라우저가 FormData를 전송할 때 boundary를 자동으로 붙이도록 Content-Type 헤더는 지정하지 않는 방식이 권장됩니다. (현재 코드처럼 수동 지정하면 boundary 누락/불일치 가능성이 있습니다.)

🔧 제안 수정
-    const { data } = await axiosInstance.post("/v1/uploads/public", formData, {
-        headers: {
-            "Content-Type": "multipart/form-data",
-        },
-    });
+    const { data } = await axiosInstance.post("/v1/uploads/public", formData);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data } = await axiosInstance.post("/v1/uploads/public", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
const { data } = await axiosInstance.post("/v1/uploads/public", formData);
🤖 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/librelldd/mission01/src/apis/lp.ts` around lines 101 - 105, The
axiosInstance.post call that sends formData currently sets "Content-Type":
"multipart/form-data" manually which can omit the required boundary; remove the
headers block so the browser/axios can set the proper Content-Type automatically
when calling axiosInstance.post("/v1/uploads/public", formData, ...). If this
code runs in a Node environment using form-data, instead obtain headers from
formData.getHeaders() and pass those to axios; otherwise simply omit the headers
field.

Comment on lines +125 to +126
onClick={handleBackdropClick}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

업로드/작성 중 모달 닫기가 가능해 상태 일관성이 깨질 수 있습니다.

Line 125-126(백드롭), Line 136-137(닫기 버튼)은 isUploading/createLpMutation.isPending일 때도 동작합니다. 진행 중에는 닫기 액션을 막아 주세요.

Also applies to: 136-137, 264-272

🤖 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/librelldd/mission01/src/components/CreateLpModal.tsx` around lines 125
- 126, The backdrop onClick (handleBackdropClick), the close button handler
(handleClose) and any other close/cancel controls must be gated when an
upload/create is in progress; modify those handlers to early-return when
isUploading || createLpMutation.isPending (or compute a single boolean like
isInProgress) and also disable or remove clickability of the UI elements (e.g.,
add disabled/aria-disabled and prevent pointer events) so users cannot trigger
close while uploading; update handleBackdropClick, handleClose and the
cancel/close buttons referenced around lines 125-126, 136-137 and 264-272 to
check that flag and no-op if true.

Comment on lines +153 to +155
const { mutate: handleToggleLike } = useMutation({
mutationFn: () => (isLiked ? deleteLpLike(lpid!) : postLpLike(lpid!)),
onMutate: async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

좋아요 토글에 중복 클릭 방지(pending guard)가 필요합니다.

현재는 요청 중에도 재클릭이 가능해서 토글 API가 연속 호출될 수 있습니다. useMutation의 pending 상태로 버튼을 disable 처리해 충돌/경합을 막아 주세요.

Also applies to: 273-275

🤖 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/librelldd/mission01/src/pages/LpDetailPage.tsx` around lines 153 -
155, The like-toggle mutation (mutate alias handleToggleLike used with
mutationFn calling postLpLike/deleteLpLike) allows repeated clicks while a
request is pending; add a pending guard by using the useMutation's isLoading (or
isPending) return value to disable the like button and prevent re-entry (early
return) when a mutation is in-flight, and update the button's disabled prop
accordingly; apply the same pattern to the other toggle mutation referenced
around lines 273-275 (the other mutate handler) so both mutations check their
respective isLoading flags before invoking mutate/mutateAsync and disable the
related UI while pending.

Comment on lines +145 to +157
<div className="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all">
{/* 모달 헤더 */}
<div className="p-6 pb-0">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
프로필 수정
</h3>
<button
onClick={closeEditModal}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
>
<X className="w-5 h-5 text-gray-400" />
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

모달에 기본 대화상자 접근성 속성이 없습니다.

Line 145 모달 루트에 role="dialog"/aria-modal/aria-labelledby가 없어 보조기기에서 맥락을 전달하기 어렵습니다.

🔧 제안 수정
-          <div className="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all">
+          <div
+            className="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="edit-profile-title"
+          >
@@
-                <h3 className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
+                <h3 id="edit-profile-title" className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
                   프로필 수정
                 </h3>
                 <button
                   onClick={closeEditModal}
+                  aria-label="프로필 수정 모달 닫기"
                   className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
                 >
📝 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
<div className="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all">
{/* 모달 헤더 */}
<div className="p-6 pb-0">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
프로필 수정
</h3>
<button
onClick={closeEditModal}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
>
<X className="w-5 h-5 text-gray-400" />
</button>
<div
className="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
role="dialog"
aria-modal="true"
aria-labelledby="edit-profile-title"
>
{/* 모달 헤더 */}
<div className="p-6 pb-0">
<div className="flex items-center justify-between mb-6">
<h3 id="edit-profile-title" className="text-xl font-black text-gray-900 dark:text-white tracking-tight">
프로필 수정
</h3>
<button
onClick={closeEditModal}
aria-label="프로필 수정 모달 닫기"
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
>
<X className="w-5 h-5 text-gray-400" />
</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/librelldd/mission01/src/pages/MyPage.tsx` around lines 145 - 157, The
modal root div (the container with classes "relative bg-white ...") in
MyPage.tsx lacks accessibility attributes; add role="dialog" and
aria-modal="true" to that root, give the heading element (the <h3> "프로필 수정") a
unique id (e.g., "editProfileTitle") and set aria-labelledby on the modal root
to that id, and ensure the close button (closeEditModal) remains
keyboard-focusable; update the JSX attributes accordingly so assistive tech can
identify the dialog.

Comment on lines +48 to +53
<button
onClick={toggleDarkMode}
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-purple-50 dark:hover:bg-purple-950/40 rounded-full transition-colors"
>
{isDark ? <Sun className="w-5 h-5 text-amber-400" /> : <Moon className="w-5 h-5" />}
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

아이콘 전용 버튼에 접근 가능한 이름(aria-label)이 없습니다.

Line 48, Line 63 버튼은 텍스트가 없어 스크린리더에서 용도를 전달하지 못합니다.

🔧 제안 수정
           <button
             onClick={toggleDarkMode}
+            aria-label={isDark ? "라이트 모드로 전환" : "다크 모드로 전환"}
             className="p-2 text-gray-600 dark:text-gray-400 hover:bg-purple-50 dark:hover:bg-purple-950/40 rounded-full transition-colors"
           >
@@
             <button
               onClick={logout}
+              aria-label="로그아웃"
               className="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
             >

Also applies to: 63-68

🤖 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/librelldd/mission01/src/src/components/Navbar.tsx` around lines 48 -
53, The icon-only theme toggle button in Navbar.tsx lacks an accessible name;
update the button that calls toggleDarkMode (uses isDark to render Sun/Moon) to
include a descriptive aria-label (e.g., aria-label="Toggle dark mode" or
aria-label based on isDark state) so screen readers convey its purpose, and
apply the same fix to the other icon-only button at the referenced block (lines
63-68) so all icon-only buttons have appropriate aria-labels.

Comment on lines +21 to +22
createdAt: Date;
updatedAt: Date;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Date 타입 대신 string 사용을 고려하세요.

API 응답에서 날짜는 일반적으로 ISO 문자열 형식으로 전달됩니다. createdAtupdatedAtDate 타입으로 선언하면, 실제 런타임 데이터(문자열)와 타입이 불일치하게 됩니다. 명시적으로 파싱하지 않는 한 타입 안정성 문제가 발생할 수 있습니다.

🔧 타입 수정 제안
 export type Lp = {
     id: number;
     title: string;
     content: string;
     thumbnail: string;
     published: boolean;
     authorId: number;
-    createdAt: Date;
-    updatedAt: Date;
+    createdAt: string;
+    updatedAt: string;
     tags: Tag[];
     likes: Likes[];
 }

날짜 문자열을 Date 객체로 변환이 필요한 경우, 별도의 파서 함수를 사용하세요:

const parseLp = (raw: Lp): LpWithDates => ({
  ...raw,
  createdAt: new Date(raw.createdAt),
  updatedAt: new Date(raw.updatedAt),
});
🤖 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/librelldd/mission01/src/types/lp.ts` around lines 21 - 22, The
createdAt and updatedAt fields on the Lp type are declared as Date but API
responses are ISO date strings; change createdAt and updatedAt in the Lp
interface to string to match runtime data, and add a dedicated parser (e.g.,
parseLp) that returns an LpWithDates variant converting those strings to Date
objects via new Date(...) when you need Date semantics; update any code that
assumed Date to call parseLp or use LpWithDates instead.

Comment on lines +11 to +17
if (
!/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test(
values.email
)
) {
errors.email = "올바른 이메일 형식이 아닙니다!";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

이메일 정규식이 지나치게 제한적이라 정상 주소를 거부할 수 있습니다.

현재 패턴은 도메인/TLD 케이스를 과도하게 제한합니다. 유효한 이메일이 실패할 수 있어 검증 실패율이 올라갑니다.

수정 예시
-    if (
-        !/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test(
-            values.email
-        )
-    ) {
+    const email = values.email.trim();
+    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
         errors.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/librelldd/mission01/src/utils/validate.ts` around lines 11 - 17, The
email regex in validate.ts (the test against values.email that sets
errors.email) is too restrictive and rejects many valid addresses; replace the
current pattern with a more permissive, well-known simple validator (for example
use a non-empty local and domain check like /^[^\s@]+@[^\s@]+\.[^\s@]+$/) or
delegate to a standard email validation utility, then update the test that
assigns errors.email accordingly so valid addresses are not falsely rejected.

Copy link
Copy Markdown
Contributor

@wantkdd wantkdd left a comment

Choose a reason for hiding this comment

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

useThrottle에서 쿨다운 중 등록한 setTimeout 콜백이 실제로 setThrottledValue를 갱신하지 않아 trailing 동작이 누락될 수 있으니, 대기 후 마지막 값이 반영되도록 보완하면 좋을 것 같습니다.
검색 UI에 제목/태그 선택 옵션이 있지만 실제 API 요청이나 queryKey에는 반영되지 않아, 사용자가 선택한 검색 기준대로 동작하도록 연결하면 더 자연스러울 것 같습니다.
Debounce와 무한 스크롤 흐름은 전반적으로 잘 연결되어 있고, 위 부분만 다듬으면 8주차 핵심 의도가 더 분명하게 살아날 것 같습니다. 수고하셨습니다!

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.

2 participants