Skip to content

피드백 내용 수정 최신화#100

Merged
shinwokkang merged 43 commits intomainfrom
dev
Feb 23, 2026
Merged

피드백 내용 수정 최신화#100
shinwokkang merged 43 commits intomainfrom
dev

Conversation

@shinwokkang
Copy link
Contributor

@shinwokkang shinwokkang commented Feb 17, 2026

📌 개요 (Summary)

  • 변경 사항에 대한 간략한 요약을 적어주세요.
  • 관련 이슈가 있다면 링크를 걸어주세요 (예: [fix] cicd / build 문제 #123).

🛠️ 변경 사항 (Changes)

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 문서 업데이트
  • 기타 (설명: )

📸 스크린샷 (Screenshots)

(UI 변경 사항이 있다면 첨부해주세요)

✅ 체크리스트 (Checklist)

  • 빌드가 성공적으로 수행되었나요? (pnpm build)
  • 린트 에러가 없나요? (pnpm lint)
  • 불필요한 콘솔 로그나 주석을 제거했나요?

Summary by CodeRabbit

  • New Features

    • Floating Action Button (FAB) added across multiple pages for quick actions
    • Multi-step signup flow with centralized signup state and toast notifications
    • Logout page that signs out and redirects
    • Auth guard and store-driven login modal to gate protected pages
  • Improvements

    • Entry animations for modals and smoother transitions
    • Pagination for group notices
    • Enhanced signup/profile validation, image upload flow, and toast feedback
    • Widespread layout, spacing, responsive and hover/cursor polish across the UI

@vercel
Copy link

vercel bot commented Feb 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment Feb 23, 2026 5:26am
checkmo Ready Ready Preview, Comment Feb 23, 2026 5:26am

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds a SignupContext/provider and step-based signup pages, refactors join/signup steps and hooks to use the context (zod validation, presigned S3 uploads), overhauls auth/API flows and types, adds FloatingFab and auth guard/logout, plus many UI/layout and styling tweaks and two package deps.

Changes

Cohort / File(s) Summary
Manifest / Dependencies
package.json
Added zod (^4.3.6) to dependencies and baseline-browser-mapping (^2.9.19) to devDependencies.
Signup Context & Routing
src/contexts/SignupContext.tsx, src/app/(public)/signup/[step]/page.tsx, src/app/(public)/signup/layout.tsx, src/app/(public)/signup/page.tsx
New SignupProvider and useSignup hook; provider wrapped in signup layout; new per-step signup page; root signup reroutes to /signup/terms.
Join / Signup Steps & Hooks
src/components/base-ui/Join/**, src/components/base-ui/Join/steps/**
Refactored to use SignupContext: added zod validation, toast usage, async authService calls, S3 presigned upload, changed hook return shapes (e.g., useProfileImage, useProfileSetup, useEmailVerification) and UI/flow adjustments.
Auth / API / Types / Store
src/services/authService.ts, src/lib/api/client.ts, src/lib/api/endpoints.ts, src/types/auth.ts, src/store/useAuthStore.ts, src/components/auth/AuthProvider.tsx
Switched to profile-driven auth flow, introduced AUTH_ENDPOINTS, expanded authService APIs (signup, verify, confirm, checkNickname, getProfile, logout, presigned S3 helpers), added ApiResponse and extended User type, added auth store flags/modal controls and updated AuthProvider init.
Auth Guard & Logout
src/hooks/useAuthGuard.ts, src/app/(main)/setting/logout/page.tsx, src/app/(main)/profile/mypage/page.tsx, src/app/(main)/setting/layout.tsx
New useAuthGuard hook to gate pages and show login modal; new Logout page that calls authService.logout then navigates home; applied gating to MyPage and Settings layout.
Floating FAB & Usage
src/components/base-ui/Float.tsx, multiple pages (src/app/**, src/components/**)
New FloatingFab component and replaced bespoke floating buttons across news, groups, stories, books, etc.
Signup UI Integration & Controls
src/components/base-ui/Join/JoinLayout.tsx, src/components/base-ui/Join/JoinButton.tsx, src/components/base-ui/Join/JoinInput.tsx
JoinLayout consumes signup toast; JoinInput adds optional description; JoinButton computes default sizing and adds hover styles when not disabled.
Profile / Signup Steps logic
src/components/base-ui/Join/steps/* and related hooks
Multiple step components now use context for state, added validation flows, async interactions, new loading states, and adjusted UI/controls (e.g., PasswordEntry, ProfileImage, ProfileSetup, TermsAgreement).
Global Styles & Animations
src/app/globals.css, src/components/base-ui/Login/LoginModal.module.css, src/components/layout/SearchModal.tsx
Added keyframes/utility classes animate-fade-in and animate-slide-down; login modal slideUp animation; applied new animation utilities to modals.
API Client & Endpoints
src/lib/api/client.ts, src/lib/api/endpoints.ts
client: removed token auto-injection, added credentials: "include", 401 handling; post/put accept optional bodies. Endpoints: replaced LOGIN_URL with consolidated AUTH_ENDPOINTS (includes IMAGE_UPLOAD helper).
Wide UI/Layout / Styling tweaks
many src/components/**, src/app/**
Numerous responsive/layout/class adjustments, hover/cursor affordances, grid→flex adjustments, small behavioral tweaks (e.g., prepend new comments). Review attention: changed hook return shapes and auth-related service calls.
New / Modified Component Signatures
src/components/base-ui/Settings/EditProfile/CategorySelector.tsx, src/components/base-ui/Settings/EditProfile/ProfileImageSection.tsx, src/components/base-ui/Join/JoinInput.tsx, src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts
CategorySelector and ProfileImageSection now accept/return structured props; JoinInput adds description?; useProfileImage expands its returned API (profileImage, isProfileImageSet, handleFinish, isLoading, isValid).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Route as /signup/[step]
    participant SignupProvider
    participant AuthService
    participant API as Backend

    User->>Browser: Open /signup
    Browser->>Route: navigate (root redirects to /signup/terms)
    Route->>SignupProvider: render current step within provider
    User->>Route: submit form data (email/password/profile/terms)
    Route->>SignupProvider: update context state (set fields)
    Route->>AuthService: call verifyEmail / confirmEmail / getPresignedUrl / additionalInfo
    AuthService->>API: HTTP requests (AUTH_ENDPOINTS.*)
    API-->>AuthService: responses (success / error)
    AuthService-->>Route: result (presignedUrl, imageUrl, ok)
    Route->>SignupProvider: update state or showToast
    User->>Route: finish final step -> Route calls AuthService.signup/additionalInfo
    AuthService->>API: submit accumulated data
    API-->>AuthService: success
    AuthService-->>Browser: navigate / complete flow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✨ feat, 📦 build

Poem

🐰 I hopped into code with a joyful twitch,

Wrapped signup steps in a soft provider stitch,
Toasts and presigned pics, nicknames checked with care,
FloatingFab bounces—buttons now everywhere! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title '피드백 내용 수정 최신화' is vague and generic. It translates to 'Update feedback content modifications' but provides no specific information about what changes were made or which components/features were affected. Revise the title to be more specific about the main changes, such as 'Refactor signup flow with context management and auth service integration' or 'Apply responsive design updates and enhance UI/UX across multiple components'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @shinwokkang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 사용자 회원가입 과정을 Next.js의 동적 라우팅과 SignupContext를 활용하여 다단계로 리팩토링하고, 전역 상태 관리를 도입하는 데 중점을 두었습니다. 또한, 여러 핵심 페이지의 UI/UX를 개선하여 반응성과 시각적 일관성을 높였습니다. 새로운 의존성 추가와 CSS 애니메이션 도입을 통해 전반적인 애플리케이션의 기능성과 사용자 상호작용을 강화했습니다.

Highlights

  • 회원가입 플로우 리팩토링: Next.js 동적 라우팅과 SignupContext를 활용하여 다단계 회원가입 절차를 재구성하고, 전역 상태 관리를 도입했습니다.
  • UI/UX 개선: 도서 상세, 홈, 스토리, 마이페이지 등 여러 페이지의 레이아웃과 반응형 디자인을 개선하여 사용자 경험을 향상시켰습니다.
  • 새로운 의존성 추가 및 잠금 파일 업데이트: baseline-browser-mapping 의존성을 추가하고 pnpm-lock.yaml 파일을 업데이트했습니다.
  • CSS 애니메이션 도입: 전역 CSS에 fade-inslide-down 애니메이션을 추가하여 검색 모달 등 UI 요소에 동적인 효과를 부여했습니다.
  • 폼 유효성 검사 및 사용자 피드백 강화: 회원가입 과정에서 닉네임 유효성 검사, 중복 확인 피드백, 그리고 전역 토스트 알림 시스템을 통합하여 사용자에게 명확한 안내를 제공합니다.
  • 공지사항 페이지 기능 개선: 그룹 공지사항 페이지에 페이지네이션 기능을 추가하고, 공지사항 작성 버튼의 위치와 스타일을 조정했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • package.json
    • baseline-browser-mapping 의존성을 추가했습니다.
  • pnpm-lock.yaml
    • 새로운 의존성을 반영하고 여러 패키지 해상도에서 libc 필드를 제거하여 잠금 파일을 업데이트했습니다.
  • src/app/(main)/books/[id]/page.tsx
    • BookStoryCardBookStoryCardLarge로 교체하고 도서 이야기 레이아웃을 조정했습니다.
  • src/app/(main)/page.tsx
    • 사용자 데이터 및 섹션 헤더의 스타일을 조정하고 들여쓰기를 수정했습니다.
  • src/app/(main)/stories/new/page.tsx
    • 공지사항 생성 레이아웃을 수정하고, 모바일 뒤로가기 버튼을 제거했으며, 패딩을 조정하고 모달 상태에 따라 헤더를 조건부 렌더링하도록 변경했습니다.
  • src/app/(main)/stories/page.tsx
    • 스토리 카드 레이아웃을 업데이트하고, shrink-0 클래스를 추가했으며, 헤더 패딩을 조정했습니다.
  • src/app/(public)/signup/[step]/page.tsx
    • 다단계 회원가입을 위한 새로운 동적 라우트를 추가하여 step 매개변수에 따라 다른 컴포넌트를 렌더링하도록 했습니다.
  • src/app/(public)/signup/layout.tsx
    • 자식 컴포넌트를 SignupProvider로 감싸 전역 회원가입 상태를 관리하도록 했습니다.
  • src/app/(public)/signup/page.tsx
    • 이전의 정적 단계 관리를 제거하고 다단계 회원가입 플로우를 시작하기 위해 /signup/terms로 리다이렉트하도록 변경했습니다.
  • src/app/globals.css
    • fade-inslide-down 키프레임 애니메이션과 유틸리티 클래스를 추가했습니다.
  • src/app/groups/[id]/admin/notice/new/page.tsx
    • 공지사항 생성 페이지의 임포트, 상태 관리 및 UI를 업데이트했으며, 투표, 책장, 이미지 옵션을 포함했습니다. 모바일 뒤로가기 버튼을 제거하고 패딩을 조정했습니다.
  • src/app/groups/[id]/notice/page.tsx
    • 공지사항에 페이지네이션 기능을 구현하고 '공지사항 작성' 버튼의 위치와 스타일을 조정했습니다.
  • src/components/base-ui/BookStory/bookstory_card.tsx
    • 다양한 화면 크기에서 더 잘 보이도록 콘텐츠 스타일을 조정했습니다.
  • src/components/base-ui/Comment/comment_list.tsx
    • 답글 기능의 입력 필드와 버튼 너비를 조정했습니다.
  • src/components/base-ui/Comment/comment_section.tsx
    • 새로운 댓글이 목록 앞에 추가되도록 댓글 순서를 변경했습니다.
  • src/components/base-ui/Join/JoinButton.tsx
    • JoinButton이 props에 따라 너비/높이/패딩 클래스를 동적으로 적용하고 호버 효과를 추가하도록 개선했습니다.
  • src/components/base-ui/Join/JoinInput.tsx
    • description prop을 추가하고 입력 필드의 동적 스타일링을 개선했습니다.
  • src/components/base-ui/Join/JoinLayout.tsx
    • SignupContext를 사용하여 전역 토스트 알림 시스템을 통합했습니다.
  • src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsx
    • 로컬 토스트 상태를 제거하고 토스트 알림을 위해 SignupContext를 사용하도록 변경했습니다.
  • src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx
    • 상태 관리를 위해 SignupContext를 통합하고 비밀번호 입력 필드에 설명을 추가했습니다.
  • src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx
    • SignupContext를 통합하고, 유효성 검사를 위한 토스트 알림을 추가했으며, 모바일 단계에 대한 버튼 로직을 개선했습니다.
  • src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts
    • 상태 및 토스트 관리를 위해 SignupContext를 통합하고, 명시적인 이미지 선택 추적을 위한 isProfileImageSet을 추가했습니다.
  • src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx
    • SignupContext를 통합하고, 유효성 검사를 위한 토스트 알림을 추가했으며, 닉네임 입력 유효성 검사 및 중복 확인 피드백을 개선했습니다.
  • src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts
    • 상태 및 토스트 관리를 위해 SignupContext를 통합하고, 닉네임 유효성 검사 로직 및 소개와 이름의 글자 수 제한을 추가했습니다.
  • src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts
    • 완료 화면에 사용자 데이터를 가져오기 위해 SignupContext를 통합했습니다.
  • src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx
    • 동의 상태를 위해 SignupContext를 통합하고, 상세 약관 내용을 추가했으며, 약관 세부 정보를 위한 모달과 유사한 뷰를 구현했습니다.
  • src/components/base-ui/Join/steps/useEmailVerification.ts
    • 상태 및 토스트 관리를 위해 SignupContext를 통합하여 로컬 상태를 간소화했습니다.
  • src/components/base-ui/MyPage/MyBookStoryList.tsx
    • 마이페이지의 도서 이야기 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/MyPage/MyMeetingList.tsx
    • 마이페이지의 모임 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/MyPage/MyNotificationList.tsx
    • 마이페이지의 알림 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/MyPage/MyPageBreadcrumb.tsx
    • 브레드크럼의 패딩과 화살표 스타일을 조정했습니다.
  • src/components/base-ui/MyPage/UserProfile.tsx
    • 사용자 프로필 섹션의 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/MyPage/items/MyMeetingCard.tsx
    • 모임 카드의 스타일을 조정했으며, 텍스트 잘림 및 아이콘 크기를 포함합니다.
  • src/components/base-ui/MyPage/items/MyNotificationItem.tsx
    • 알림 항목의 스타일을 조정했으며, 텍스트 잘림 및 간격을 포함합니다.
  • src/components/base-ui/Profile/BookStoryList.tsx
    • 프로필 페이지의 도서 이야기 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/Profile/LibraryList.tsx
    • 프로필 페이지의 라이브러리 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/Profile/MeetingList.tsx
    • 프로필 페이지의 모임 목록 레이아웃과 패딩을 조정했습니다.
  • src/components/base-ui/Profile/OtherUser/ProfileBreadcrumb.tsx
    • 브레드크럼의 패딩과 화살표 스타일을 조정했습니다.
  • src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx
    • 소개 텍스트에 line-clamp를 추가했습니다.
  • src/components/base-ui/Profile/items/MeetingCard.tsx
    • 모임 카드의 스타일을 조정했으며, 텍스트 잘림 및 글꼴 크기를 포함합니다.
  • src/components/base-ui/Search/search_bookresult.tsx
    • 최대 너비 제약을 제거하고, 패딩을 조정했으며, 조건부 클래스 결합을 간소화했습니다.
  • src/components/layout/BookSelectModal.tsx
    • 모달 위치와 패딩을 조정했습니다.
  • src/components/layout/Header.tsx
    • 태블릿 및 데스크톱 헤더 패딩을 조정했습니다.
  • src/components/layout/SearchModal.tsx
    • CSS 애니메이션을 추가하고, 레이아웃을 조정했으며, 검색 모달의 반응형 스타일링을 개선했습니다.
  • src/contexts/SignupContext.tsx
    • 사용자 입력, 유효성 검사, 토스트 알림을 포함한 다단계 회원가입 프로세스의 전역 상태를 관리하기 위한 새로운 컨텍스트를 추가했습니다.
Activity
  • shinwokkang 님이 PR을 생성하여 피드백 내용을 수정하고 최신화했습니다.
  • 회원가입 플로우를 동적 라우팅과 전역 상태 관리(SignupContext)를 사용하도록 대대적으로 리팩토링했습니다.
  • 다양한 페이지(도서 상세, 홈, 스토리, 마이페이지 등)의 UI/UX를 개선하여 반응형 디자인과 시각적 일관성을 높였습니다.
  • 새로운 baseline-browser-mapping 의존성을 추가하고 pnpm-lock.yaml을 업데이트했습니다.
  • 전역 CSS에 fade-inslide-down 애니메이션을 추가하여 UI 상호작용에 동적인 효과를 부여했습니다.
  • 회원가입 과정에서 닉네임 유효성 검사, 중복 확인, 전역 토스트 알림 등 사용자 피드백 시스템을 강화했습니다.
  • 그룹 공지사항 페이지에 페이지네이션 기능을 추가하고, 공지사항 작성 버튼의 위치와 스타일을 조정했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/app/groups/[id]/admin/notice/new/page.tsx (2)

114-118: ⚠️ Potential issue | 🟠 Major

Memory leak: cleanup effect captures a stale imagePreviews reference.

The empty dependency array [] means the cleanup function closes over the initial empty imagePreviews. When the component unmounts, it revokes zero URLs while the actual object URLs remain leaked. Either track URLs in a ref, or include imagePreviews in the dependency array (revoking only on change/unmount).

🐛 Proposed fix using a ref to track URLs
+ const imageUrlsRef = useRef<string[]>([]);
+
  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (!files || files.length === 0) return;

    const urls = Array.from(files).map((file) => URL.createObjectURL(file));
+   imageUrlsRef.current = [...imageUrlsRef.current, ...urls];
    setImagePreviews((prev) => [...prev, ...urls]);
  };

- // 생성한 object URL 정리
- useEffect(() => {
-   return () => {
-     imagePreviews.forEach((url) => URL.revokeObjectURL(url));
-   };
- }, []);
+ // 생성한 object URL 정리
+ useEffect(() => {
+   return () => {
+     imageUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
+   };
+ }, []);

Also update the delete handler to remove from the ref:

  onClick={() => {
    const urlToRemove = imagePreviews[index];
    URL.revokeObjectURL(urlToRemove);
+   imageUrlsRef.current = imageUrlsRef.current.filter((u) => u !== urlToRemove);
    setImagePreviews((prev) => prev.filter((_, i) => i !== index));
  }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/admin/notice/new/page.tsx around lines 114 - 118, The
cleanup effect currently closes over a stale imagePreviews value in useEffect;
change to track previews in a ref (e.g., imagePreviewsRef) and update all places
that add/remove previews (the preview creation logic and the delete handler) to
push/remove URLs from imagePreviewsRef.current; then update the useEffect
cleanup to iterate imagePreviewsRef.current and revokeObjectURL for each URL
(and clear the ref) so object URLs are revoked correctly on unmount or change.

367-373: ⚠️ Potential issue | 🟡 Minor

Button labeled "임시저장" (temp save) but handler only navigates back.

handleCancel (called by this button) simply calls router.back(), discarding all form state. If temp-save functionality is intended, this is a functional bug. If not, the label is misleading to users.

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

In `@src/app/groups/`[id]/admin/notice/new/page.tsx around lines 367 - 373, The
"임시저장" button currently calls handleCancel which only invokes router.back() and
thus discards form state; either implement real temp-save logic or change the
label. Update handleCancel (or create a new handler like handleTempSave) to
serialize the form data (from the form state/hooks used in this page) and
persist it (e.g., localStorage, indexedDB, or call the existing saveDraft API)
and then navigate or notify the user, or alternatively rename the button to "취소"
and keep router.back() if temporary save is not intended; reference handleCancel
and the button element rendering to locate and change behavior.
src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx (1)

18-19: ⚠️ Potential issue | 🟠 Major

Password validation does not match the displayed rules.

The description on line 29 states "비밀번호는 6-12자, 영어 최소 1자 이상, 특수문자 최소 1자 이상" (6–12 chars, at least 1 letter, at least 1 special char), but the isMatch check on lines 18–19 only verifies length > 0 && length <= 20. This means:

  1. Passwords shorter than 6 characters are accepted.
  2. No upper bound of 12 characters is enforced (allows up to 20).
  3. No check for at least one English letter or one special character.

Users will see the rules but can bypass them entirely.

Suggested fix
- const isMatch =
-   password.length > 0 && password.length <= 20 && password === confirmPassword;
+ const hasLetter = /[a-zA-Z]/.test(password);
+ const hasSpecialChar = /[^a-zA-Z0-9]/.test(password);
+ const isMatch =
+   password.length >= 6 &&
+   password.length <= 12 &&
+   hasLetter &&
+   hasSpecialChar &&
+   password === confirmPassword;

Also update maxLength on both inputs from 20 to 12 to be consistent.

Also applies to: 29-29

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

In `@src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx` around
lines 18 - 19, The password check is inconsistent with the UI rules: update the
isMatch logic in PasswordEntry.tsx (the isMatch constant that uses password and
confirmPassword) to enforce length >=6 and <=12, require at least one English
letter and at least one special character (e.g. with regex tests), and still
ensure password === confirmPassword; also change the maxLength prop on both
input elements (the password and confirmPassword inputs) from 20 to 12 so the
inputs match the validation.
src/app/(main)/stories/new/page.tsx (1)

35-35: ⚠️ Potential issue | 🟡 Minor

Remove console.log before merging.

The PR checklist specifically mentions removing unnecessary console logs. This debug statement should be replaced with the actual save logic or removed.

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

In `@src/app/`(main)/stories/new/page.tsx at line 35, Remove the debug
console.log("저장:", { title, detail }); and replace it with the actual save flow:
call your save handler (e.g., await saveStory({ title, detail }) or dispatch the
appropriate action) or invoke the API client inside the same handler (handleSave
/ onSubmit) and handle success/error states, instead of leaving the console log;
ensure you reference the title and detail variables when calling the save
function and remove the console.log line entirely.
src/components/layout/SearchModal.tsx (1)

153-166: ⚠️ Potential issue | 🟡 Minor

href="#" on the Link will scroll to top and add a hash to the URL.

Using href="#" with Next.js Link will trigger a client-side navigation to #, scrolling the page to the top. If this is a placeholder, consider using a <button> styled as a link, or set a real destination URL.

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

In `@src/components/layout/SearchModal.tsx` around lines 153 - 166, In
SearchModal.tsx the Link with href="#" in the "알라딘 랭킹 더 보러가기" element causes an
undesired hash navigation; update the Link usage inside the SearchModal
component so it either points to the real destination URL (replace href="#" with
the real path) or, if it is meant to be a non-navigation action, replace the
Link with a semantic <button> styled like a link and handle the click via
onClick, or keep Link but preventDefault and call the intended handler; locate
the Link element (the one with className "flex items-center gap-1
text-white...") and apply one of these fixes.
src/components/base-ui/Search/search_bookresult.tsx (1)

62-64: ⚠️ Potential issue | 🟡 Minor

Change flex1 to flex-1flex1 is not a valid Tailwind class. The standard flexbox utility in Tailwind is flex-1 (with a hyphen).

Proposed fix
-          <p className="flex1 h-full text-Gray-4 body_1_2 line-clamp-6">
+          <p className="flex-1 h-full text-Gray-4 body_1_2 line-clamp-6">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Search/search_bookresult.tsx` around lines 62 - 64,
The className on the paragraph uses an invalid Tailwind token "flex1"; update
the class string in the Search book result component (the <p> rendering
clippedDetail in search_bookresult.tsx) to replace "flex1" with the correct
Tailwind utility "flex-1" so the element gets proper flex sizing (i.e., change
the className value that currently contains "flex1 h-full text-Gray-4 body_1_2
line-clamp-6" to use "flex-1" instead).
src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts (1)

8-21: ⚠️ Potential issue | 🟡 Minor

Remove console.log statements before merging to main.

The PR checklist explicitly asks to verify removal of unnecessary console logs. Lines 9, 14, and 19 contain debug console.log calls that should be removed. Additionally, handleSearchMeeting and handleCreateMeeting are stubs with commented-out routing — consider adding a TODO or tracking issue.

   const handleSearchMeeting = () => {
-    console.log("Search Meeting clicked");
-    // router.push('/meeting/search');
+    // TODO: implement meeting search navigation
+    router.push('/meeting/search');
   };
 
   const handleCreateMeeting = () => {
-    console.log("Create Meeting clicked");
-    // router.push('/meeting/create');
+    // TODO: implement meeting creation navigation
+    router.push('/meeting/create');
   };
 
   const handleUseWithoutMeeting = () => {
-    console.log("Use Without Meeting clicked");
     router.push("/");
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts` around
lines 8 - 21, Remove the debug console.log statements from handleSearchMeeting,
handleCreateMeeting, and handleUseWithoutMeeting; keep the router.push("/") call
in handleUseWithoutMeeting but delete its console.log, and in
handleSearchMeeting and handleCreateMeeting replace the commented-out
router.push stubs with a TODO comment (e.g., "// TODO: implement navigation to
/meeting/search" and "// TODO: implement navigation to /meeting/create") so the
intent is tracked; reference the functions handleSearchMeeting,
handleCreateMeeting, and handleUseWithoutMeeting in useSignupComplete.ts when
making these changes.
src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts (1)

59-66: ⚠️ Potential issue | 🔴 Critical

Blob URL cleanup on unmount will invalidate the context-held image reference.

Since profileImage now lives in SignupContext (persists across step navigation), revoking the blob URL when this hook's host component unmounts will break the image if the user navigates away from this step and returns. The context will still hold the now-revoked blob URL.

The cleanup on change (case a — old URL revoked when a new image is set) is correct, but the cleanup on unmount (case b) should not revoke the current URL since it may still be needed.

Suggested fix: only revoke the previous URL, not on unmount
- // 메모리 누수 방지를 위한 cleanup
- useEffect(() => {
-   return () => {
-     if (profileImage && profileImage.startsWith("blob:")) {
-       URL.revokeObjectURL(profileImage);
-     }
-   };
- }, [profileImage]);
+ // Revoke *previous* blob URL when profileImage changes, but not on unmount
+ const prevImageRef = useRef<string | null>(null);
+ useEffect(() => {
+   if (prevImageRef.current && prevImageRef.current.startsWith("blob:") && prevImageRef.current !== profileImage) {
+     URL.revokeObjectURL(prevImageRef.current);
+   }
+   prevImageRef.current = profileImage;
+ }, [profileImage]);

You'll also need to add useRef to the imports:

-import { useEffect } from "react";
+import { useEffect, useRef } from "react";

Note: Consider moving blob URL lifecycle management to the SignupContext (e.g., in resetSignup) for a single source of truth.

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

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts` around
lines 59 - 66, The cleanup currently revokes the blob URL on unmount which
invalidates the context-held profileImage; modify useProfileImage so it only
revokes the previous blob URL when profileImage changes (track the previous URL
with a ref, e.g., prevBlobRef) and do not revoke the current profileImage on
component unmount; add useRef to imports and update the effect to revoke
prevBlobRef.current if it startsWith("blob:") when a new profileImage is set,
then assign prevBlobRef.current = profileImage, and remove the unmount revoke
logic (or alternatively centralize blob lifecycle in SignupContext/resetSignup).
🧹 Nitpick comments (28)
src/app/groups/[id]/admin/notice/new/page.tsx (2)

170-250: Vote option items are hardcoded to exactly 4 — consider making this dynamic.

The vote items array is initialized with 4 empty strings and the UI always renders indices [0, 1, 2, 3]. Users cannot add or remove vote options. If this is intentional for MVP, a brief comment would help; otherwise, consider adding add/remove functionality.

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

In `@src/app/groups/`[id]/admin/notice/new/page.tsx around lines 170 - 250, The
vote UI currently maps a hardcoded [0,1,2,3] instead of the voteItems state
(voteItems and handleVoteItemChange), preventing add/remove of options; update
the JSX to map over voteItems so the list size reflects state, add handler
functions (e.g., addVoteItem, removeVoteItem) that update voteItems immutably,
render an "add" control and a per-item "remove" control (respecting any min/max
constraints you want), and ensure handleVoteItemChange updates the correct index
in voteItems so dynamic additions/removals work correctly.

146-148: Mixed min-h-63 / h-63 toggle may cause layout jumps.

When selectedOption is "vote", the container switches to min-h-63 (growable), otherwise h-63 (fixed). On tablet+ the height is always t:h-190. This means on mobile, switching between vote and non-vote modes causes an abrupt height change. If this is the intended responsive behavior, no action needed — just flagging for awareness.

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

In `@src/app/groups/`[id]/admin/notice/new/page.tsx around lines 146 - 148, Mixed
mobile height classes cause abrupt layout jumps when toggling selectedOption;
update the container class interpolation in page.tsx (the div using
selectedOption === "vote") so mobile uses a consistent height rule — e.g.,
replace the conditional "min-h-63" / "h-63" with a single consistent class like
"min-h-63" for both branches (or "h-63" for both) or apply a transition class to
smooth changes; ensure tablet rule t:h-190 remains unchanged and only adjust the
mobile classes around the selectedOption check.
src/app/(main)/stories/page.tsx (1)

37-37: Flex-wrap layout may produce inconsistent column counts on tablet.

Below the d: breakpoint, the layout uses flex flex-wrap justify-center without explicit item widths, so the number of columns depends entirely on the intrinsic width of BookStoryCardLarge. If the card width doesn't cleanly divide the container, you may get uneven rows (e.g., 3 cards then 1 card, or 2 then 2 with large gaps). Consider adding a responsive width or basis-* on the card wrapper for tablet to ensure a predictable column count.

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

In `@src/app/`(main)/stories/page.tsx at line 37, The flex-wrap container div (the
element with classes "flex flex-wrap gap-5 mt-6 justify-center d:grid ...")
allows unpredictable column counts on tablet because child widths aren't
constrained; update the wrapper around each BookStoryCardLarge (or the
BookStoryCardLarge component itself) to include a responsive width/basis (e.g.,
tablet breakpoint classes like a basis-* or w-*/sm:w-*/md:w-* pattern) so the
number of columns is deterministic below the d: breakpoint and rows don't end up
uneven.
src/app/globals.css (1)

145-148: Duplicate breakpoint definitions.

--breakpoint-t and --breakpoint-d are already defined in the @theme inline block at lines 35–36. This second @theme block is redundant and could cause confusion about which takes precedence.

Suggested fix
-@theme {
-  --breakpoint-t: 768px;
-  --breakpoint-d: 1440px;
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 145 - 148, The second `@theme` block
duplicates CSS custom properties --breakpoint-t and --breakpoint-d; remove this
redundant `@theme` block (or merge any non-duplicate rules into the existing
`@theme` inline block) so there’s a single source of truth for the breakpoints and
avoid precedence confusion—look for the existing `@theme` inline block that
already defines --breakpoint-t and --breakpoint-d and delete the later `@theme`
block containing those same variables.
package.json (1)

28-28: autoprefixer may be unnecessary with Tailwind CSS v4.

Tailwind CSS v4 uses Lightning CSS internally, which handles vendor prefixes automatically. The autoprefixer dependency is likely redundant.

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

In `@package.json` at line 28, The package.json currently includes an
"autoprefixer" dependency that is likely redundant with Tailwind CSS v4
(Lightning CSS) — remove the "autoprefixer" entry from package.json and
uninstall it from devDependencies, then update any PostCSS or build
configuration that references the autoprefixer plugin (e.g., postcss.config.js
or any webpack/rollup/vite plugins) to remove that plugin invocation so builds
rely on Tailwind/Lightning CSS for vendor prefixing; run a clean install and a
test build to confirm no postcss references remain.
src/components/base-ui/Profile/LibraryList.tsx (1)

16-16: Consider extracting this dense className for readability.

This single className string packs mobile grid, tablet flex-wrap, and desktop gap overrides along with child-width selectors into one line. While functional, it's hard to scan and maintain. A brief inline comment or splitting across lines would help.

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

In `@src/components/base-ui/Profile/LibraryList.tsx` at line 16, The long, dense
className on the wrapping div in the LibraryList component should be extracted
for readability; refactor the class string from the JSX element into a named
constant (e.g., libraryContainerClass) or use a helper like clsx/twMerge and
break the classes across multiple lines with a brief inline comment indicating
which block targets mobile/tablet/desktop and child selectors; update the div to
reference that constant (preserving all existing classes including
[&>div]:w-full and md:[&>div]:w-[244px]) so behavior is unchanged but the markup
is much easier to scan and maintain.
src/components/layout/BookSelectModal.tsx (1)

62-80: onClose in the dependency array may cause unnecessary effect re-runs.

If the parent doesn't wrap onClose in useCallback, this effect will re-register the escape handler and toggle overflow on every render. Consider using a ref to hold the latest onClose or memoizing it at the call site.

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

In `@src/components/layout/BookSelectModal.tsx` around lines 62 - 80, The effect
currently depends on onClose which can be unstable and cause repeated
re-registration of handleEscape and toggling of document.body.style.overflow;
change to store the latest onClose in a ref (e.g., onCloseRef) and update that
ref in a separate useEffect, then in the main useEffect (the one that sets
overflow and adds/removes handleEscape) only depend on isOpen; inside
handleEscape call the ref (onCloseRef.current()) instead of onClose.
Alternatively, memoize onClose at the parent, but prefer the ref approach to
avoid unnecessary effect re-runs while keeping handleEscape, useEffect, isOpen
and onCloseRef as the locating symbols.
src/components/base-ui/MyPage/MyPageBreadcrumb.tsx (1)

6-6: Note: lg:px-[20px] differs from sibling components' lg:px-0.

Other MyPage list components (MyMeetingList, etc.) use lg:px-0 at the large breakpoint, while this breadcrumb retains lg:px-[20px]. This may be intentional given the wider max-w-[1440px] container, but verify it's the desired behavior to avoid inconsistent edge spacing.

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

In `@src/components/base-ui/MyPage/MyPageBreadcrumb.tsx` at line 6, Breadcrumb
container in MyPageBreadcrumb.tsx uses lg:px-[20px] which differs from sibling
components that use lg:px-0; update the className on the root div (the element
with className containing max-w-[1440px] and border-b) to use lg:px-0 for
consistent large-breakpoint edge spacing (or confirm and document why
lg:px-[20px] should remain if it's intentional).
src/components/base-ui/Profile/OtherUser/ProfileBreadcrumb.tsx (1)

4-24: Misleading component name: MyPageBreadcrumb used for the other-user profile page.

This component lives under Profile/OtherUser/ and renders "다른 사람 페이지", yet is named MyPageBreadcrumb — identical to the component in MyPage/MyPageBreadcrumb.tsx. This is confusing and could lead to import errors. Consider renaming to ProfileBreadcrumb or OtherUserBreadcrumb.

Additionally, this component is nearly a 1:1 duplicate of MyPage/MyPageBreadcrumb.tsx (only the label text differs). A shared Breadcrumb component accepting a label prop would eliminate the duplication.

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

In `@src/components/base-ui/Profile/OtherUser/ProfileBreadcrumb.tsx` around lines
4 - 24, The component is misnamed and duplicates another component: rename
MyPageBreadcrumb to OtherUserBreadcrumb (or ProfileBreadcrumb) to match its
location and exported identifier, update the component declaration and any
exports/imports that reference MyPageBreadcrumb, and to remove duplication
extract a reusable Breadcrumb component that accepts a label prop (e.g., create
Breadcrumb({ label }) used by OtherUserBreadcrumb and MyPageBreadcrumb) so both
pages render the same structure with different label text.
src/app/(main)/books/[id]/page.tsx (1)

62-78: Redundant wrapper — BookStoryCardLarge already accepts an onClick prop.

The wrapping <div> adds click handling and cursor-pointer, but BookStoryCardLarge already supports an onClick prop and applies cursor-pointer internally. Pass onClick directly to the component to eliminate the unnecessary wrapper and avoid double cursor styling.

Proposed fix
          {relatedStories.map((story) => (
-            <div
+            <BookStoryCardLarge
               key={story.id}
-              onClick={() => router.push(`/stories/${story.id}`)}
-              className="cursor-pointer"
-            >
-              <BookStoryCardLarge
-                authorName={story.authorName}
-                createdAt={story.createdAt}
-                viewCount={story.viewCount}
-                coverImgSrc={story.bookImageUrl}
-                title={story.title}
-                content={story.content}
-                likeCount={story.likeCount}
-                commentCount={story.commentCount}
-                subscribeText="구독"
-              />
-            </div>
+              onClick={() => router.push(`/stories/${story.id}`)}
+              authorName={story.authorName}
+              createdAt={story.createdAt}
+              viewCount={story.viewCount}
+              coverImgSrc={story.bookImageUrl}
+              title={story.title}
+              content={story.content}
+              likeCount={story.likeCount}
+              commentCount={story.commentCount}
+              subscribeText="구독"
+            />
          ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(main)/books/[id]/page.tsx around lines 62 - 78, The outer clickable
<div> is redundant because BookStoryCardLarge already accepts an onClick and
handles cursor styling; remove the wrapper div and instead pass the click
handler to BookStoryCardLarge (e.g., onClick={() =>
router.push(`/stories/${story.id}`)}) and delete the wrapper's
className="cursor-pointer"; update the JSX where BookStoryCardLarge is rendered
(refer to BookStoryCardLarge, router.push, and story.id) so the component
receives the onClick prop directly.
src/app/(public)/signup/page.tsx (1)

6-14: Consider using a server-side redirect instead of a client-side useEffect redirect.

The current approach renders a blank page (null) while the client-side redirect fires. In Next.js App Router, you can use redirect() from next/navigation in a Server Component (or permanentRedirect()) to avoid the blank flash entirely:

♻️ Proposed refactor — server-side redirect
-"use client";
-
-import { useEffect } from "react";
-import { useRouter } from "next/navigation";
-
-export default function SignupPage() {
-  const router = useRouter();
-
-  useEffect(() => {
-    router.replace("/signup/terms");
-  }, [router]);
-
-  return null;
-}
+import { redirect } from "next/navigation";
+
+export default function SignupPage() {
+  redirect("/signup/terms");
+}

This eliminates the blank flash, works without JavaScript, and is the idiomatic Next.js pattern for route-level redirects. Alternatively, a next.config.ts redirect rule could handle this without a page component at all.

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

In `@src/app/`(public)/signup/page.tsx around lines 6 - 14, Replace the
client-side useEffect/router.replace redirect in SignupPage with a server-side
redirect: remove useRouter and useEffect imports/usages in
src/app/(public)/signup/page.tsx and instead call next/navigation's redirect (or
permanentRedirect) from the top-level of the exported component (or directly
before returning) to send users to "/signup/terms"; alternatively remove the
page and add a next.config.ts redirect rule—target symbols: SignupPage,
useRouter, useEffect, router.replace, and
next/navigation::redirect/permanentRedirect.
src/components/base-ui/Profile/BookStoryList.tsx (2)

70-77: w-fit on grid may cause alignment issues with fixed-width cards.

The inner grid uses w-fit while BookStoryCard has fixed pixel widths (w-[161px] / md:w-[336px]). At the min-[540px]:grid-cols-3 breakpoint, three 161px cards (~483px + gaps) may not fill the available space cleanly, and the w-fit grid won't stretch. This is fine if intentional centering via the parent's items-center is the goal, but verify it looks correct at viewport widths around 540–767px where three narrow cards may appear off-center or leave large margins.

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

In `@src/components/base-ui/Profile/BookStoryList.tsx` around lines 70 - 77, The
grid in BookStoryList uses the `w-fit` utility which prevents the grid from
expanding to the container width and can cause misalignment for the fixed-width
BookStoryCard components (see BookStoryList and BookStoryCard symbols); change
the grid sizing so it can stretch to the parent (remove `w-fit` and/or replace
with `w-full` or a responsive width class) or make the cards responsive (adjust
fixed widths in BookStoryCard to use percentage or responsive utilities) so at
the min-[540px]:grid-cols-3 breakpoint three cards fill and align correctly;
ensure the parent centering (`items-center`) still produces the intended layout
after the change.

5-66: Mock data is inlined in the component — consider extracting to a constants file.

MyBookStoryList already imports its mock data from @/constants/mocks/mypage. For consistency and to avoid duplicating mock patterns, consider moving MOCK_STORIES to a shared constants file as well.

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

In `@src/components/base-ui/Profile/BookStoryList.tsx` around lines 5 - 66,
MOCK_STORIES is defined inline in BookStoryList.tsx causing duplication with
MyBookStoryList's mock imports; move the MOCK_STORIES array into the shared mock
constants module used elsewhere (the module referenced by MyBookStoryList, e.g.
the mocks export that currently lives under the project's constants/mocks area),
export it as a named constant, then replace the inline MOCK_STORIES in
BookStoryList.tsx with an import of that named export (update references to
MOCK_STORIES in the component to use the imported constant).
src/components/layout/SearchModal.tsx (1)

21-26: recommendedBooks array is recreated on every render.

This is a static dummy dataset. Move it outside the component to avoid unnecessary allocations on each render.

♻️ Proposed refactor
+const RECOMMENDED_BOOKS = [
+  { id: 1, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
+  { id: 2, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
+  { id: 3, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
+  { id: 4, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
+];
+
 export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
   const router = useRouter();
   const [topOffset, setTopOffset] = useState(0);
   const [likedBooks, setLikedBooks] = useState<Record<number, boolean>>({});
   const [searchValue, setSearchValue] = useState("");
-
-  // 더미 추천 책 데이터
-  const recommendedBooks = [
-    { id: 1, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
-    { id: 2, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
-    { id: 3, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
-    { id: 4, imgUrl: "/booksample.svg", title: "책 제목", author: "작가작가작가" },
-  ];

Then reference RECOMMENDED_BOOKS instead of recommendedBooks in the JSX.

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

In `@src/components/layout/SearchModal.tsx` around lines 21 - 26, The array
recommendedBooks is being recreated on every render; move that static dummy
dataset out of the SearchModal component by declaring it as a top-level constant
(e.g., RECOMMENDED_BOOKS = [{ id: 1, ... }, ...]) and then update the component
to reference RECOMMENDED_BOOKS instead of recommendedBooks wherever used (JSX
mapping, props, etc.) so the allocation happens once.
src/components/base-ui/Join/JoinInput.tsx (2)

60-70: Duplicated class-detection logic with JoinButton.tsx.

The hasHeight / hasPx / hasPy string-includes checks are identical to those in JoinButton.tsx (lines 26–37). Extract this into a shared utility (e.g., parseClassOverrides(className)) to keep both components in sync and reduce duplication.

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

In `@src/components/base-ui/Join/JoinInput.tsx` around lines 60 - 70, The
duplicated class-detection logic (hasHeight / hasPx / hasPy) used in
JoinInput.tsx and JoinButton.tsx should be extracted into a shared utility
function—create parseClassOverrides(className) that returns { hasHeight, hasPx,
hasPy } based on the same string.includes checks, export it from a common
utilities module, then replace the inline checks in both JoinInput.tsx and
JoinButton.tsx with calls to parseClassOverrides(className) and use the returned
properties.

92-94: Password pr-[40px] is always appended and may conflict with consumer-provided padding.

When isPasswordType is true, pr-[40px] is unconditionally added regardless of what the consumer passes in className. If a consumer explicitly sets pr-[20px], both classes will coexist and the result depends on CSS specificity rather than intention. Since this is a controlled internal component with limited consumers, the risk is low — but worth noting if usage expands.

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

In `@src/components/base-ui/Join/JoinInput.tsx` around lines 92 - 94, The
JoinInput component always appends the "pr-[40px]" class when isPasswordType is
true which can conflict with consumer-provided padding in className; change the
logic in JoinInput to avoid forcing that class by either (a) adding the password
padding before appending the consumer-supplied className so the consumer can
override it, or (b) conditionally adding "pr-[40px]" only when the incoming
className does not already include any "pr-"/ "px-"/ "p-" right-padding utility
(perform a simple string check on the className), or (c) expose a prop like
passwordIconPadding and use that instead of a hardcoded class; locate this in
the JSX where className is composed (the template string referencing hasHeight,
hasPx, hasPy, className, isPasswordType) and implement one of these approaches
so consumer padding wins or is configurable.
src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx (2)

27-27: showToast is already available inside useProfileSetup — avoid a second useSignup() call.

useProfileSetup already calls useSignup() and destructures showToast internally. Rather than calling the context hook again in the component, expose the needed function through useProfileSetup's return value. This keeps the component decoupled from SignupContext and consolidates context access in one place.

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

In `@src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx` at line 27,
The component is calling useSignup() to get showToast even though
useProfileSetup already extracts it; remove the extra useSignup() call in
ProfileSetup and instead update/use the showToast exposed by useProfileSetup's
return value (ensure useProfileSetup returns showToast if it doesn't yet).
Locate useProfileSetup and its return signature and add showToast to the
returned object, then update the ProfileSetup component to read showToast from
useProfileSetup rather than calling useSignup().

128-134: Button disabled condition diverges from handleNextClick validation.

The button is enabled when all fields are non-empty, but handleNextClick additionally requires isNicknameChecked (via isValid). If this is intentional UX (clickable button → toast prompt), consider using a visual cue (e.g., a different variant or subtle indicator) so users understand the nickname check is pending. Otherwise, align disabled with !isValid.

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

In `@src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx` around lines
128 - 134, The JoinButton's disabled predicate currently checks only nickname,
intro, name, and phone but handleNextClick relies on isValid (which includes
isNicknameChecked), causing a mismatch; update the disabled prop on JoinButton
to use !isValid (or, if you intend to allow clicks and show a toast, change the
button variant/appearance when isNicknameChecked is false to indicate pending
nickname verification) so the UI behavior matches the validation in
handleNextClick; reference JoinButton, handleNextClick, isValid,
isNicknameChecked, nickname, intro, name, and phone to locate and update the
condition.
src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts (1)

18-24: Nickname regex is dense — consider extracting as a named constant.

The inline regex /[^a-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/g is hard to read and maintain. Extracting it as a named constant (e.g., NICKNAME_ALLOWED_CHARS) at module scope would improve clarity and make the allowed character set easier to audit and update.

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

In `@src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts` around
lines 18 - 24, Extract the inline character-filtering regex in
handleNicknameChange into a module-scoped named constant (e.g.,
NICKNAME_ALLOWED_CHARS) and use that constant inside handleNicknameChange to
improve readability and maintainability; update the comment to describe allowed
characters, declare NICKNAME_ALLOWED_CHARS near the top of the file, and replace
the inline `/[^...]/g` usage in handleNicknameChange with a reference to that
constant.
src/components/base-ui/Join/JoinButton.tsx (1)

25-37: String-based class detection is fragile; consider a more explicit API.

className?.includes("w-") can false-positive on unrelated substrings (e.g., a custom class containing "w-"). A safer approach would be to accept explicit boolean/override props (e.g., fullWidth, size) or use a utility like tailwind-merge / clsx to handle class conflicts declaratively.

This works for now given the controlled usage, but may become a maintenance burden as more consumers appear.

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

In `@src/components/base-ui/Join/JoinButton.tsx` around lines 25 - 37, The current
fragile string-based detection (hasWidth, hasHeight, hasPx, hasPy) inspects
className with includes("w-") etc., which can yield false positives; replace
this heuristic with explicit override props (e.g., add boolean props like
fullWidth, overrideWidth, size or paddingOverride) and update JoinButton to
prefer those props over parsing className, or integrate a class-merge utility
(tailwind-merge/clsx) to resolve conflicts deterministically; locate the
variables hasWidth / hasHeight / hasPx / hasPy in JoinButton.tsx, add the new
explicit props to the component signature and prop handling, and use them when
deciding whether to append internal width/height/padding classes instead of
doing className.includes checks.
src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx (3)

26-167: Consider extracting TERMS_CONTENT to a separate file.

~140 lines of static legal content in the component file hurts readability. Moving it to a dedicated data file (e.g., termsContent.ts) would keep this component focused on behavior and layout.

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

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` around
lines 26 - 167, The TermsAgreement component contains a large TERMS_CONTENT
constant that clutters the file; extract TERMS_CONTENT into a new module (e.g.,
termsContent.ts) and import it into TermsAgreement.tsx. Move the entire
TERMS_CONTENT object out, export it as a named export (TERMS_CONTENT) from the
new file, update the TermsAgreement component to import { TERMS_CONTENT } and
remove the in-file constant, and ensure any tests or usages referencing
TERMS_CONTENT are updated to import from the new module.

199-238: renderMarkdown does not handle numbered lists.

The terms content includes numbered list items (e.g., 1. "서비스"란 ..., 2. "회원"이란 ...). These fall through to the <p> fallback and lose their structured list formatting. Consider adding a pattern for ordered list items.

Also, this custom renderer is growing in scope — if formatting needs expand further, consider using a lightweight markdown library (e.g., react-markdown) instead of extending this manually.

Quick fix for numbered list items
+     if (/^\d+\.\s/.test(trimmed)) {
+       return (
+         <div key={index} className="flex gap-[8px] pl-[4px] mb-[4px] text-Gray-6 font-sans text-[14px] md:text-[18px] font-medium leading-[145%] md:leading-[135%] tracking-[-0.018px]">
+           <span className="shrink-0">{trimmed.match(/^\d+/)?.[0]}.</span>
+           <span>{trimmed.replace(/^\d+\.\s/, "")}</span>
+         </div>
+       );
+     }
      if (trimmed.startsWith("- ")) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` around
lines 199 - 238, renderMarkdown currently ignores ordered list syntax so lines
like "1. ..." fall through to <p>; add handling for numbered list items by
detecting lines matching /^\d+\. / (use trimmed.match(/^\d+\.\s/)) and render
them as ordered list markup (wrap consecutive numbered lines into an <ol> with
<li> children) inside the renderMarkdown implementation; update the mapping
logic in renderMarkdown to group consecutive ordered items into a single <ol>
element (similar to how bullets are rendered) and output <li> elements
containing trimmed.replace(/^\d+\.\s/, "") so numbered lists preserve structure
and numbering.

240-242: No guard if selectedTermId doesn't match a TERMS_CONTENT key.

TERMS_CONTENT[selectedTermId] could be undefined if someone adds a term to TERMS_DATA without a corresponding TERMS_CONTENT entry. This would cause a runtime error on term.content.

A quick guard or a shared type between the two constants would prevent this:

Optional guard
  if (selectedTermId) {
    const term = TERMS_CONTENT[selectedTermId];
+   if (!term) {
+     setSelectedTermId(null);
+     return null;
+   }
    return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` around
lines 240 - 242, Guard against a missing TERMS_CONTENT entry before accessing
term.content: after computing const term = TERMS_CONTENT[selectedTermId] (and
anywhere selectedTermId is used), check if term is undefined and return a safe
fallback (e.g., null, a default message component, or an error UI) to avoid
runtime errors; alternatively, align TYPES so TERMS_CONTENT and TERMS_DATA share
a union type for keys (or add a compile-time mapping) to ensure selectedTermId
cannot reference a non-existent TERMS_CONTENT entry.
src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts (1)

33-43: toggleInterest uses closure values instead of functional updaters — potential stale state.

setSelectedInterests is called with the closure-captured selectedInterests array rather than using a functional updater. Since the context setter is (interests: string[]) => setState(prev => ({ ...prev, selectedInterests: interests })), rapid calls could operate on stale data. In practice this is unlikely with user clicks, but using a functional pattern would be more robust.

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

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts` around
lines 33 - 43, The toggleInterest function currently reads selectedInterests
from the closure and calls setSelectedInterests with that stale value; change it
to use the functional updater form so updates always operate on the latest
state: inside toggleInterest (and when checking length/including/removing) call
setSelectedInterests(prev => { ... }) and perform includes, filter, push logic
against prev (and enforce the 6-item limit using prev.length) so rapid
consecutive toggles won't lose updates; update references to selectedInterests
inside toggleInterest accordingly.
src/components/base-ui/Join/steps/useEmailVerification.ts (1)

52-58: Timer recreates the interval on every tick due to timeLeft in the dependency array.

Because setTimeLeft accepts a plain value (not a functional updater), the effect must include timeLeft in its deps, causing the interval to be torn down and recreated every second. This works correctly but is slightly wasteful.

A cleaner approach would be to enhance the context setter to accept a callback, or use a local ref to track the current value:

Alternative using a ref to avoid interval churn
+ const timeLeftRef = useRef(timeLeft);
+ useEffect(() => {
+   timeLeftRef.current = timeLeft;
+ }, [timeLeft]);
+
  useEffect(() => {
    if (timeLeft === null || timeLeft === 0) return;
    const interval = setInterval(() => {
-     setTimeLeft(timeLeft > 0 ? timeLeft - 1 : 0);
+     const current = timeLeftRef.current;
+     if (current !== null && current > 0) {
+       setTimeLeft(current - 1);
+     } else {
+       clearInterval(interval);
+     }
    }, 1000);
    return () => clearInterval(interval);
-  }, [timeLeft, setTimeLeft]);
+  }, [timeLeft === null || timeLeft === 0, setTimeLeft]);

This would require importing useRef and adjusting the deps, but keeps a single interval running.

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

In `@src/components/base-ui/Join/steps/useEmailVerification.ts` around lines 52 -
58, The effect in useEmailVerification recreates the interval every second
because timeLeft is in the dependency array and setTimeLeft is called with a
plain value; change the updater to avoid subscribing to timeLeft each tick by
either (a) calling setTimeLeft with a functional updater (prev => Math.max(prev
- 1, 0)) inside the interval so you can remove timeLeft from the deps, or (b)
use a local ref (e.g., timeLeftRef) that mirrors timeLeft and read/update that
ref inside a single interval; update the cleanup to clearInterval as before and
keep the effect deps minimal (e.g., [] or [setTimeLeft]) to prevent interval
churn.
src/contexts/SignupContext.tsx (1)

74-74: Single useState for all signup state may cause unnecessary re-renders.

Every field update (including toast visibility changes) produces a new state object, causing all useSignup() consumers to re-render. For a multi-step form where only one step is mounted at a time this is mostly fine, but the toast transitions alone trigger 3 state updates (show → fade → unmount) that will re-render the active step component each time.

Consider splitting toast state into a separate context or using useReducer if re-render performance becomes noticeable. Not urgent for a signup flow, but worth keeping in mind.

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

In `@src/contexts/SignupContext.tsx` at line 74, The SignupContext currently
stores all UI and form values in one useState (const [state, setState] =
useState<SignupState>(initialState)), causing every small update (e.g., toast
show/fade/unmount) to re-render all useSignup() consumers; split the
frequently-changing toast/UI bits out of SignupState by creating a separate
state holder (e.g., useState or a small ToastContext) or switch to useReducer to
partition updates: keep form fields in the existing state/setState (or reducer
slice) and move toastVisible/toastStage into their own state hook or context so
toast transitions no longer cause full signup consumer re-renders. Ensure
references to state, setState, SignupState, initialState and useSignup reflect
the new split so existing consumers still access form data without being
triggered by toast changes.
src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx (2)

25-29: showToast could be returned from useProfileImage to avoid a redundant useSignup() call.

useProfileImage already calls useSignup() internally and destructures showToast from it. Rather than calling useSignup() a second time here, consider exposing showToast from the useProfileImage return object. This would keep the context access centralized in the hook and reduce the component's direct dependencies.

Also minor style nit: Line 29 uses React.useRef while useState/useEffect are destructured imports on Line 3. Consider importing useRef alongside them for consistency.

Suggested change in useProfileImage.ts return value
  return {
    selectedInterests,
    profileImage,
    isProfileImageSet,
    toggleInterest,
    handleResetImage,
    handleImageUpload,
    isValid,
+   showToast,
  };

Then in ProfileImage.tsx:

- const { showToast } = useSignup();
- import { useSignup } from "@/contexts/SignupContext";
- import React, { useState, useEffect } from "react";
+ import React, { useState, useEffect, useRef } from "react";
- const interestRef = React.useRef<HTMLDivElement>(null);
+ const interestRef = useRef<HTMLDivElement>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx` around lines
25 - 29, The component calls useSignup() just to get showToast even though
useProfileImage already calls useSignup internally; update useProfileImage to
include showToast in its return value (e.g., return { ..., showToast }) and then
remove the redundant useSignup() call from the ProfileImage component so it
consumes showToast from useProfileImage instead; also replace React.useRef with
a destructured useRef import (add useRef to the existing import list) and change
interestRef to useRef<HTMLDivElement>(null) for consistent imports and style.

67-71: Intentionally keeping the button always enabled — acknowledged.

The comment explains the design rationale well (toast feedback requires the button to remain clickable). The isButtonDisabled variable being hardcoded to false is a bit of dead indirection — you could simply pass disabled={false} or omit the prop entirely — but it's fine as documentation of the deliberate override.

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

In `@src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx` around lines
67 - 71, The isButtonDisabled constant in ProfileImage.tsx is a dead indirection
(hardcoded false); simplify by removing the isButtonDisabled variable and either
omit the disabled prop on the button or pass disabled={false} directly where the
button is rendered (or if you prefer to keep the sentinel, replace it with a
clearly named constant like FORCE_ENABLE_BUTTON and add a one-line comment
stating it's intentional); update any references to isButtonDisabled in the
component (e.g., the button element) to the chosen approach.

subscribingCount={u.subscribingCount}
subscribersCount={u.subscribersCount}
onSubscribeClick={() => console.log('subscribe', u.id)}
onSubscribeClick={() => console.log("subscribe", u.id)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug console.log in subscribe handler.

Per the PR checklist, unnecessary console logs should be removed before merging.

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

In `@src/app/`(main)/page.tsx at line 64, Remove the debug console.log from the
onSubscribeClick JSX prop in page.tsx: replace the inline
console.log("subscribe", u.id) with a real handler (e.g., pass a reference to a
new handleSubscribe function) or call the existing subscription function so it
actually performs the subscribe action; locate the onSubscribeClick prop and the
user id reference (u.id) and wire it to handleSubscribe(userId) (or the proper
subscribe function) instead of logging.

<div className="relative mx-auto w-full max-w-[1400px] px-4">
<div className="t:mt-8 h-[44px] d:h-[54px] flex gap-14 items-center border-b border-zinc-300">
<div className="text-center text-Gray-7 body_1 t:subhead_2 leading-7 cursor-pointer hover:text-zinc-600">
<div className="t:mt-3 h-[44px] d:h-[54px] flex gap-14 items-center border-b border-zinc-300 overflow-x-auto scrollbar-hide">
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if scrollbar-hide is defined via plugin or custom CSS
echo "=== Searching for scrollbar-hide references ==="
rg "scrollbar-hide" --type-list | head -20

echo "=== Searching in CSS files ==="
fd -e css | xargs grep -l "scrollbar-hide" 2>/dev/null || echo "No scrollbar-hide found in CSS"

echo "=== Searching in TypeScript/JavaScript config files ==="
fd -E node_modules \( -name "*.config.ts" -o -name "*.config.js" -o -name "tailwind.config.*" \) | xargs cat 2>/dev/null

echo "=== Checking package.json for scrollbar-related dependencies ==="
cat package.json | jq '.dependencies + .devDependencies' 2>/dev/null | grep -i scrollbar || echo "No scrollbar dependencies found"

echo "=== General search for scrollbar-hide ==="
rg "scrollbar-hide" --type-list
rg "scrollbar-hide" -i

Repository: checkmo2025/FE

Length of output: 1673


🏁 Script executed:

#!/bin/bash
echo "=== Finding all CSS files ==="
fd -e css -e scss --type f

echo "=== Checking globals or main CSS files ==="
find . -path ./node_modules -prune -o -name "globals.css" -o -name "global.css" -o -name "styles.css" | head -20

echo "=== Checking all CSS content for scrollbar rules ==="
fd -e css | xargs cat 2>/dev/null | grep -A 2 -B 2 "scrollbar" || echo "No scrollbar rules found"

echo "=== Looking for Tailwind config ==="
find . -path ./node_modules -prune -o \( -name "tailwind.config.*" -type f -print \) 

echo "=== Reading Tailwind config if exists ==="
cat tailwind.config.* 2>/dev/null || echo "No tailwind.config found"

Repository: checkmo2025/FE

Length of output: 693


Remove scrollbar-hide or add the required configuration—it is not defined in your Tailwind setup.

scrollbar-hide is not available in your codebase. No CSS rule, Tailwind plugin, or custom utility defines it. The class will be ignored, leaving scrollbars visible on the horizontally scrollable tab bar.

Either:

  • Remove scrollbar-hide and handle scrollbar styling via CSS rules in globals.css
  • Install and configure tailwind-scrollbar-hide plugin in tailwind.config.js
  • Add a custom utility rule to hide scrollbars

This same issue appears in src/app/(main)/news/page.tsx as well.

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

In `@src/app/`(main)/stories/page.tsx at line 20, The JSX contains a non-existent
Tailwind class "scrollbar-hide" on the horizontal tab container (the div with
class "t:mt-3 h-[44px] d:h-[54px] flex gap-14 items-center border-b ...
scrollbar-hide") in the stories page component (and the same usage in the news
page component); fix by either removing "scrollbar-hide" and adding explicit CSS
rules in your global stylesheet to hide or style the scrollbar, or install and
enable the tailwind-scrollbar-hide plugin in your Tailwind config, or create a
custom utility in your Tailwind config that defines "scrollbar-hide" so the
class is available.

Comment on lines +21 to +28
const steps: Record<string, React.ReactNode> = {
terms: <TermsAgreement onNext={() => navigateTo("email")} />,
email: <EmailVerification onNext={() => navigateTo("password")} />,
password: <PasswordEntry onNext={() => navigateTo("profile")} />,
profile: <ProfileSetup onNext={() => navigateTo("profile-image")} />,
"profile-image": <ProfileImage onNext={() => navigateTo("complete")} />,
complete: <SignupComplete />,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

All step components are instantiated on every render.

The steps record eagerly creates React elements for all six steps on each render, even though only one is displayed. This triggers hooks and side effects in every step component simultaneously, which can cause unexpected behavior (e.g., timers, API calls). Use a component map instead and render lazily.

Proposed fix — use a component map
-    const steps: Record<string, React.ReactNode> = {
-        terms: <TermsAgreement onNext={() => navigateTo("email")} />,
-        email: <EmailVerification onNext={() => navigateTo("password")} />,
-        password: <PasswordEntry onNext={() => navigateTo("profile")} />,
-        profile: <ProfileSetup onNext={() => navigateTo("profile-image")} />,
-        "profile-image": <ProfileImage onNext={() => navigateTo("complete")} />,
-        complete: <SignupComplete />,
-    };
-
-    const currentStep = steps[step];
-
-    if (!currentStep) {
-        // Falls back to terms if step is invalid
-        return null;
-    }
-
-    return <>{currentStep}</>;
+    const steps: Record<string, React.FC<{ onNext?: () => void }>> = {
+        terms: TermsAgreement,
+        email: EmailVerification,
+        password: PasswordEntry,
+        profile: ProfileSetup,
+        "profile-image": ProfileImage,
+        complete: SignupComplete,
+    };
+
+    const nextStepMap: Record<string, string> = {
+        terms: "email",
+        email: "password",
+        password: "profile",
+        profile: "profile-image",
+        "profile-image": "complete",
+    };
+
+    const StepComponent = steps[step];
+
+    if (!StepComponent) {
+        router.replace("/signup/terms");
+        return null;
+    }
+
+    const onNext = nextStepMap[step]
+        ? () => navigateTo(nextStepMap[step])
+        : undefined;
+
+    return <StepComponent onNext={onNext} />;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/signup/[step]/page.tsx around lines 21 - 28, The current
steps record eagerly instantiates every React element (steps: Record<string,
React.ReactNode>), causing all step components (TermsAgreement,
EmailVerification, PasswordEntry, ProfileSetup, ProfileImage, SignupComplete) to
mount on each render; change steps to a component map (e.g., Record<string,
React.ComponentType<any>> or a more specific prop type) that stores the
component constructors instead of JSX, then select and render only the active
step: const Step = steps[currentStep]; return <Step onNext={() =>
navigateTo(...)} /> (or React.createElement(Step, { onNext: () =>
navigateTo(...) })). Update the type signature and where steps is used so
hooks/side effects run only for the rendered step.

Comment on lines +32 to +34
if (!currentStep) {
// Falls back to terms if step is invalid
return null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading comment — no fallback to terms actually occurs.

The comment says "Falls back to terms if step is invalid" but the code returns null, rendering a blank page. Either redirect to /signup/terms or update the comment to match the behavior.

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

In `@src/app/`(public)/signup/[step]/page.tsx around lines 32 - 34, The comment is
wrong: the code returns null instead of falling back to terms. Fix by either (A)
implementing an actual redirect to the terms step (replace "return null" with a
redirect('/signup/terms') from next/navigation or router.push('/signup/terms')
in a client component) inside the page component where currentStep is checked,
or (B) update the comment to reflect that the component intentionally renders
nothing when currentStep is invalid; reference currentStep and the early-return
that currently does "return null" and choose one of the two behaviors
consistently.


// TODO: 실제 저장 로직 구현
console.log('공지사항 저장:', {
console.log("공지사항 저장:", {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove leftover console.log calls before merging.

There are console.log statements in handleSubmit (Line 58), handleCreateVote (Line 79), handleRegisterBookshelf (Line 90), and handleImageFile (Line 101). The PR checklist explicitly asks for removal of unnecessary console logs.

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

In `@src/app/groups/`[id]/admin/notice/new/page.tsx at line 58, Remove the
leftover console.log calls in the page component by deleting the console.log
lines inside handleSubmit, handleCreateVote, handleRegisterBookshelf, and
handleImageFile; if logging is required for debugging, replace them with the
project's standard logger or return values instead of leaving console
statements, ensuring no console.log calls remain in those functions before
merging.

2. 서비스와 회원 간 분쟁에 대해 소송이 제기될 경우 대한민국 법을 준거법으로 합니다.

## 제13조 (시행일)
본 약관은부터 2025년 12월 21일부터 시행합니다.`,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in legal text: duplicated "부터".

Line 130 reads 본 약관은부터 2025년 12월 21일부터 시행합니다. — "부터" appears twice, and "은부터" is grammatically incorrect. This should likely be:

-본 약관은부터 2025년 12월 21일부터 시행합니다.
+본 약관은 2025년 12월 21일부터 시행합니다.
📝 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
약관은부터 2025 12 21일부터 시행합니다.`,
약관은 2025 12 21일부터 시행합니다.`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` at line
130, Fix the typo in the TermsAgreement component's legal text: locate the
string literal in TermsAgreement.tsx that currently reads "본 약관은부터 2025년 12월
21일부터 시행합니다." and change it to the correct Korean phrasing (e.g., "본 약관은 2025년
12월 21일부터 시행합니다." or "본 약관은 2025년 12월 21일부터 적용됩니다."), ensuring only the
duplicated "부터" is removed and the grammar is correct.

Comment on lines +299 to 304
<span
className="flex-1 text-[#353535] font-sans text-[12px] font-normal leading-[145%] tracking-[-0.012px] t:text-[19.861px] t:leading-[15.605px] cursor-pointer hover:underline pr-[4px]"
onClick={() => setSelectedTermId(term.id)}
>
{term.label}
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

line-height is smaller than font-size on tablet breakpoint — text will overlap.

At the t: breakpoint, the term label has t:text-[19.861px] t:leading-[15.605px]. A line-height of ~15.6px with a font-size of ~19.9px means multi-line labels will have overlapping text. These labels are long enough to wrap, so this will be visually broken.

Likely the leading value should be larger than the text value (e.g., t:leading-[28px] or similar).

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

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` around
lines 299 - 304, The tablet breakpoint in the TermsAgreement component sets
t:text-[19.861px] but t:leading-[15.605px], causing line-height smaller than
font-size and overlapping wrapped term.label text; update the span's tablet
leading class (the element that calls setSelectedTermId on click) so the
t:leading value is greater than the t:text (for example t:leading-[28px] or
another value that provides sufficient vertical spacing) to prevent overlap.

Comment on lines +87 to 89
<div className="gap-[12px] w-full self-stretch text-[#8D8D8D] font-sans text-[14px] font-medium leading-[145%] line-clamp-3 md:line-clamp-none">
{user.intro}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

gap-[12px] has no effect without flex or grid display.

The gap CSS property only applies to flex or grid containers. This div has no display type set (defaults to block), so gap-[12px] is inert. If the gap was intentional (e.g., for spacing child elements), add flex flex-col back.

Proposed fix
-            <div className="gap-[12px] w-full self-stretch text-[`#8D8D8D`] font-sans text-[14px] font-medium leading-[145%] line-clamp-3 md:line-clamp-none">
+            <div className="flex flex-col gap-[12px] w-full self-stretch text-[`#8D8D8D`] font-sans text-[14px] font-medium leading-[145%] line-clamp-3 md:line-clamp-none">
📝 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="gap-[12px] w-full self-stretch text-[#8D8D8D] font-sans text-[14px] font-medium leading-[145%] line-clamp-3 md:line-clamp-none">
{user.intro}
</div>
<div className="flex flex-col gap-[12px] w-full self-stretch text-[`#8D8D8D`] font-sans text-[14px] font-medium leading-[145%] line-clamp-3 md:line-clamp-none">
{user.intro}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/MyPage/UserProfile.tsx` around lines 87 - 89, The div
rendering user.intro in the UserProfile component uses gap-[12px] which is
ineffective on a block element; either make it a flex container (e.g., add "flex
flex-col" to the className on that div) if the intent is spacing between child
elements, or remove the gap utility if it was added by mistake—update the
className on the div that contains user.intro accordingly.

Comment on lines +73 to +95
export const SignupProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<SignupState>(initialState);
const toastTimers = useRef<NodeJS.Timeout[]>([]);

const showToast = useCallback((message: string) => {
// Clear existing timers
toastTimers.current.forEach(clearTimeout);
toastTimers.current = [];

setState((prev) => ({ ...prev, toast: { message, visible: true } }));

// Fade out after 2.5s
const fadeOutTimer = setTimeout(() => {
setState((prev) => prev.toast ? { ...prev, toast: { ...prev.toast, visible: false } } : prev);
}, 2500);

// Unmount after 3s
const unmountTimer = setTimeout(() => {
setState((prev) => ({ ...prev, toast: null }));
}, 3000);

toastTimers.current.push(fadeOutTimer, unmountTimer);
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Toast timers are not cleaned up on provider unmount.

If SignupProvider unmounts while fade-out/unmount timers are still pending, the setTimeout callbacks will attempt to call setState on an unmounted component. While React 19 no longer warns about this, it's still a minor resource leak.

Add cleanup in a useEffect
 export const SignupProvider = ({ children }: { children: ReactNode }) => {
     const [state, setState] = useState<SignupState>(initialState);
     const toastTimers = useRef<NodeJS.Timeout[]>([]);
 
+    useEffect(() => {
+        return () => {
+            toastTimers.current.forEach(clearTimeout);
+        };
+    }, []);
+
     const showToast = useCallback((message: string) => {
📝 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 SignupProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<SignupState>(initialState);
const toastTimers = useRef<NodeJS.Timeout[]>([]);
const showToast = useCallback((message: string) => {
// Clear existing timers
toastTimers.current.forEach(clearTimeout);
toastTimers.current = [];
setState((prev) => ({ ...prev, toast: { message, visible: true } }));
// Fade out after 2.5s
const fadeOutTimer = setTimeout(() => {
setState((prev) => prev.toast ? { ...prev, toast: { ...prev.toast, visible: false } } : prev);
}, 2500);
// Unmount after 3s
const unmountTimer = setTimeout(() => {
setState((prev) => ({ ...prev, toast: null }));
}, 3000);
toastTimers.current.push(fadeOutTimer, unmountTimer);
}, []);
export const SignupProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<SignupState>(initialState);
const toastTimers = useRef<NodeJS.Timeout[]>([]);
useEffect(() => {
return () => {
toastTimers.current.forEach(clearTimeout);
};
}, []);
const showToast = useCallback((message: string) => {
// Clear existing timers
toastTimers.current.forEach(clearTimeout);
toastTimers.current = [];
setState((prev) => ({ ...prev, toast: { message, visible: true } }));
// Fade out after 2.5s
const fadeOutTimer = setTimeout(() => {
setState((prev) => prev.toast ? { ...prev, toast: { ...prev.toast, visible: false } } : prev);
}, 2500);
// Unmount after 3s
const unmountTimer = setTimeout(() => {
setState((prev) => ({ ...prev, toast: null }));
}, 3000);
toastTimers.current.push(fadeOutTimer, unmountTimer);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/SignupContext.tsx` around lines 73 - 95, SignupProvider's
showToast stores timers in toastTimers (useRef) but never clears them on
unmount; add a useEffect inside SignupProvider that returns a cleanup function
which iterates toastTimers.current and calls clearTimeout for each timer and
then sets toastTimers.current = [] to avoid pending setState callbacks after
unmount (no dependencies so it runs once). This ensures timers created by
showToast are cancelled when the provider unmounts.

Copy link

@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: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/app/groups/page.tsx (1)

53-59: ⚠️ Potential issue | 🟠 Major

Remove console.log debug artifacts before merging.

console.log on Line 56 logs the user-supplied application reason, which qualifies as user-entered content that should never be logged in production code. The PR checklist explicitly requires removing unnecessary console logs.

Line 103 (onSubmit={() => console.log(...)) is also a placeholder that must be replaced with the real API call or at minimum removed.

🐛 Proposed fix
  const onSubmitApply = (clubId: number, reason: string) => {
    if (!reason.trim()) return;

-   console.log('apply:', clubId, reason);
    // TODO API
    setApplyClubId(null);
  };
-           onSubmit={() => console.log(q, category, group, region)}
+           onSubmit={() => { /* TODO: wire up search API */ }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/page.tsx` around lines 53 - 59, Remove the debug console.log
statements that leak user-entered content: inside onSubmitApply (function
onSubmitApply) delete the console.log('apply:', clubId, reason) and instead
invoke the real API call (or a service function like applyToClub(clubId,
reason)) and handle success/error (then call setApplyClubId(null)); also remove
the inline console.log used in the onSubmit JSX prop (replace with onSubmitApply
invocation or the proper submit handler) so no user input is written to the
console.
src/app/groups/[id]/bookcase/[bookId]/DebateSection.tsx (1)

80-91: ⚠️ Potential issue | 🟡 Minor

cursor-pointer and hover:brightness-95 on a <div> with no onClick handler creates a false affordance.

The profile container visually signals interactivity but has no click handler, keyboard accessibility, or ARIA role. If this is meant to navigate to the user's profile, the handler is missing. If it's purely decorative, remove cursor-pointer and the hover effect.

🐛 Option A — add a profile navigation handler (if intended)
-         <div className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px] hover:brightness-95 cursor-pointer">
+         <div
+           className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px] hover:brightness-95 cursor-pointer"
+           onClick={onProfileClick}   // wire up a prop or router.push
+           role="button"
+           tabIndex={0}
+           onKeyDown={(e) => e.key === 'Enter' && onProfileClick?.()}
+         >
🐛 Option B — remove the interaction affordances (if not clickable)
-         <div className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px] hover:brightness-95 cursor-pointer">
+         <div className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px]">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/DebateSection.tsx around lines 80 -
91, The profile container in DebateSection (the div wrapping Image with
profileSrc and the paragraph showing myName) currently has interactive classes
(cursor-pointer and hover:brightness-95) but no click/keyboard handlers or ARIA
role; either remove those classes if it’s purely decorative, or make it truly
interactive by wiring a navigation handler (e.g., onClick that navigates to the
user profile), adding keyboard support (tabIndex=0 and onKeyDown handling
Enter/Space), and an accessible role/aria-label (or wrap with a Link) so screen
readers and keyboard users can activate it; update the div used in DebateSection
accordingly to implement one of these two options.
src/app/groups/[id]/page.tsx (1)

52-62: ⚠️ Potential issue | 🟡 Minor

Duplicate cursor-pointer in the <Link> className.

cursor-pointer appears on both the standalone line (58) and the hover line (59) of the same className string. Remove the duplicate.

🐛 Proposed fix
  className="
    block w-full
    rounded-[8px]
    border border-Subbrown-3
    bg-White
    p-4
-   cursor-pointer
-   hover:brightness-98 hover:-translate-y-[1px] cursor-pointer
+   hover:brightness-98 hover:-translate-y-[1px] cursor-pointer
    focus-visible:outline-none
    focus-visible:ring-2 focus-visible:ring-Subbrown-2
  "
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/page.tsx around lines 52 - 62, The className string on
the Link JSX has a duplicated "cursor-pointer" token; remove the redundant
occurrence (keep a single "cursor-pointer") in the className for the Link in the
groups page component (the JSX attribute named className where the Tailwind-like
utilities include hover:brightness-98), ensuring only one "cursor-pointer"
remains in that className string.
src/app/groups/[id]/bookcase/[bookId]/ReviewSection.tsx (1)

96-105: ⚠️ Potential issue | 🟡 Minor

Profile/name block has hover:brightness-95 cursor-pointer but no onClick handler.

Same deceptive-affordance issue as in TeamMemberItem.tsx — the div dims and shows a pointer cursor, but there is no interaction. Add an onClick (e.g., navigate to user profile) or remove the interactive styles.

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

In `@src/app/groups/`[id]/bookcase/[bookId]/ReviewSection.tsx around lines 96 -
105, The profile/name block in ReviewSection.tsx (the div wrapping Image and the
myName <p> using profileSrc and myName) has interactive styles
(hover:brightness-95 cursor-pointer) but no interaction; either remove those
styles or add a real click handler: implement an onClick on that div that
navigates to the user's profile (e.g., via router.push or a Link to the user's
page), and also add accessible attributes (role="button" and tabIndex={0}) and
keyboard handlers (onKeyDown for Enter/Space) so keyboard users can activate it;
ensure the handler uses the user id available in the component and update any
import of useRouter/Link as needed.
src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx (1)

19-37: ⚠️ Potential issue | 🟡 Minor

cursor-pointer + hover style on a non-interactive div creates a deceptive affordance.

The left container (profile image + name) has hover:brightness-95 cursor-pointer but no onClick handler. Users will perceive it as clickable (it dims and shows a pointer cursor) but nothing happens. Either wire up a click handler (e.g., navigate to the member's profile) or remove the interactive styling until the handler is implemented.

💡 Proposed fix
-      <div className="flex items-center gap-[12px] hover:brightness-95 cursor-pointer">
+      <div
+        className="flex items-center gap-[12px] hover:brightness-95 cursor-pointer"
+        onClick={onProfileClick}   // add an onProfileClick prop, or remove styles until needed
+      >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx` around lines 19 -
37, The container div in TeamMemberItem (the element with class "flex
items-center gap-[12px] hover:brightness-95 cursor-pointer") currently presents
a clickable affordance but has no click behavior; either remove the interactive
styling classes (hover:brightness-95 and cursor-pointer) to make it
non-interactive, or wire up an actual handler: add an onClick prop (and keyboard
support like onKeyDown/role="button" or make it an anchor/button) that performs
the intended action (e.g., navigate to the member profile using the member
id/name), keeping references to profileImageUrl and name as before so the UI
stays the same.
🧹 Nitpick comments (10)
src/components/base-ui/Bookcase/MeetingInfo.tsx (1)

38-46: cursor-pointer should be on the <button> element, not the inner <span>.

Browsers set <button> cursor to default by default. Placing cursor-pointer only on the inner span means the icon area and button padding show the default cursor, making the interactive zone feel inconsistent.

♻️ Proposed fix
          <button
            onClick={onManageGroupClick}
-           className="flex items-center gap-[8px] px-[8px] hover:bg-black/5 rounded transition-colors"
+           className="flex items-center gap-[8px] px-[8px] hover:bg-black/5 rounded transition-colors cursor-pointer"
            >
-           <span className="text-Gray-4 body_1_2 cursor-pointer">조 관리하기</span>
+           <span className="text-Gray-4 body_1_2">조 관리하기</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/MeetingInfo.tsx` around lines 38 - 46, In
MeetingInfo.tsx, the cursor-pointer class is applied to the inner <span> which
causes the button (and its icon/padding) to show the default cursor; remove
cursor-pointer from the span and add it to the <button> element's className (the
button with onManageGroupClick and className="flex items-center gap-[8px]
px-[8px] hover:bg-black/5 rounded transition-colors") so the entire interactive
area, including the icon and padding, uses the pointer cursor.
src/components/base-ui/Group-Search/search_groupsearch.tsx (1)

108-108: items-start on a <span> is a CSS no-op.

align-items (what items-start maps to) only affects flex/grid containers. A <span> is an inline element, so this class produces no layout change. The min-w-[38px] t:min-w-[75px] constraint is the meaningful addition here and should be kept.

♻️ Proposed fix
-           <span className='items-start min-w-[38px] t:min-w-[75px]'>{category}</span>
+           <span className='min-w-[38px] t:min-w-[75px]'>{category}</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_groupsearch.tsx` at line 108, The
span element rendering {category} includes the Tailwind class "items-start"
which is a no-op on an inline element; remove "items-start" and keep the width
constraints ("min-w-[38px] t:min-w-[75px]"); if you actually need cross-axis
alignment, either change the span to an inline-flex/div with "inline-flex
items-start" or wrap it in a flex container—update the element in
search_groupsearch.tsx where the span is rendered to reflect one of these
options.
src/components/base-ui/Group/group_admin_menu.tsx (1)

42-44: hover:brightness-50 is significantly more aggressive than the rest of the PR.

All other interactive buttons in this PR use hover:brightness-90 or hover:brightness-95 for subtle feedback. The brightness-50 here halves element brightness, which will be jarring on the "모임 관리하기" text+icon button. Consider aligning with the rest of the PR's hover convention (brightness-90).

Also, the trailing space in className="body_1_3 text-Gray-5 " on Line 44 is a no-op that can be cleaned up.

♻️ Proposed fix
-        className="flex items-center gap-2 hover:brightness-50 cursor-pointer"
+        className="flex items-center gap-2 hover:brightness-90 cursor-pointer"
-        <span className="body_1_3 text-Gray-5 ">모임 관리하기</span>
+        <span className="body_1_3 text-Gray-5">모임 관리하기</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group/group_admin_menu.tsx` around lines 42 - 44, The
hover style on the Group admin menu is too aggressive and the class string
contains a trailing space: update the container's className that currently
includes "hover:brightness-50" to use "hover:brightness-90" to match the PR
convention, and remove the trailing space from the span's className (the span
rendering "모임 관리하기") so it becomes "body_1_3 text-Gray-5" with no extra
whitespace.
src/components/base-ui/Group-Search/search_club_apply_modal.tsx (1)

171-173: LGTM — the width constraint, style correction (body_1_2), and hover/cursor additions look good.

Adjacent unchanged lines 112, 115, and 129 still reference Body_1_2 / Body_2_2 (capital B), which are inconsistent with the corrected lowercase body_1_2 on Line 173. If these are custom typography utilities defined with lowercase names, those three class references may be no-ops worth fixing in a follow-up.

#!/bin/bash
# Verify whether Body_1_2 and Body_2_2 are defined anywhere in the codebase
rg -n '\bBody_[0-9]' --type css --type scss
rg -n '\bBody_[0-9]' --type ts --type tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_club_apply_modal.tsx` around lines
171 - 173, There are inconsistent typography class names: replace the
capitalized class references "Body_1_2" and "Body_2_2" (found in the same
component) with the corrected lowercase utilities "body_1_2" and "body_2_2" so
they match the working class used ('body_1_2') in the updated button; update
every occurrence in search_club_apply_modal.tsx (and any adjacent JSX in this
component) to use the lowercase names to avoid no-op/mismatched tailwind
utilities.
src/components/base-ui/Float.tsx (1)

41-49: Screen readers may double-announce the label — use alt="" on the image inside an aria-label-ed button.

The <button> has aria-label={iconAlt} and the inner <Image> also has alt={iconAlt}. When a button has an explicit aria-label, the image's alt text is redundant and some screen readers will announce both.

♻️ Suggested fix
  <Image
    src={iconSrc}
-   alt={iconAlt}
+   alt=""
    width={24}
    height={24}
    className={["w-[24px] h-[24px] object-contain", iconClassName].join(" ")}
  />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Float.tsx` around lines 41 - 49, The button in the
Float component currently sets aria-label={iconAlt} while the nested <Image>
uses alt={iconAlt}, causing potential double-announcement; update the Image
usage (the JSX with Image, iconSrc, iconAlt, iconClassName) to use alt="" when
the button provides aria-label (or conditionally set alt to "" whenever
aria-label is present) so the button keeps aria-label={iconAlt} and the inner
Image is decorative only; ensure you retain width/height/className props and
only change the Image alt attribute.
src/components/base-ui/Bookcase/bookid/TeamSection.tsx (1)

41-41: hover:brightness-0 renders the arrow fully black on hover — verify this is intentional.

brightness-0 applies filter: brightness(0), making the SVG completely black regardless of its original color. The same pattern appears on the Contact modal close button in groups/[id]/page.tsx, so this appears to be a deliberate design choice — just confirm the /Arrow-Right2.svg looks correct when rendered solid black on hover.

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

In `@src/components/base-ui/Bookcase/bookid/TeamSection.tsx` at line 41, The hover
currently uses the Tailwind class hover:brightness-0 on the arrow container
(className in TeamSection.tsx) which applies filter:brightness(0) and renders
the /Arrow-Right2.svg fully black; confirm this is the intended visual or
replace hover:brightness-0 with a less extreme hover state (e.g.,
hover:brightness-75 or hover:opacity-80) on the same element to preserve the
SVG's color on hover; update the className on the element in TeamSection.tsx
(and mirror the same change in the Contact modal close button in
groups/[id]/page.tsx if consistency is desired).
src/app/groups/create/page.tsx (1)

153-173: "중복확인" button still uses hover:opacity-90 — inconsistent with the brightness-based hover used everywhere else in this file.

All other interactive buttons in this page were migrated to hover:brightness-90. This button retains hover:opacity-90 active:opacity-80, breaking visual consistency.

♻️ Suggested fix
- hover:opacity-90 active:opacity-80
+ hover:brightness-90 cursor-pointer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/create/page.tsx` around lines 153 - 173, The "중복확인" button JSX
still uses the old opacity-based states; update the class list inside the button
(the element using onClick={fakeCheckName} and props DuplicationCheckisDisabled,
DuplicationCheckisConfirmed, nameCheck) to replace "hover:opacity-90
active:opacity-80" with the brightness-based utility used elsewhere (e.g.,
"hover:brightness-90 active:brightness-90") so the hover/active behavior matches
other buttons on the page while keeping the existing disabled and conditional
styles intact.
src/components/base-ui/Group-Search/search_mybookclub.tsx (1)

96-105: Hover/cursor styles are redundant on <div> children of a <button>.

cursor-pointer and hover:brightness-98 are applied to <div> elements inside the <button> toggle. The browser cursor is already pointer over the whole button, and the brightness filter only covers the inner div's subtree — hovering the button's padding area outside the div triggers neither effect. Move the interactive styles to the <button> itself for consistent coverage.

♻️ Suggested fix
  <button
    type="button"
    onClick={() => setOpen((v) => !v)}
-   className="w-full rounded-[6px] bg-transparent text-[13px] flex items-center justify-center gap-[6px] text-Gray-3"
+   className="w-full rounded-[6px] bg-transparent text-[13px] flex items-center justify-center gap-[6px] text-Gray-3 hover:brightness-98 cursor-pointer"
  >
    {open ? (
-     <div className="flex items-center justify-center gap-1 hover:brightness-98 cursor-pointer">
+     <div className="flex items-center justify-center gap-1">
        <span className="text-Gray-7 body_1_2">접기</span>
        <Image src="/ArrowTop.svg" alt="" width={24} height={24} />
      </div>
    ) : (
-     <div className="flex items-center justify-center gap-1 hover:brightness-98  cursor-pointer">
+     <div className="flex items-center justify-center gap-1">
        <span className="text-Gray-7 body_1_2">전체보기</span>
        <Image src="/ArrowDown.svg" alt="" width={24} height={24} />
      </div>
    )}
  </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_mybookclub.tsx` around lines 96 -
105, Move the interactive styles (cursor-pointer and hover:brightness-98) from
the inner <div> elements to the parent <button> that wraps the toggle so the
entire button area—including padding—shows the pointer and hover effect; update
the JSX in the Group-Search toggle (the conditional rendering block that
currently renders the inner divs with classes "flex items-center justify-center
gap-1 hover:brightness-98 cursor-pointer") by adding those classes to the button
element and removing them from the inner divs, keeping layout classes (flex,
gap, etc.) on the inner container only and preserving the existing text and
Image nodes.
src/components/base-ui/Group-Create/Chip.tsx (1)

27-27: disabled:hover:bg-White is dead code after the hover-style migration.

Previously this prevented a background-color hover on disabled chips. Since hover now uses hover:brightness-98, the background override is a no-op. Consider removing it, and if you want to suppress the brightness effect on disabled chips, add disabled:hover:brightness-100.

♻️ Suggested fix
- "disabled:text-Gray-4 disabled:border-Gray-4 disabled:hover:bg-White disabled:cursor-not-allowed",
+ "disabled:text-Gray-4 disabled:border-Gray-4 disabled:hover:brightness-100 disabled:cursor-not-allowed",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Create/Chip.tsx` at line 27, Remove the dead
utility class "disabled:hover:bg-White" from the Chip component's className list
(in the Group-Create/Chip.tsx code where the disabled classes are composed) and,
if you want to prevent the hover brightness effect on disabled chips, replace it
with "disabled:hover:brightness-100" so disabled state suppresses the hover
brightness; update the class string used by the Chip component accordingly (look
for the disabled:text-Gray-4 disabled:border-Gray-4 entry and edit there).
src/components/base-ui/Bookcase/bookid/ReviewList.tsx (1)

96-102: Next.js Image intrinsic size (width=28) is smaller than the CSS desktop size (d:w-[40px])

The tablet+ profile <Image> declares width={28} height={28} for Next.js optimization but the Tailwind class d:w-[40px] d:h-[40px] renders it at 40×40 on desktop. Next.js uses the width/height props to size the served image, so on desktop it upscales a 28px-optimized image to 40px, potentially appearing blurry on HiDPI screens. Use the largest rendered size (40) as the intrinsic dimension, or add a sizes prop.

♻️ Proposed fix
                <Image
                  src={profileSrc}
                  alt=""
-                 width={28}
-                 height={28}
+                 width={40}
+                 height={40}
                  className="rounded-full object-cover w-[28px] h-[28px] d:w-[40px] d:h-[40px]"
                />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/bookid/ReviewList.tsx` around lines 96 - 102,
The Next.js Image is declared with intrinsic width/height 28 while Tailwind
classes (d:w-[40px] d:h-[40px]) render it at 40×40 on desktop causing upscaling;
update the Image props in ReviewList (the <Image ... /> using profileSrc) to use
the largest rendered dimensions (width={40} height={40}) or alternatively keep
28/28 and add a responsive sizes prop that guarantees the browser requests 40px
on desktop (e.g., sizes informed by your breakpoints) so the served image
matches the CSS render size and avoids blurriness on HiDPI screens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(main)/news/[id]/page.tsx:
- Around line 163-166: The FloatingFab instance in NewsDetailPage is rendered
without an onClick handler, so the button does nothing; fix by making the click
behavior client-side: either extract the FloatingFab usage into a small client
wrapper component (e.g., NewsContactFabClient) or convert the page to a client
component and pass an onClick prop to FloatingFab that performs the desired
action (navigate to /contact, open a modal, or call router.push) and ensure
FloatingFab remains a button type="button". Update the NewsDetailPage render to
use the new client wrapper or pass the onClick prop to FloatingFab so the
inquiry button becomes functional.
- Around line 1-3: This file is a server component and the project currently
depends on vulnerable React versions; update the react and react-dom
dependencies to at least 19.2.4 (e.g., in package.json replace react/react-dom
19.2.0 with 19.2.4+), regenerate the lockfile (npm/yarn/pnpm install) to update
package-lock.json or pnpm-lock.yaml, ensure Next.js is at 16.1.6+ if using that
patch, run the full test/build (including pages using FloatingFab,
TodayRecommendedBooks, and next/image) and redeploy to confirm no runtime/SSR
regressions.

In `@src/app/`(main)/news/page.tsx:
- Around line 110-113: The FloatingFab instance in page.tsx is missing an
onClick handler so the "문의하기" button does nothing; since this is already a "use
client" component, add an onClick prop to the FloatingFab (the same way you
fixed it in news/[id]/page.tsx) that triggers the inquiry action (e.g., open
modal or navigate to contact) — locate the FloatingFab usage and provide an
inline handler function for onClick that calls the existing inquiry/opening
logic or dispatches the appropriate UI action.

In `@src/app/groups/`[id]/bookcase/[bookId]/meeting/page.tsx:
- Around line 388-391: FloatingFab is rendered in meeting page without the
required onClick handler (FloatingFab in Float.tsx accepts onClick but defaults
to undefined), leaving a non-functional FAB; fix by passing a handler from the
page (e.g., onClick={() => /* open inquiry modal or navigate to /inquiry */} or
a temporary noop like () => {}) or remove the FloatingFab until the feature is
implemented — update the JSX where FloatingFab is used to supply the onClick
prop so the button performs an action.

In `@src/app/groups/`[id]/bookcase/page.tsx:
- Around line 81-85: The FloatingFab call uses iconAlt which FloatingFab maps to
aria-label, so change the misleading iconAlt="문의하기" to a context-appropriate
label (e.g., iconAlt="새 책장 항목 추가" or "책장 항목 추가") in the FloatingFab instance
that navigates to `/groups/${groupId}/admin/bookcase/new`; also audit the other
FloatingFab usages (in meeting/page.tsx, UserProfile.tsx, notice/page.tsx) and
replace copy-pasted "문의하기" with descriptive aria labels relevant to each action
(or if FloatingFab supports a dedicated ariaLabel prop, pass that instead and
leave iconAlt as the visual alt text).

In `@src/app/groups/`[id]/dummy.ts:
- Around line 20-21: The array contains duplicate category objects reusing codes
COMPUTER_IT and HISTORY_CULTURE with mismatched descriptions; fix by either
removing the erroneous entries (the objects with description '정치/외교/국방' and
'어린이/청소년') or remapping them to valid ClubCategoryCode values, or if those
categories are required add new enum members (e.g., POLITICS, CHILDREN_YOUTH) to
the ClubCategoryCode type and update all usages; update the entries in the array
so each object uses a unique, semantically correct code matching the
ClubCategoryCode definition (check places that reference COMPUTER_IT,
HISTORY_CULTURE and the ClubCategoryCode type).

In `@src/app/groups/`[id]/notice/page.tsx:
- Around line 171-175: The FloatingFab in this component uses iconAlt="문의하기",
which is incorrect for the create-notice action; update the aria label by
changing the iconAlt prop on the FloatingFab used with handleAddNotice to a
clear label reflecting creating a notice (e.g., "공지사항 작성" or "새 공지 작성") so
screen readers announce the correct action; ensure the change is applied to the
FloatingFab invocation in this file (the instance with
onClick={handleAddNotice}).

In `@src/app/groups/`[id]/page.tsx:
- Line 43: isAdmin is hardcoded to true which exposes GroupAdminMenu to
everyone; change the logic so isAdmin (and setIsAdmin) are driven by real
auth/session and group membership data instead of a constant. Initialize isAdmin
to false, then in a useEffect (or server-side data fetch) retrieve the current
user/session and the group's admin/membership info (using your existing
auth/session hook or API), compute whether the current user is an admin of the
group, and call setIsAdmin accordingly so GroupAdminMenu only renders when the
computed flag is true.

In `@src/app/groups/create/page.tsx`:
- Around line 225-227: The disabled buttons still get hover brightness because
the old disabled:hover:bg-Gray-2 class is dead; locate the button class
arrays/strings used for navigation buttons in this page (the same concatenation
that contains "bg-primary-1 text-White hover:brightness-90 ...
disabled:bg-Gray-2 ..."), remove the obsolete "disabled:hover:bg-Gray-2" and add
"disabled:hover:brightness-100" alongside the existing "disabled:bg-Gray-2
disabled:cursor-not-allowed"; apply this exact replacement for every navigation
button class string instance mentioned in the comment (the occurrences around
the btn class declarations).

In `@src/components/base-ui/Bookcase/bookid/ReviewList.tsx`:
- Around line 44-55: In ReviewList.tsx the profile block (the div wrapping the
Image and {item.name}) is styled as interactive but has no click handler; either
convert that wrapper to a semantic clickable element (e.g., a <button> or your
routing <Link>) and add the navigation/onClick handler that navigates to the
user's profile (use the existing profileSrc/item.id or item.name as needed), or
remove the interactive styles (cursor-pointer and hover:brightness-95) to avoid
misleading users; update both occurrences in the ReviewList component that
render the profile block (the Image/profileSrc and the paragraph showing
item.name).

In `@src/components/base-ui/Bookcase/bookid/TeamFilter.tsx`:
- Around line 25-31: Update the className on the TeamFilter button to use
Tailwind-compatible utilities: replace the invalid hover:brightness-97 with an
arbitrary value like hover:brightness-[0.97] (or hover:brightness-[97%]) and
change transition-colors to a transition that includes transforms (e.g.,
transition-transform or transition-all) so the hover:-translate-y-[3px]
animates; locate this in TeamFilter.tsx inside the className template where
isActive toggles bg-primary-2 / bg-White.

In
`@src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx`:
- Line 171: In the SearchClubListItem component, the button/container div
currently has conflicting Tailwind width classes "w-[100px] w-[132px]"; remove
the unwanted class so only the intended width remains (e.g., keep "w-[132px]"
and delete "w-[100px]") in the div with className used for the button container
to avoid fragile, implementation-defined styling.

In `@src/components/base-ui/Group/DebateList.tsx`:
- Around line 42-48: The next/image usages in DebateList (the Image rendering
profileSrc) pass width={28} height={28} while CSS renders 24px on mobile and
40px at the desktop breakpoint, causing inefficient/upsampled images; update the
Image props to match the largest rendered size (use width={40} height={40}) and
add a sizes prop that mirrors your CSS breakpoints (e.g. "(min-width:
<tablet-breakpoint>) 40px, 24px") so the optimizer serves correct srcset for
both mobile and desktop; locate the Image instances in DebateList and modify the
width/height and add sizes accordingly.
- Around line 41-52: The profile/name wrapper div in the DebateList component
(the element rendering profileSrc and item.name) currently uses
hover:brightness-95 and cursor-pointer but has no click or keyboard handlers;
either add the intended click behavior (attach an onClick that navigates to the
profile, and add accessible affordances such as role="button", tabIndex={0}, and
an onKeyDown handling Enter/Space) to the same wrapper, or remove the
interactive classes (hover:brightness-95 and cursor-pointer) if it’s purely
decorative so it no longer falsely appears clickable.

In `@src/components/base-ui/LongtermInput.tsx`:
- Line 76: Replace the invalid Tailwind v4 class in the LongtermInput
component's className string—change the current `hover:brightness-70` token to
the arbitrary value syntax `hover:brightness-[0.70]` so the element using
className="shrink-0 ml-5 w-6 h-6 relative hover:brightness-70 cursor-pointer"
becomes valid Tailwind; update the className in the LongtermInput component
accordingly.

In `@src/components/base-ui/MyPage/UserProfile.tsx`:
- Around line 103-106: FloatingFab in UserProfile.tsx is missing an onClick
handler and uses the wrong semantic label; add an onClick prop that invokes the
profile-edit action (e.g., call an existing openEditProfile handler or router
push to the profile edit route) so the FAB is functional, and change iconAlt
from "문의하기" to a clear edit label like "프로필 수정" (or "Edit profile") so Float.tsx
will expose the correct aria-label; ensure the same pattern is applied as in
meeting/page.tsx where FABs are interactive.

---

Outside diff comments:
In `@src/app/groups/`[id]/bookcase/[bookId]/DebateSection.tsx:
- Around line 80-91: The profile container in DebateSection (the div wrapping
Image with profileSrc and the paragraph showing myName) currently has
interactive classes (cursor-pointer and hover:brightness-95) but no
click/keyboard handlers or ARIA role; either remove those classes if it’s purely
decorative, or make it truly interactive by wiring a navigation handler (e.g.,
onClick that navigates to the user profile), adding keyboard support (tabIndex=0
and onKeyDown handling Enter/Space), and an accessible role/aria-label (or wrap
with a Link) so screen readers and keyboard users can activate it; update the
div used in DebateSection accordingly to implement one of these two options.

In `@src/app/groups/`[id]/bookcase/[bookId]/ReviewSection.tsx:
- Around line 96-105: The profile/name block in ReviewSection.tsx (the div
wrapping Image and the myName <p> using profileSrc and myName) has interactive
styles (hover:brightness-95 cursor-pointer) but no interaction; either remove
those styles or add a real click handler: implement an onClick on that div that
navigates to the user's profile (e.g., via router.push or a Link to the user's
page), and also add accessible attributes (role="button" and tabIndex={0}) and
keyboard handlers (onKeyDown for Enter/Space) so keyboard users can activate it;
ensure the handler uses the user id available in the component and update any
import of useRouter/Link as needed.

In `@src/app/groups/`[id]/page.tsx:
- Around line 52-62: The className string on the Link JSX has a duplicated
"cursor-pointer" token; remove the redundant occurrence (keep a single
"cursor-pointer") in the className for the Link in the groups page component
(the JSX attribute named className where the Tailwind-like utilities include
hover:brightness-98), ensuring only one "cursor-pointer" remains in that
className string.

In `@src/app/groups/page.tsx`:
- Around line 53-59: Remove the debug console.log statements that leak
user-entered content: inside onSubmitApply (function onSubmitApply) delete the
console.log('apply:', clubId, reason) and instead invoke the real API call (or a
service function like applyToClub(clubId, reason)) and handle success/error
(then call setApplyClubId(null)); also remove the inline console.log used in the
onSubmit JSX prop (replace with onSubmitApply invocation or the proper submit
handler) so no user input is written to the console.

In `@src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx`:
- Around line 19-37: The container div in TeamMemberItem (the element with class
"flex items-center gap-[12px] hover:brightness-95 cursor-pointer") currently
presents a clickable affordance but has no click behavior; either remove the
interactive styling classes (hover:brightness-95 and cursor-pointer) to make it
non-interactive, or wire up an actual handler: add an onClick prop (and keyboard
support like onKeyDown/role="button" or make it an anchor/button) that performs
the intended action (e.g., navigate to the member profile using the member
id/name), keeping references to profileImageUrl and name as before so the UI
stays the same.

---

Duplicate comments:
In `@src/app/`(main)/stories/page.tsx:
- Line 20: The className contains a non-existent Tailwind utility
"scrollbar-hide" — remove that token from the div (the element with className
containing "scrollbar-hide" in page.tsx) and either install/enable the
tailwind-scrollbar-hide plugin, or replace it with the inline approach used
elsewhere: add the inline styles that hide scrollbars for different engines
(e.g. MS/Firefox properties like MsOverflowStyle and scrollbarWidth) and ensure
WebKit scrollbars are hidden via the project's global CSS (::-webkit-scrollbar {
display: none; }) so the div no longer relies on a missing utility.

In `@src/app/groups/`[id]/notice/page.tsx:
- Around line 170-176: The FloatingFab is colliding with the pagination bar on
small screens; update the FloatingFab usage (the JSX where FloatingFab is
rendered and handleAddNotice is passed) to increase its bottom offset on mobile
by adding a responsive class or prop so it sits above the pagination bar (e.g.,
larger bottom spacing on small viewports and keep the existing bottom on larger
screens); adjust the z-index if needed (FloatingFab z-[60]) but primarily change
the bottom spacing responsively (e.g., mobile bottom > pagination bottom) either
by passing a className/position prop into FloatingFab or by updating
FloatingFab's internal styles to use a mobile-specific bottom value.
- Around line 104-167: The pagination UI is not actually paginating because
notices.map renders all items and totalPages is hardcoded; update the logic so
totalPages is computed as Math.ceil(notices.length / pageSize) and derive the
displayed items by slicing notices using currentPage and pageSize before mapping
(e.g., compute start = (currentPage - 1) * pageSize and end = start + pageSize
and map over notices.slice(start, end)); ensure pageSize is defined (or passed
in) and that setCurrentPage boundaries still reference the computed totalPages.
- Line 105: The fixed bar's className in the JSX div uses the incorrect Tailwind
token "d:left-50" which sets left:12.5rem and breaks centering; replace that
token with the correct responsive centering classes (e.g., use "d:left-1/2" and
add the horizontal translate class "d:-translate-x-1/2", and clear right with
"d:right-auto" if needed) on the same div in src/app/groups/[id]/notice/page.tsx
so the fixed bar is centered at the d: breakpoint instead of offset by 200px.

In `@src/components/base-ui/MyPage/UserProfile.tsx`:
- Around line 88-90: The div in UserProfile.tsx with className containing
"gap-[12px]" is a block element so gap has no effect; either remove the
"gap-[12px]" utility from that div's className or make the element a flex/grid
container (e.g., add "flex" or "grid") on the same element or its parent that
actually contains multiple children you want spaced. Locate the div rendering
{user.intro} and choose one: delete the gap utility if it's unused, or add
"flex" / "grid" to the appropriate container (keeping "gap-[12px]" only on an
element that is a flex/grid container).

---

Nitpick comments:
In `@src/app/groups/create/page.tsx`:
- Around line 153-173: The "중복확인" button JSX still uses the old opacity-based
states; update the class list inside the button (the element using
onClick={fakeCheckName} and props DuplicationCheckisDisabled,
DuplicationCheckisConfirmed, nameCheck) to replace "hover:opacity-90
active:opacity-80" with the brightness-based utility used elsewhere (e.g.,
"hover:brightness-90 active:brightness-90") so the hover/active behavior matches
other buttons on the page while keeping the existing disabled and conditional
styles intact.

In `@src/components/base-ui/Bookcase/bookid/ReviewList.tsx`:
- Around line 96-102: The Next.js Image is declared with intrinsic width/height
28 while Tailwind classes (d:w-[40px] d:h-[40px]) render it at 40×40 on desktop
causing upscaling; update the Image props in ReviewList (the <Image ... /> using
profileSrc) to use the largest rendered dimensions (width={40} height={40}) or
alternatively keep 28/28 and add a responsive sizes prop that guarantees the
browser requests 40px on desktop (e.g., sizes informed by your breakpoints) so
the served image matches the CSS render size and avoids blurriness on HiDPI
screens.

In `@src/components/base-ui/Bookcase/bookid/TeamSection.tsx`:
- Line 41: The hover currently uses the Tailwind class hover:brightness-0 on the
arrow container (className in TeamSection.tsx) which applies
filter:brightness(0) and renders the /Arrow-Right2.svg fully black; confirm this
is the intended visual or replace hover:brightness-0 with a less extreme hover
state (e.g., hover:brightness-75 or hover:opacity-80) on the same element to
preserve the SVG's color on hover; update the className on the element in
TeamSection.tsx (and mirror the same change in the Contact modal close button in
groups/[id]/page.tsx if consistency is desired).

In `@src/components/base-ui/Bookcase/MeetingInfo.tsx`:
- Around line 38-46: In MeetingInfo.tsx, the cursor-pointer class is applied to
the inner <span> which causes the button (and its icon/padding) to show the
default cursor; remove cursor-pointer from the span and add it to the <button>
element's className (the button with onManageGroupClick and className="flex
items-center gap-[8px] px-[8px] hover:bg-black/5 rounded transition-colors") so
the entire interactive area, including the icon and padding, uses the pointer
cursor.

In `@src/components/base-ui/Float.tsx`:
- Around line 41-49: The button in the Float component currently sets
aria-label={iconAlt} while the nested <Image> uses alt={iconAlt}, causing
potential double-announcement; update the Image usage (the JSX with Image,
iconSrc, iconAlt, iconClassName) to use alt="" when the button provides
aria-label (or conditionally set alt to "" whenever aria-label is present) so
the button keeps aria-label={iconAlt} and the inner Image is decorative only;
ensure you retain width/height/className props and only change the Image alt
attribute.

In `@src/components/base-ui/Group-Create/Chip.tsx`:
- Line 27: Remove the dead utility class "disabled:hover:bg-White" from the Chip
component's className list (in the Group-Create/Chip.tsx code where the disabled
classes are composed) and, if you want to prevent the hover brightness effect on
disabled chips, replace it with "disabled:hover:brightness-100" so disabled
state suppresses the hover brightness; update the class string used by the Chip
component accordingly (look for the disabled:text-Gray-4 disabled:border-Gray-4
entry and edit there).

In `@src/components/base-ui/Group-Search/search_club_apply_modal.tsx`:
- Around line 171-173: There are inconsistent typography class names: replace
the capitalized class references "Body_1_2" and "Body_2_2" (found in the same
component) with the corrected lowercase utilities "body_1_2" and "body_2_2" so
they match the working class used ('body_1_2') in the updated button; update
every occurrence in search_club_apply_modal.tsx (and any adjacent JSX in this
component) to use the lowercase names to avoid no-op/mismatched tailwind
utilities.

In `@src/components/base-ui/Group-Search/search_groupsearch.tsx`:
- Line 108: The span element rendering {category} includes the Tailwind class
"items-start" which is a no-op on an inline element; remove "items-start" and
keep the width constraints ("min-w-[38px] t:min-w-[75px]"); if you actually need
cross-axis alignment, either change the span to an inline-flex/div with
"inline-flex items-start" or wrap it in a flex container—update the element in
search_groupsearch.tsx where the span is rendered to reflect one of these
options.

In `@src/components/base-ui/Group-Search/search_mybookclub.tsx`:
- Around line 96-105: Move the interactive styles (cursor-pointer and
hover:brightness-98) from the inner <div> elements to the parent <button> that
wraps the toggle so the entire button area—including padding—shows the pointer
and hover effect; update the JSX in the Group-Search toggle (the conditional
rendering block that currently renders the inner divs with classes "flex
items-center justify-center gap-1 hover:brightness-98 cursor-pointer") by adding
those classes to the button element and removing them from the inner divs,
keeping layout classes (flex, gap, etc.) on the inner container only and
preserving the existing text and Image nodes.

In `@src/components/base-ui/Group/group_admin_menu.tsx`:
- Around line 42-44: The hover style on the Group admin menu is too aggressive
and the class string contains a trailing space: update the container's className
that currently includes "hover:brightness-50" to use "hover:brightness-90" to
match the PR convention, and remove the trailing space from the span's className
(the span rendering "모임 관리하기") so it becomes "body_1_3 text-Gray-5" with no
extra whitespace.

Comment on lines +1 to 3
import FloatingFab from "@/components/base-ui/Float";
import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books";
import Image from "next/image";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check installed React, React-DOM, and Next.js versions in package.json
fd -g 'package.json' -E 'node_modules' --exec grep -H '"react"\|"next"\|"react-dom"' {}

Repository: checkmo2025/FE

Length of output: 175


🌐 Web query:

CVE-2025-55182 CVE-2025-66478 React 19.2.0 RCE

💡 Result:

  • CVE-2025-55182 (“React2Shell”): a pre-auth unauthenticated RCE in React Server Components (RSC) due to unsafe deserialization in the RSC/“Flight” protocol. It affects React 19.0.0, 19.1.0, 19.1.1, and 19.2.0 (notably the react-server-dom-* packages). React 19.2.0 is vulnerable. Patched in React 19.2.1 (and also in the 19.0/19.1 patch lines). [1][2] (cloud.google.com)
  • CVE-2025-66478 (Next.js): this was the Next.js tracking ID for the same underlying issue in frameworks bundling vulnerable RSC (e.g., Next.js 15.x/16.x App Router), and was later marked as a duplicate of CVE-2025-55182. Upgrade Next.js to one of the listed patched releases. [1][3] (cloud.google.com)

What to do: if you run React 19.2.0, upgrade at least to React 19.2.1 (and upgrade Next.js to a patched version if applicable), then redeploy. [1] (cloud.google.com)

Sources
[1] Google Cloud blog: “Responding to CVE-2025-55182” (cloud.google.com)
[2] NVD entry for CVE-2025-55182 (nvd.nist.gov)
[3] CIRCL/Vulnerability-Lookup entry for CVE-2025-66478 (cve.circl.lu)

Citations:


🌐 Web query:

CVE-2025-55184 CVE-2025-67779 React infinite loop DoS

💡 Result:

CVE-2025-55184 and CVE-2025-67779 are high-severity (CVSS 7.5) denial-of-service (DoS) issues in React Server Components / Server Functions: a specially crafted HTTP request to a Server Function endpoint can trigger unsafe deserialization that leads to an infinite loop, pegging CPU and hanging the server process (pre-auth, no user interaction). CVE-2025-67779 exists because the original fix for CVE-2025-55184 was incomplete. [1][3]

What to do

  • Upgrade React to patched versions. React’s advisory notes additional fixes were published Jan 26, 2026, and states React 19.0.4 / 19.1.5 / 19.2.4 are safe. [1]
  • If you can’t upgrade immediately, consider edge/WAF mitigation: Fastly released a virtual patch for CVE-2025-55184 that also covers CVE-2025-67779 (enabled by default for their Next‑Gen WAF customers). [2]

Who is most likely affected

  • Apps using React Server Components / Server Functions (commonly via frameworks like Next.js App Router and other RSC-enabled setups). [3][1]

Sources: React security advisory (React.dev) [1], Fastly WAF virtual patch notice [2], Orca Security write-up [3].


🌐 Web query:

CVE-2026-23864 React vulnerability

💡 Result:

CVE-2026-23864 is a high-severity (CVSS 7.5) denial-of-service (DoS) vulnerability in React Server Components (RSC). It can be triggered by specially crafted HTTP requests to Server Function endpoints, potentially causing server crashes, out-of-memory conditions, or excessive CPU usage. (react.dev)

What’s affected (React):

  • RSC runtime packages: react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack
  • Vulnerable versions: 19.0.0–19.2.3 (including 19.0.3 / 19.1.4 / 19.2.3, which were incomplete fixes) (react.dev)
  • Fixed versions: 19.0.4, 19.1.5, 19.2.4 (react.dev)

What’s affected (Next.js / frameworks using RSC):

  • Impacts frameworks that actively use RSC (not “React on the client” by itself). (react.dev)
  • Akamai lists Next.js fixes (examples): 15.0.8, 15.1.11, 15.2.9, 15.3.9, 15.4.11, 15.5.10, 16.0.11, 16.1.5, plus canary fixes; and notes some older major lines “will not be fixed.” (akamai.com)

Am I affected if I only use React in the browser (no server / no RSC)?

  • Generally no—if you don’t use a server or a framework/bundler that supports React Server Components, you’re not affected. (react.dev)

What to do

  • Upgrade immediately to one of the patched React RSC versions (19.0.4 / 19.1.5 / 19.2.4) and update your framework (e.g., Next.js) accordingly. (react.dev)

Citations:


🌐 Web query:

CVE-2025-55182 CVSS score severity React 19.2.0

💡 Result:

  • CVE-2025-55182 (React2Shell) CVSS / severity: CVSS base score 10.0 — Critical (as assigned by CVE.org per Google’s advisory). [1]
  • React 19.2.0 status: Vulnerable (affected React versions include 19.0.0, 19.1.0, 19.1.1, 19.2.0), fixed in 19.2.1. [1]
  • Why you may see a different score in some tools: Sonatype reports CVSS v4 = 9.3 (Critical) due to its own scoring/precedence while NVD had not yet published an NVD-assessed CVSS vector at the time of their note (and NVD may still show “assessment not yet provided”). [2] [3]

Sources: [1] [2] [3]


🌐 Web query:

React 19.2.4 CVE-2026-23864 patched versions 19.2.1 19.2.3

💡 Result:

CVE-2026-23864 (DoS in React Server Components / Server Functions) affects the react-server-dom-* packages—not the “react” package in general—and 19.2.1 and 19.2.3 are still vulnerable (they were earlier/incomplete fixes). (react.dev)

Fixed (patched) versions:

Affected examples (19.2 line):

Citations:


🌐 Web query:

Next.js 16.1.6 CVE-2026-23864 patch

💡 Result:

Next.js 16.1.6 is already patched for CVE-2026-23864—Vercel’s advisory lists 16.1.5 as the first fixed release in the 16.1 line, so anything ≥ 16.1.5 (including 16.1.6) is not affected. [1][2]

Patch / upgrade:

npm i next@16.1.6
# or
yarn add next@16.1.6
# or
pnpm add next@16.1.6

If you can’t move to 16.1.6 for some reason, the minimum safe target is next@16.1.5. [1][2]


🌐 Web query:

Next.js 16.1.5 16.1.6 CVE-2026-23864 patched versions

💡 Result:

For CVE-2026-23864 in Next.js (App Router / Server Components), the Next.js 16.x patch levels are:

  • Vulnerable: >= 16.0.0 and < 16.1.5 (so 16.1.4 and earlier are vulnerable). [1]
  • Patched (minimum safe):
    • 16.0.11 (for the 16.0.x line) [1]
    • 16.1.5 (first fixed release for the 16.1.x line) [1][2]
  • Therefore 16.1.5 is patched, and 16.1.6 (newer than 16.1.5) is also patched. [1][3]

Sources: [1] GitHub Security Advisory (vercel/next.js) GHSA-h25m-26qc-wcjf, [2] Akamai advisory, [3] Next.js GitHub release tag v16.1.6.


Upgrade react and react-dom to 19.2.4+ — active RCE and DoS CVEs affect 19.2.0

The project uses react@19.2.0 / react-dom@19.2.0, which are confirmed vulnerable in multiple advisories that directly affect App Router server components like this page:

CVE Severity Impact Fixed in React
CVE-2025-55182 / CVE-2025-66478 CVSS 10.0 Critical Unauthenticated attacker can execute arbitrary code on a server using nothing more than a crafted HTTP request 19.2.1+
CVE-2025-55184 / CVE-2025-67779 CVSS 7.5 High An attacker can force a vulnerable application server into an infinite loop by crafting a specific request 19.2.4+
CVE-2026-23864 CVSS 7.5 High DoS via specially crafted HTTP requests (January 2026) 19.2.4+

The vulnerability is present in versions 19.0, 19.1.0, 19.1.1, and 19.2.0; initial fixes were incomplete through 19.2.3. A complete fix was issued in versions 19.0.4, 19.1.5, and 19.2.4.

Action: Upgrade react and react-dom to 19.2.4+ immediately. Next.js 16.1.6 already includes the patch for CVE-2026-23864 (patched from 16.1.5 onwards).

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

In `@src/app/`(main)/news/[id]/page.tsx around lines 1 - 3, This file is a server
component and the project currently depends on vulnerable React versions; update
the react and react-dom dependencies to at least 19.2.4 (e.g., in package.json
replace react/react-dom 19.2.0 with 19.2.4+), regenerate the lockfile
(npm/yarn/pnpm install) to update package-lock.json or pnpm-lock.yaml, ensure
Next.js is at 16.1.6+ if using that patch, run the full test/build (including
pages using FloatingFab, TodayRecommendedBooks, and next/image) and redeploy to
confirm no runtime/SSR regressions.

Comment on lines +163 to +166
<FloatingFab
iconSrc="/icons_calling.svg"
iconAlt="문의하기"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"문의하기" FloatingFab has no onClick — button is completely non-functional

No onClick prop is provided. Since FloatingFab renders a <button type="button">, clicking it silently does nothing. An inquiry/contact button with zero behavior is a broken user-facing feature.

🐛 Proposed fix — wire up a navigation or modal handler
+import { useRouter } from "next/navigation";
 // or pass a handler from a parent if this becomes a client component
 
-      <FloatingFab
-              iconSrc="/icons_calling.svg"
-              iconAlt="문의하기"
-      />
+      <FloatingFab
+        iconSrc="/icons_calling.svg"
+        iconAlt="문의하기"
+        onClick={() => { /* navigate to contact page or open modal */ }}
+      />

Note: since NewsDetailPage is an async server component, adding an onClick requires either extracting FloatingFab into a wrapper client component or converting the page to "use client".

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

In `@src/app/`(main)/news/[id]/page.tsx around lines 163 - 166, The FloatingFab
instance in NewsDetailPage is rendered without an onClick handler, so the button
does nothing; fix by making the click behavior client-side: either extract the
FloatingFab usage into a small client wrapper component (e.g.,
NewsContactFabClient) or convert the page to a client component and pass an
onClick prop to FloatingFab that performs the desired action (navigate to
/contact, open a modal, or call router.push) and ensure FloatingFab remains a
button type="button". Update the NewsDetailPage render to use the new client
wrapper or pass the onClick prop to FloatingFab so the inquiry button becomes
functional.

Comment on lines +110 to +113
<FloatingFab
iconSrc="/icons_calling.svg"
iconAlt="문의하기"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same broken "문의하기" FloatingFab — missing onClick here too

Identical to the issue in src/app/(main)/news/[id]/page.tsx: no onClick means clicking this inquiry button silently does nothing. Unlike the detail page, this is already a "use client" component, so an inline handler can be added directly.

🐛 Proposed fix
-      <FloatingFab
-        iconSrc="/icons_calling.svg"
-        iconAlt="문의하기"
-      />
+      <FloatingFab
+        iconSrc="/icons_calling.svg"
+        iconAlt="문의하기"
+        onClick={() => { /* e.g. router.push("/contact") or open modal */ }}
+      />
📝 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
<FloatingFab
iconSrc="/icons_calling.svg"
iconAlt="문의하기"
/>
<FloatingFab
iconSrc="/icons_calling.svg"
iconAlt="문의하기"
onClick={() => { /* e.g. router.push("/contact") or open modal */ }}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(main)/news/page.tsx around lines 110 - 113, The FloatingFab
instance in page.tsx is missing an onClick handler so the "문의하기" button does
nothing; since this is already a "use client" component, add an onClick prop to
the FloatingFab (the same way you fixed it in news/[id]/page.tsx) that triggers
the inquiry action (e.g., open modal or navigate to contact) — locate the
FloatingFab usage and provide an inline handler function for onClick that calls
the existing inquiry/opening logic or dispatches the appropriate UI action.

Comment on lines +388 to +391
<FloatingFab
iconSrc="/icons_chat.svg"
iconAlt="문의하기"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FloatingFab renders a non-functional button — onClick prop is missing.

FloatingFab accepts onClick which defaults to undefined in Float.tsx. Without it, the button is a visible affordance that does nothing when clicked. Define and pass a handler, or remove the FAB until the feature is ready.

💡 Proposed fix (placeholder until feature is defined)
-      <FloatingFab
-              iconSrc="/icons_chat.svg"
-              iconAlt="문의하기"
-            />
+      {/* TODO: wire up inquiry/contact handler */}
+      <FloatingFab
+        iconSrc="/icons_chat.svg"
+        iconAlt="문의하기"
+        onClick={() => { /* router.push(…) */ }}
+      />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/meeting/page.tsx around lines 388 -
391, FloatingFab is rendered in meeting page without the required onClick
handler (FloatingFab in Float.tsx accepts onClick but defaults to undefined),
leaving a non-functional FAB; fix by passing a handler from the page (e.g.,
onClick={() => /* open inquiry modal or navigate to /inquiry */} or a temporary
noop like () => {}) or remove the FloatingFab until the feature is implemented —
update the JSX where FloatingFab is used to supply the onClick prop so the
button performs an action.

Comment on lines +81 to +85
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="문의하기"
onClick={() => router.push(`/groups/${groupId}/admin/bookcase/new`)}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

iconAlt="문의하기" ("inquiry") is misleading for a "create bookcase entry" action.

FloatingFab sets aria-label={iconAlt}, so screen readers will announce "문의하기" for a button that navigates to admin/bookcase/new. The same copy-paste label appears in meeting/page.tsx, UserProfile.tsx, and notice/page.tsx as well.

       <FloatingFab
         iconSrc="/icons_pencil.svg"
-        iconAlt="문의하기"
+        iconAlt="책장 항목 추가"
         onClick={() => router.push(`/groups/${groupId}/admin/bookcase/new`)}
       />
📝 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
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="문의하기"
onClick={() => router.push(`/groups/${groupId}/admin/bookcase/new`)}
/>
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="책장 항목 추가"
onClick={() => router.push(`/groups/${groupId}/admin/bookcase/new`)}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/page.tsx around lines 81 - 85, The FloatingFab
call uses iconAlt which FloatingFab maps to aria-label, so change the misleading
iconAlt="문의하기" to a context-appropriate label (e.g., iconAlt="새 책장 항목 추가" or "책장
항목 추가") in the FloatingFab instance that navigates to
`/groups/${groupId}/admin/bookcase/new`; also audit the other FloatingFab usages
(in meeting/page.tsx, UserProfile.tsx, notice/page.tsx) and replace copy-pasted
"문의하기" with descriptive aria labels relevant to each action (or if FloatingFab
supports a dedicated ariaLabel prop, pass that instead and leave iconAlt as the
visual alt text).


{/* 버튼 */}
<div className="w-[100px] t:w-[132px] mt-auto">
<div className="w-[100px] w-[132px] mt-auto">
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Conflicting width classes — w-[100px] was not removed when updated to w-[132px].

Both classes are present on the button container, leaving the effective width implementation-defined (last one wins in the generated CSS, but this is fragile).

🐛 Proposed fix
- <div className="w-[100px] w-[132px] mt-auto">
+ <div className="w-[132px] mt-auto">
📝 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="w-[100px] w-[132px] mt-auto">
<div className="w-[132px] mt-auto">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx`
at line 171, In the SearchClubListItem component, the button/container div
currently has conflicting Tailwind width classes "w-[100px] w-[132px]"; remove
the unwanted class so only the intended width remains (e.g., keep "w-[132px]"
and delete "w-[100px]") in the div with className used for the button container
to avoid fragile, implementation-defined styling.

Comment on lines +41 to +52
<div className="flex items-center gap-3 hover:brightness-95 cursor-pointer">
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
<p className="text-Gray-7 body_1_2 truncate min-w-0">
{item.name}
</p>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

cursor-pointer / hover styling on non-interactive <div> — no onClick handler attached

Both the mobile (line 41) and tablet+ (line 86) profile/name blocks carry hover:brightness-95 cursor-pointer, strongly implying they are clickable, but neither has an onClick (or onKeyDown/role) attached. This creates a false interactive affordance, and can confuse both sighted and assistive-technology users.

Either wire up the intended handler (e.g., navigate to the user's profile), or remove the hover/cursor classes if the area is purely decorative.

🛠️ Proposed fix (add handler or remove affordance)
-<div className="flex items-center gap-3 hover:brightness-95 cursor-pointer">
+<div
+  className="flex items-center gap-3 hover:brightness-95 cursor-pointer"
+  role="button"
+  tabIndex={0}
+  onClick={() => { /* navigate to profile */ }}
+  onKeyDown={(e) => { if (e.key === 'Enter') { /* navigate to profile */ } }}
+>

Or, if no click action is intended:

-<div className="flex items-center gap-3 hover:brightness-95 cursor-pointer">
+<div className="flex items-center gap-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
<div className="flex items-center gap-3 hover:brightness-95 cursor-pointer">
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
<p className="text-Gray-7 body_1_2 truncate min-w-0">
{item.name}
</p>
</div>
<div
className="flex items-center gap-3 hover:brightness-95 cursor-pointer"
role="button"
tabIndex={0}
onClick={() => { /* navigate to profile */ }}
onKeyDown={(e) => { if (e.key === 'Enter') { /* navigate to profile */ } }}
>
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
<p className="text-Gray-7 body_1_2 truncate min-w-0">
{item.name}
</p>
</div>
Suggested change
<div className="flex items-center gap-3 hover:brightness-95 cursor-pointer">
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
<p className="text-Gray-7 body_1_2 truncate min-w-0">
{item.name}
</p>
</div>
<div className="flex items-center gap-3">
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
<p className="text-Gray-7 body_1_2 truncate min-w-0">
{item.name}
</p>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group/DebateList.tsx` around lines 41 - 52, The
profile/name wrapper div in the DebateList component (the element rendering
profileSrc and item.name) currently uses hover:brightness-95 and cursor-pointer
but has no click or keyboard handlers; either add the intended click behavior
(attach an onClick that navigates to the profile, and add accessible affordances
such as role="button", tabIndex={0}, and an onKeyDown handling Enter/Space) to
the same wrapper, or remove the interactive classes (hover:brightness-95 and
cursor-pointer) if it’s purely decorative so it no longer falsely appears
clickable.

Comment on lines +42 to +48
<Image
src={profileSrc}
alt=""
width={28}
height={28}
className="rounded-full object-cover w-[24px] h-[24px]"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

next/image intrinsic width/height props don't match CSS-rendered sizes

  • Mobile block (lines 42–48): props are width={28} height={28} but CSS locks the display to w-[24px] h-[24px]. Minor — Next.js will serve a slightly larger image than needed, which is harmless but wasteful.
  • Tablet+ block (lines 87–93): props are also width={28} height={28}, but at the desktop breakpoint the CSS expands the element to d:w-[40px] d:h-[40px]. Because Next.js uses the width/height props to generate the optimized srcset, the image will be upscaled from 28 px → 40 px and may appear blurry on desktop screens.

Align the props with the largest rendered size (40 px for the tablet+ image), or use sizes to give the optimizer the correct hint.

🛠️ Proposed fix
 // Mobile block
 <Image
   src={profileSrc}
   alt=""
-  width={28}
-  height={28}
+  width={24}
+  height={24}
   className="rounded-full object-cover w-[24px] h-[24px]"
 />

 // Tablet+ block
 <Image
   src={profileSrc}
   alt=""
-  width={28}
-  height={28}
+  width={40}
+  height={40}
   className="rounded-full object-cover w-[28px] h-[28px] d:w-[40px] d:h-[40px]"
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group/DebateList.tsx` around lines 42 - 48, The
next/image usages in DebateList (the Image rendering profileSrc) pass width={28}
height={28} while CSS renders 24px on mobile and 40px at the desktop breakpoint,
causing inefficient/upsampled images; update the Image props to match the
largest rendered size (use width={40} height={40}) and add a sizes prop that
mirrors your CSS breakpoints (e.g. "(min-width: <tablet-breakpoint>) 40px,
24px") so the optimizer serves correct srcset for both mobile and desktop;
locate the Image instances in DebateList and modify the width/height and add
sizes accordingly.

type="button"
onClick={handleSend}
className="shrink-0 ml-5 w-6 h-6 relative"
className="shrink-0 ml-5 w-6 h-6 relative hover:brightness-70 cursor-pointer"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for tailwindcss version in package.json and package-lock.json
echo "=== Checking package.json ===" 
cat package.json | jq '.dependencies.tailwindcss, .devDependencies.tailwindcss' 2>/dev/null || rg '"tailwindcss"' -A 1 package.json

echo -e "\n=== Checking package-lock.json ===" 
if [ -f package-lock.json ]; then
  cat package-lock.json | jq '.dependencies.tailwindcss.version' 2>/dev/null || rg '"tailwindcss"' -B 2 -A 2 package-lock.json | head -20
else
  echo "package-lock.json not found"
fi

echo -e "\n=== Checking for tailwind.config.js ===" 
if [ -f tailwind.config.js ]; then
  cat tailwind.config.js | head -30
elif [ -f tailwind.config.ts ]; then
  cat tailwind.config.ts | head -30
else
  echo "No tailwind config found"
fi

Repository: checkmo2025/FE

Length of output: 404


🌐 Web query:

Tailwind CSS v4 brightness scale arbitrary numeric values documentation

💡 Result:

In Tailwind CSS v4, the brightness utilities are documented under filter: brightness():

  • Scale classes: brightness-<number> compile to filter: brightness(<number>%); (for example, brightness-50, brightness-100, brightness-200). [1]
  • Arbitrary numeric values: use brightness-[<value>] to set any valid CSS brightness() argument, e.g.:
    • brightness-[1.75]filter: brightness(1.75); [1]
    • You can also use percentages like brightness-[40%] (CSS brightness() accepts <number> or <percentage>). [2]
  • CSS variable shorthand: brightness-(--my-brightness)filter: brightness(var(--my-brightness)); [1]

Sources: Tailwind docs (brightness) [1], MDN brightness() reference [2].


🏁 Script executed:

sed -n '70,82p' src/components/base-ui/LongtermInput.tsx

Repository: checkmo2025/FE

Length of output: 390


Fix invalid Tailwind v4 brightness class syntax.

hover:brightness-70 is not a valid Tailwind v4 brightness value. Predefined scale values are: 50, 100, 200, etc. Use hover:brightness-[0.70] instead for arbitrary numeric values.

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

In `@src/components/base-ui/LongtermInput.tsx` at line 76, Replace the invalid
Tailwind v4 class in the LongtermInput component's className string—change the
current `hover:brightness-70` token to the arbitrary value syntax
`hover:brightness-[0.70]` so the element using className="shrink-0 ml-5 w-6 h-6
relative hover:brightness-70 cursor-pointer" becomes valid Tailwind; update the
className in the LongtermInput component accordingly.

Comment on lines 103 to 106
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="문의하기"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FloatingFab is non-functional — onClick is missing, and iconAlt is semantically wrong.

  1. No onClick prop → clicking the FAB does nothing (same issue as in meeting/page.tsx).
  2. iconAlt="문의하기" ("inquiry") is passed as aria-label by Float.tsx, but a pencil icon in a profile page strongly implies editing/profile-edit, not inquiry.
💡 Proposed fix
-        <FloatingFab
-                iconSrc="/icons_pencil.svg"
-                iconAlt="문의하기"
-              />
+        <FloatingFab
+          iconSrc="/icons_pencil.svg"
+          iconAlt="프로필 편집"
+          onClick={() => { /* router.push('/mypage/edit') */ }}
+        />
📝 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
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="문의하기"
/>
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="프로필 편집"
onClick={() => { /* router.push('/mypage/edit') */ }}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/MyPage/UserProfile.tsx` around lines 103 - 106,
FloatingFab in UserProfile.tsx is missing an onClick handler and uses the wrong
semantic label; add an onClick prop that invokes the profile-edit action (e.g.,
call an existing openEditProfile handler or router push to the profile edit
route) so the FAB is functional, and change iconAlt from "문의하기" to a clear edit
label like "프로필 수정" (or "Edit profile") so Float.tsx will expose the correct
aria-label; ensure the same pattern is applied as in meeting/page.tsx where FABs
are interactive.

Copy link

@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, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
src/components/base-ui/Bookcase/bookid/TeamSection.tsx (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Remove the console.log debug artifact.

console.log is being passed directly as the onMoreClick handler in production code. This also matches your own PR checklist item ("불필요한 콘솔 로그/주석 제거 여부").

🐛 Proposed fix
-            onMoreClick={() => console.log(`${member.name} 더보기 클릭`)}
+            onMoreClick={() => {}}

Or wire up the real handler if the feature is ready.

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

In `@src/components/base-ui/Bookcase/bookid/TeamSection.tsx` at line 54, In
TeamSection.tsx remove the console.log debug artifact currently passed to the
onMoreClick prop (onMoreClick={() => console.log(`${member.name} 더보기 클릭`)});
either wire the real handler if implemented (e.g., pass the actual handler
function) or replace it with a proper no-op callback or omit the prop entirely
so production code contains no console logging; ensure references to member.name
remain unchanged if you keep a handler.
src/components/base-ui/Bookcase/MeetingInfo.tsx (1)

38-46: ⚠️ Potential issue | 🟡 Minor

Move cursor-pointer from the inner <span> to the <button> itself.

cursor-pointer on the <span> only applies when hovering over the text label. Hovering over the Setting.svg icon — which is also part of the same clickable button — will use the browser's default cursor instead. The class belongs on the <button> to cover the full interactive hit area.

🛠️ Proposed fix
  <button
    onClick={onManageGroupClick}
-   className="flex items-center gap-[8px] px-[8px] hover:bg-black/5 rounded transition-colors"
+   className="flex items-center gap-[8px] px-[8px] hover:bg-black/5 rounded transition-colors cursor-pointer"
    >
-   <span className="text-Gray-4 body_1_2 cursor-pointer">조 관리하기</span>
+   <span className="text-Gray-4 body_1_2">조 관리하기</span>
    <div className="relative h-[24px] w-[24px]">
      <Image src="/Setting.svg" alt="설정" fill />
    </div>
  </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/MeetingInfo.tsx` around lines 38 - 46, In the
MeetingInfo component, move the "cursor-pointer" utility class from the inner
<span> to the outer <button> so the entire button (including the Setting.svg
icon) shows the pointer cursor; update the button's className (the element with
onManageGroupClick) to include "cursor-pointer" and remove "cursor-pointer" from
the span with text-Gray-4 body_1_2.
src/app/groups/[id]/bookcase/[bookId]/ReviewSection.tsx (1)

96-105: ⚠️ Potential issue | 🟠 Major

cursor-pointer + hover:brightness-95 on a non-interactive <div>

The profile/name container at line 96 now sports cursor-pointer and a hover-brightness effect, both of which signal clickability to the user — but neither this <div> nor any of its children has an onClick handler, and the Props type exposes no corresponding callback (e.g. onClickProfile). The false affordance will confuse users who click expecting navigation to a profile page.

Either wire up the intended interaction or remove the cursor/hover styles until the feature is ready.

🛠 Option A — remove the false affordance for now
-            <div className="flex shrink-0 items-center gap-3 t:min-w-[128px] d:min-w-[178px] hover:brightness-95 cursor-pointer">
+            <div className="flex shrink-0 items-center gap-3 t:min-w-[128px] d:min-w-[178px]">
🛠 Option B — add the missing prop and handler
 type Props = {
   myName: string;
   myProfileImageUrl?: string | null;
   defaultProfileUrl?: string;
+  onClickProfile?: () => void;
   ...
 };

 export default function ReviewSection({
   ...
+  onClickProfile,
 }: Props) {
   ...
-            <div className="flex shrink-0 items-center gap-3 t:min-w-[128px] d:min-w-[178px] hover:brightness-95 cursor-pointer">
+            <div
+              className="flex shrink-0 items-center gap-3 t:min-w-[128px] d:min-w-[178px] hover:brightness-95 cursor-pointer"
+              onClick={onClickProfile}
+              role={onClickProfile ? 'button' : undefined}
+              tabIndex={onClickProfile ? 0 : undefined}
+            >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/ReviewSection.tsx around lines 96 -
105, The profile/name container in ReviewSection.tsx (the div rendering Image
with profileSrc and the paragraph showing myName) exposes a false affordance via
cursor-pointer and hover:brightness-95 but no onClick or prop; fix by either
removing those classes from that div (and from any props type that suggests
interactivity) OR add a new prop (e.g., onClickProfile: () => void) to the Props
type and wire it to the div as an onClick handler and appropriate accessible
attributes (role="button" and tabIndex={0}) plus keyboard handling; choose one
option and update ReviewSection.tsx (references: Props, profileSrc, myName)
accordingly.
src/lib/api/client.ts (2)

55-59: ⚠️ Potential issue | 🔴 Critical

401 handler falls through — every 401 still throws an ApiError to the caller

After logout() and toast.error(...), there is no early exit. Execution continues to the JSON-parsing block and then hits !response.ok (line 74), which always throws an ApiError. The net effect is:

  1. Session-expiry toast fires ✅
  2. logout() fires ✅
  3. ApiError is still thrown to the caller ❌ — any caller with its own error-toast handler shows a second error message to the user.

There is also no redirect to the login page, which is the standard UX after a forced logout.

🐛 Proposed fix
  if (response.status === 401) {
    console.warn("Session expired. Logging out...");
    useAuthStore.getState().logout();
    toast.error("세션이 만료되었습니다. 다시 로그인해주세요.");
+   window.location.href = "/login";
+   throw new ApiError("세션이 만료되었습니다.", "HTTP401", null);
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/client.ts` around lines 55 - 59, The 401 branch currently calls
useAuthStore.getState().logout() and toast.error(...) but falls through so the
later JSON-parsing and the !response.ok path still throws an ApiError; modify
the handler in the response.status === 401 block (the check around
response.status, useAuthStore.getState().logout(), and toast.error) to perform
an immediate exit—return early after handling (and optionally trigger a redirect
to the login route or window.location to ensure UX redirects) so the function
does not continue to the JSON parsing or the ApiError throw path.

55-59: ⚠️ Potential issue | 🔴 Critical

401 handler falls through — callers always receive a thrown ApiError after the session-expiry toast

After calling logout() and toast.error(...), there is no return or throw, so execution continues to the JSON-parsing block (lines 62–71) and then to the !response.ok check (line 74), which always throws an ApiError for a 401. The result is:

  1. Session-expiry toast fires ✅
  2. logout() fires ✅
  3. An ApiError is still thrown to the caller ❌ — any caller that has its own catch/error-toast will show a second error to the user.

Additionally, there is no redirect to the login page, which is the typical expectation after a forced logout.

🐛 Proposed fix
  if (response.status === 401) {
    console.warn("Session expired. Logging out...");
    useAuthStore.getState().logout();
    toast.error("세션이 만료되었습니다. 다시 로그인해주세요.");
+   // Redirect to login and stop further processing
+   window.location.href = "/login";
+   throw new ApiError("세션이 만료되었습니다.", "HTTP401", null);
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/client.ts` around lines 55 - 59, The 401 branch currently calls
useAuthStore.getState().logout() and toast.error(...) but then continues to
parse the response and ultimately throws an ApiError; stop that by
short-circuiting after handling the 401: after calling
useAuthStore.getState().logout() and toast.error(...), perform a redirect to the
login page (e.g., navigate to /login) and then return early (or throw a specific
handled error) so the JSON parsing and the subsequent !response.ok ApiError
throw do not run; ensure the change is applied in the response.status === 401
block and that callers can distinguish this handled logout case from other
ApiError failures.
src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx (1)

40-50: ⚠️ Potential issue | 🟡 Minor

hover:brightness-0 will turn the icon completely black on hover.

brightness-0 maps to filter: brightness(0), rendering the element as solid black. If the intent is a subtle darkening effect, use hover:brightness-75 or hover:brightness-50 instead. If making the icon fully black is intentional (e.g., a light-gray icon that should appear bold on hover), disregard this.

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

In `@src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx` around lines 40 -
50, The hover class on the menu button in TeamMemberItem.tsx uses
hover:brightness-0 which makes the icon fully black on hover; update the
button's className to a milder filter such as hover:brightness-75 (or
hover:brightness-50) instead of hover:brightness-0 to achieve a subtle darkening
effect while keeping the existing onMoreClick and Image usage intact.
src/components/base-ui/Group-Search/search_club_apply_modal.tsx (1)

167-178: ⚠️ Potential issue | 🟡 Minor

Submit button lacks visual feedback for the disabled state.

The button is disabled when the reason is empty (line 175), but no disabled styling is applied — it still shows the primary color and hover effects. Users won't know the button is non-interactive.

Proposed fix
               'body_1_2 hover:brightness-90 cursor-pointer',
+              'disabled:opacity-50 disabled:cursor-not-allowed',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_club_apply_modal.tsx` around lines
167 - 178, The submit button (the JSX button that calls onSubmit(club.clubId,
reason)) is disabled when reason is empty but lacks visual/interaction changes;
update the button rendering to apply a distinct disabled style and remove hover
effects when reason.trim().length === 0 (e.g., add conditional classes such as
reduced opacity, cursor-not-allowed, and no hover brightness) and keep the
disabled attribute; ensure the conditional class logic references the existing
symbols (onSubmit, club.clubId, reason) so the UI clearly indicates
non-interactivity when disabled and normal styles/hover resume when enabled.
src/app/groups/[id]/bookcase/[bookId]/DebateSection.tsx (1)

80-91: ⚠️ Potential issue | 🟡 Minor

cursor-pointer on the profile container without a click handler is misleading.

This <div> has cursor-pointer and hover:brightness-95, signaling interactivity to the user, but no onClick handler is attached. Users will see a pointer cursor and hover feedback with no resulting action.

Either add the intended click behavior (e.g., navigate to profile) or remove the interactive styling until the feature is wired up.

Proposed fix (remove premature interactive styling)
-          <div className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px] hover:brightness-95 cursor-pointer">
+          <div className="flex shrink-0 items-center gap-3 t:min-w-[150px] d:min-w-[200px]">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/DebateSection.tsx around lines 80 -
91, The profile container in DebateSection.tsx currently uses interactive styles
(cursor-pointer and hover:brightness-95) but has no click handler; either remove
those classes from the div (the container showing Image and myName) to avoid
misleading affordance, or wire up the intended click behavior by adding an
onClick (e.g., navigate to the user's profile) on that same div so the visual
affordance matches the action provided.
src/components/base-ui/Group-Search/search_mybookclub.tsx (1)

78-83: ⚠️ Potential issue | 🟡 Minor

Group row has interactive affordances but no onClick handler.

cursor-pointer and hover effects signal that the row is clickable, but clicking does nothing. If these rows should navigate to the group detail page, an onClick (or wrapping with <Link>) is needed.

💡 Suggested fix
+import { useRouter } from "next/navigation";
 
+  const router = useRouter();
 
   {displayGroups.map((group) => (
     <div
       key={group.id}
-      className="flex w-full h-[36px] t:h-[52px] py-3 px-4 items-center rounded-lg bg-white  hover:brightness-98 hover:-translate-y-[1px] cursor-pointer"
+      className="flex w-full h-[36px] t:h-[52px] py-3 px-4 items-center rounded-lg bg-white  hover:brightness-98 hover:-translate-y-[1px] cursor-pointer"
+      onClick={() => router.push(`/groups/${group.id}`)}
+      role="button"
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Search/search_mybookclub.tsx` around lines 78 -
83, The row div in search_mybookclub.tsx renders interactive affordances
(cursor-pointer, hover) but has no click behavior; update the div that contains
{group.name} to either wrap it with a navigation Link (e.g., <Link
href={`/groups/${group.id}`}>...) or add an onClick that calls your router
navigation (e.g., useRouter().push(`/groups/${group.id}`)) so clicks navigate to
the group detail page; target the div element that renders group.name and use
group.id (or the appropriate group identifier) for the route, and ensure
accessibility by adding role="button" and tabIndex={0} if you keep the onClick
approach.
🟡 Minor comments (18)
src/components/base-ui/Login/LoginModal.module.css-18-21 (1)

18-21: ⚠️ Potential issue | 🟡 Minor

Rename slideUpslide-up to satisfy the Stylelint keyframes-name-pattern rule.

Stylelint reports a lint error on Line 21 because slideUp is camelCase rather than kebab-case. The same name must be updated in both the animation shorthand (Line 18) and the @keyframes declaration (Line 21). The PR checklist requires pnpm lint to pass with no errors.

🛠️ Proposed fix
-  animation: slideUp 0.3s ease-out;
+  animation: slide-up 0.3s ease-out;
 }

-@keyframes slideUp {
+@keyframes slide-up {
   from {
     transform: translateY(20px);
     opacity: 0;
   }
   to {
     transform: translateY(0);
     opacity: 1;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Login/LoginModal.module.css` around lines 18 - 21,
Rename the keyframe name from "slideUp" to "slide-up" in both places: update the
animation shorthand that references slideUp (the animation property) and the
corresponding `@keyframes` declaration name to "slide-up" so the identifier
matches and satisfies the keyframes-name-pattern rule (ensure both the
animation: value and `@keyframes` identifier are changed).
src/components/base-ui/Group-Create/Chip.tsx-26-26 (1)

26-26: ⚠️ Potential issue | 🟡 Minor

hover:brightness-98 is not a standard Tailwind brightness value and will silently produce no CSS.

Tailwind's default brightness scale includes only: 0, 50, 75, 90, 95, 100, 105, 110, 125, 150, 200. The project's tailwind.config.js does not extend the brightness theme with custom values, so brightness-98 is undefined and generates no CSS output.

Use arbitrary value syntax instead: hover:brightness-[0.98].

Proposed fix
-        "hover:brightness-98 hover:-translate-y-[1px] cursor-pointer",
+        "hover:brightness-[0.98] hover:-translate-y-[1px] cursor-pointer",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group-Create/Chip.tsx` at line 26, In Group-Create's
Chip component replace the non-standard Tailwind class "hover:brightness-98"
with the arbitrary-value syntax "hover:brightness-[0.98]" so Tailwind will
generate the correct CSS; locate the class string in Chip.tsx (the array/JSX
className that currently contains "hover:brightness-98 hover:-translate-y-[1px]
cursor-pointer") and update that token only.
src/lib/api/client.ts-99-102 (2)

99-102: ⚠️ Potential issue | 🟡 Minor

Falsy body check silently drops valid JSON values; any type triggers lint errors

Two issues on these lines:

  1. body ? JSON.stringify(body) : undefined silently skips stringification for any falsy but valid JSON body: null, 0, false, or "". While unusual for REST payloads, null is the most realistic footgun. An explicit undefined check is safer.
  2. ESLint reports @typescript-eslint/no-explicit-any on both lines 99 and 101. unknown is the safer, idiomatic alternative.
♻️ Proposed fix
- post: <T>(url: string, body?: any, options?: RequestOptions) =>
-   request<T>(url, { ...options, method: "POST", body: body ? JSON.stringify(body) : undefined }),
- put: <T>(url: string, body?: any, options?: RequestOptions) =>
-   request<T>(url, { ...options, method: "PUT", body: body ? JSON.stringify(body) : undefined }),
+ post: <T>(url: string, body?: unknown, options?: RequestOptions) =>
+   request<T>(url, { ...options, method: "POST", body: body !== undefined ? JSON.stringify(body) : undefined }),
+ put: <T>(url: string, body?: unknown, options?: RequestOptions) =>
+   request<T>(url, { ...options, method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/client.ts` around lines 99 - 102, The post and put helpers use a
falsy check and an any-typed body which causes valid payloads like
null/0/false/"" to be dropped and triggers ESLint; change the body parameter
type from any to unknown in the post and put signatures and pass body through
JSON.stringify only when body !== undefined (not a truthy check), e.g.
request<T>(url, { ...options, method: "POST", body: body !== undefined ?
JSON.stringify(body) : undefined }), and do the same for the put helper; keep
reference to request and RequestOptions unchanged.

99-102: ⚠️ Potential issue | 🟡 Minor

Falsy-check silently drops valid JSON bodies; any type triggers lint errors

Two issues:

  1. body ? JSON.stringify(body) : undefined treats null, 0, false, and "" as "no body". While uncommon for REST payloads, null in particular is a valid JSON body and would be silently dropped. Prefer an explicit undefined check.

  2. ESLint reports @typescript-eslint/no-explicit-any on both lines 99 and 101. Using unknown (or a generic type parameter) is safer and idiomatic.

♻️ Proposed fix
- post: <T>(url: string, body?: any, options?: RequestOptions) =>
-   request<T>(url, { ...options, method: "POST", body: body ? JSON.stringify(body) : undefined }),
- put: <T>(url: string, body?: any, options?: RequestOptions) =>
-   request<T>(url, { ...options, method: "PUT", body: body ? JSON.stringify(body) : undefined }),
+ post: <T>(url: string, body?: unknown, options?: RequestOptions) =>
+   request<T>(url, { ...options, method: "POST", body: body !== undefined ? JSON.stringify(body) : undefined }),
+ put: <T>(url: string, body?: unknown, options?: RequestOptions) =>
+   request<T>(url, { ...options, method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/client.ts` around lines 99 - 102, The post and put helpers in
src/lib/api/client.ts incorrectly use a falsy check and the any type: change the
body falsy check (currently body ? JSON.stringify(body) : undefined) to an
explicit undefined check so null/0/false/"" are serialized (use body !==
undefined ? JSON.stringify(body) : undefined), and replace the explicit any type
on both post and put signatures with unknown (or a second generic like B =
unknown) so the signatures read e.g. post: <T>(url: string, body?: unknown,
options?: RequestOptions) and similarly for put; keep using request<T>(...) with
the updated body handling to satisfy ESLint and preserve correct JSON behavior.
src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor

cursor-pointer on the profile container without a click handler is misleading.

Same issue as in DebateSection.tsx — this <div> signals interactivity to users but has no onClick. Either wire up a handler (e.g., onProfileClick prop) or remove the interactive styling.

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

In `@src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx` at line 19, The
container div in TeamMemberItem.tsx uses interactive styling ("cursor-pointer"
and hover) but has no click handler, which is misleading; either remove the
interactive classes from the div or add an explicit click prop and handler
(e.g., accept an onProfileClick prop on the TeamMemberItem component and attach
it as the div's onClick), and ensure accessibility by adding role="button" and
tabIndex if you keep it interactive; update the JSX to reference the existing
TeamMemberItem component and its className string accordingly.
src/app/(main)/setting/logout/page.tsx-7-23 (1)

7-23: ⚠️ Potential issue | 🟡 Minor

Logout-on-page-load is a CSRF logout vector and provides no user confirmation.

Navigating to /setting/logout (e.g., via a link in an email, embedded image, or redirect) will silently log the user out. While logout CSRF is low-severity (no data breach), it can be used to harass users.

Consider either:

  1. Adding a confirmation step — render a "Are you sure?" prompt before calling authService.logout().
  2. Using a POST-based action (e.g., a server action or form submission) instead of triggering logout on page mount, which prevents GET-based CSRF.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(main)/setting/logout/page.tsx around lines 7 - 23, The page
currently calls authService.logout() inside LogoutPage on mount (performLogout
in useEffect), enabling logout via GET and creating a CSRF vector; change this
by either (A) replacing the auto-run behavior with an explicit confirmation UI:
render an "Are you sure?" prompt with “Logout” and “Cancel” buttons, remove
performLogout from useEffect, and call authService.logout() and router.push("/")
only from the Logout button handler, or (B) convert the flow to a POST-based
action: remove the automatic useEffect call and replace the page with a form or
server action that submits a POST to your logout endpoint (or a Next.js server
action) which performs the logout server-side and then redirects, ensuring
logout only happens on an explicit POST rather than on page load; reference
LogoutPage, performLogout, authService.logout, useEffect, and router.push when
making the change.
src/components/base-ui/Settings/EditProfile/ProfileImageSection.tsx-44-46 (1)

44-46: ⚠️ Potential issue | 🟡 Minor

English fallback "Loading..." in a Korean-language UI.

Use a Korean fallback string to keep the UX consistent.

✏️ Suggested fix
-              {nickname || "Loading..."}
+              {nickname || "불러오는 중..."}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Settings/EditProfile/ProfileImageSection.tsx` around
lines 44 - 46, The UI uses an English fallback "Loading..." in
ProfileImageSection; replace that with a Korean fallback (e.g., "불러오는 중...")
where the span renders {nickname || "Loading..."} so the fallback reads
{nickname || "불러오는 중..."} to keep language consistency for the Korean UI; update
the fallback in the span that contains nickname in the ProfileImageSection
component.
src/components/base-ui/Settings/EditProfile/CategorySelector.tsx-46-62 (1)

46-62: ⚠️ Potential issue | 🟡 Minor

Non-button interactive element is not keyboard-accessible.

The category chip is a <div> with onClick but no role="button", tabIndex, or onKeyDown handler. Users who navigate by keyboard can't activate it.

♻️ Proposed fix
-            <div
-              key={cat}
-              onClick={() => onToggle?.(cat)}
-              className={`... cursor-pointer ...`}
-            >
+            <button
+              type="button"
+              key={cat}
+              onClick={() => onToggle?.(cat)}
+              className={`... cursor-pointer ...`}
+            >
               <span ...>{cat}</span>
-            </div>
+            </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Settings/EditProfile/CategorySelector.tsx` around
lines 46 - 62, The category chip is a non-button div with an onClick (in
CategorySelector.tsx) so make it keyboard-accessible: add role="button" and
tabIndex={0} to the div, implement an onKeyDown handler that calls onToggle(cat)
when Enter or Space is pressed, and set an appropriate ARIA state such as
aria-pressed={isSelected} (use the existing onToggle, cat and isSelected
identifiers) so keyboard and screen-reader users can activate and understand the
selected state.
src/components/base-ui/Float.tsx-27-49 (1)

27-49: ⚠️ Potential issue | 🟡 Minor

Redundant alt on image inside a button that already has aria-label.

aria-label={iconAlt} gives the <button> its accessible name. The inner <Image alt={iconAlt} /> then exposes the same string again, which some screen readers will announce twice. Decorative images nested inside a labelled interactive element should have an empty alt.

♿ Proposed fix
       <Image
         src={iconSrc}
-        alt={iconAlt}
+        alt=""
         width={24}
         height={24}
         className={["w-[24px] h-[24px] object-contain", iconClassName].join(" ")}
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Float.tsx` around lines 27 - 49, The button in
Float.tsx uses aria-label={iconAlt} for its accessible name but the nested
<Image src={iconSrc} alt={iconAlt} /> repeats that text causing some screen
readers to announce it twice; update the Image usage in the Float component to
use an empty alt (alt="") or otherwise mark the image as decorative (e.g.,
aria-hidden) when the outer button provides the accessible name, ensuring
iconAlt remains only on the button and not duplicated on the <Image>.
src/components/base-ui/Bookcase/bookid/ReviewList.tsx-80-80 (1)

80-80: ⚠️ Potential issue | 🟡 Minor

Replace hover:brightness-80 with hover:brightness-75 (or an arbitrary value) — the utility is not defined in Tailwind's default brightness scale.

Tailwind v3 and v4's default brightness scale includes: 0, 50, 75, 90, 95, 100, 105, 110, 125, 150, 200. Since brightness-80 is absent and there is no custom theme entry, the hover effect will be silently dropped. Use brightness-75 (the nearest standard step) or an arbitrary value like [brightness:0.8].

This also applies to src/components/base-ui/Bookcase/BookDetailNav.tsx.

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

In `@src/components/base-ui/Bookcase/bookid/ReviewList.tsx` at line 80, In
ReviewList.tsx (and similarly in BookDetailNav.tsx) the Tailwind class
"hover:brightness-80" uses a non-existent brightness step; replace it with a
valid utility such as "hover:brightness-75" or an arbitrary value like
"hover:[brightness:0.8]" in the className string (e.g., the element with
className="relative w-6 h-6 shrink-0 justify-self-end self-center
hover:brightness-80 cursor-pointer") so the hover brightness effect is applied.
src/components/base-ui/Group/DebateList.tsx-71-71 (1)

71-71: ⚠️ Potential issue | 🟡 Minor

hover:brightness-70 is not a valid Tailwind utility and requires an arbitrary value syntax.

The default Tailwind brightness scale (0, 50, 75, 90, 95, 100, 105, 110, 125, 150, 200) does not include 70. Since your tailwind.config.js doesn't extend the brightness scale, this utility will produce no CSS output. Use hover:brightness-[.70] instead.

Affects lines 71 and 116 in src/components/base-ui/Group/DebateList.tsx.

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

In `@src/components/base-ui/Group/DebateList.tsx` at line 71, In DebateList.tsx
the Tailwind utility "hover:brightness-70" (seen inside the className on the
clickable icon/button in the DebateList component) is invalid and produces no
CSS; update any occurrences of "hover:brightness-70" to use arbitrary value
syntax like "hover:brightness-[.70]" (apply the same replacement for the other
occurrence around line 116) so the hover brightness is applied correctly.
src/app/groups/create/page.tsx-285-288 (1)

285-288: ⚠️ Potential issue | 🟡 Minor

Use arbitrary value syntax for custom brightness values.

The project uses Tailwind v4 without custom brightness theme extensions. The default scale includes 90, 95, 100, 105, etc., but not 97 or 98. These utilities will not generate any CSS and the hover effects won't apply.

Use the arbitrary value syntax with decimal notation instead:

Required fixes

In src/app/groups/create/page.tsx:

- "hover:brightness-98 hover:-translate-y-[1px] cursor-pointer",
+ "hover:brightness-[0.98] hover:-translate-y-[1px] cursor-pointer",
- hover:brightness-97 hover:-translate-y-[1px] cursor-pointer
+ hover:brightness-[0.97] hover:-translate-y-[1px] cursor-pointer

Also update the same patterns in:

  • src/components/base-ui/Group-Search/search_mybookclub.tsx (brightness-98)
  • src/components/base-ui/Group-Create/Chip.tsx (brightness-98)
  • src/components/base-ui/Bookcase/bookid/TeamFilter.tsx (brightness-97)
  • src/app/groups/[id]/page.tsx (brightness-98)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/create/page.tsx` around lines 285 - 288, Tailwind hover
brightness utilities like "hover:brightness-98" are invalid in our config;
replace them with arbitrary-value syntax using decimal notation (e.g.,
"hover:brightness-[0.98]" or "hover:brightness-[0.97]") wherever you build the
class strings. Specifically update the class array that contains
"hover:brightness-98" in the create group page render (the conditional class
list around profileMode === "default"), and mirror the same change in the
components that use the same pattern: the search_mybookclub component (search
MyBookClub render), the Chip component (Chip), the TeamFilter component
(TeamFilter), and the group detail page component that uses the same hover
brightness class.
src/app/(main)/setting/profile/page.tsx-39-49 (1)

39-49: ⚠️ Potential issue | 🟡 Minor

handleSave only logs to console — profile changes are silently discarded.

The function shows a success toast without persisting anything. Combined with the console.log on line 40, this can mislead users into thinking their changes were saved. The TODO on line 47 acknowledges this, but consider at minimum disabling the save button or showing a "not yet implemented" message until the API is wired up.

Also, the console.log on line 40 and the one on line 87 (console.log("Check nickname duplicate")) should be removed per the PR checklist.

Would you like me to help scaffold the profile update API call?

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

In `@src/app/`(main)/setting/profile/page.tsx around lines 39 - 49, The handleSave
function currently only logs and shows a success toast without persisting
changes; remove the debug console.log calls (both in handleSave and the "Check
nickname duplicate" log) and either disable the Save button or change handleSave
to show a "Not implemented" / error toast until the backend is wired;
alternatively scaffold a real async API call by replacing handleSave with an
async function that calls your profile update service (e.g., updateProfile or
ProfileService.updateProfile) with
{nickname,intro,name,phone,selectedCategories}, handles errors (show toast.error
on failure), and only shows toast.success on a successful response, and ensure
the Save button uses the loading/disabled state while the request is in-flight.
src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx-6-6 (1)

6-6: ⚠️ Potential issue | 🟡 Minor

Remove unused useSignup import — showToast is never referenced in this component.

useProfileSetup internally uses useSignup and calls showToast within its validate() and handleCheckDuplicate() functions. The showToast destructured on line 27 is never used elsewhere in this file.

Proposed fix
-import { useSignup } from "@/contexts/SignupContext";
-  const { showToast } = useSignup();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx` at line 6,
The import useSignup is unused in this component; remove the import and the
unused showToast destructuring so ProfileSetup.tsx only uses useProfileSetup's
API. Specifically, delete the import statement for useSignup and remove any
local destructuring of showToast (the variable referenced on line where
showToast is pulled out) since useProfileSetup already calls showToast inside
validate() and handleCheckDuplicate(); keep all other references to
useProfileSetup unchanged.
src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts-46-58 (1)

46-58: ⚠️ Potential issue | 🟡 Minor

Phone formatting mismatch with schema regex for 10-digit numbers.

The schema on line 12 accepts 010-\d{3,4}-\d{4} (i.e., both 10- and 11-digit Korean numbers), but the formatter always allocates 4 digits to the middle group (value.slice(3, 7)). A user entering a 10-digit number gets 010-1234-567, which fails the regex (last group needs 4 digits).

If you only intend to support 11-digit numbers (modern standard), tighten the regex to 010-\d{4}-\d{4}. Otherwise, adjust the formatter to handle the 10-digit case.

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

In `@src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts` around
lines 46 - 58, The phone formatter in handlePhoneChange currently always formats
the middle group as 4 digits which breaks validation for 10-digit numbers;
either tighten the validation regex to require 11-digit numbers (e.g., change
schema from 010-\d{3,4}-\d{4} to 010-\d{4}-\d{4}) if you only support 11-digit
numbers, or modify handlePhoneChange to format conditionally: when the raw
numeric value length is 10 produce 3-3-4 grouping (e.g., 010-123-4567) and when
length is 11 produce 3-4-4 (e.g., 010-1234-5678) before calling setPhone,
ensuring the formatted string matches the schema.
src/components/base-ui/Join/steps/useEmailVerification.ts-46-47 (1)

46-47: ⚠️ Potential issue | 🟡 Minor

Use unknown instead of any for caught errors.

Same ESLint issue as in useProfileSetup.ts. Apply the unknown + instanceof Error pattern consistently.

Proposed fix
-    } catch (error: any) {
-      showToast(error.message || "인증번호 발송에 실패했습니다.");
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : "인증번호 발송에 실패했습니다.";
+      showToast(message);
     }
-    } catch (error: any) {
-      showToast(error.message || "인증 확인 중 오류가 발생했습니다.");
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : "인증 확인 중 오류가 발생했습니다.";
+      showToast(message);
     }

Also applies to: 65-66

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

In `@src/components/base-ui/Join/steps/useEmailVerification.ts` around lines 46 -
47, In useEmailVerification.ts update the two catch blocks in the
useEmailVerification flow to use catch (error: unknown) instead of any, then
narrow the type with if (error instanceof Error) to pass error.message to
showToast; otherwise call showToast with the default Korean message ("인증번호 발송에
실패했습니다." or the other default used at lines 65-66). This applies to the catch
blocks inside the useEmailVerification function where showToast is invoked.
src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts-73-75 (1)

73-75: ⚠️ Potential issue | 🟡 Minor

Use unknown instead of any for the caught error.

ESLint flags error: any. Use unknown and narrow with an instanceof check or a type guard.

Proposed fix
-    } catch (error: any) {
-      showToast(error.message || "닉네임 확인 중 오류가 발생했습니다.");
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : "닉네임 확인 중 오류가 발생했습니다.";
+      showToast(message);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts` around
lines 73 - 75, Change the catch clause in useProfileSetup to use error: unknown
instead of any and narrow it before using message: in the catch block for the
function/method where showToast is called, replace catch (error: any) with catch
(error: unknown) and then check if (error instanceof Error) to call
showToast(error.message || "닉네임 확인 중 오류가 발생했습니다."); otherwise call
showToast("닉네임 확인 중 오류가 발생했습니다.") so you avoid using any and safely access the
error message.
src/app/(main)/page.tsx-172-187 (1)

172-187: ⚠️ Potential issue | 🟡 Minor

bottom-21 is not a valid Tailwind utility class and will be silently ignored.

Tailwind's default spacing scale jumps from 20 (5rem) to 24 (6rem) — bottom-21 has no effect on mobile. Use an arbitrary value like bottom-[5.25rem] or a standard class like bottom-20.

Also, replace the hardcoded colors (bg-[#7B6154], hover:bg-[#5E4A40]) with design tokens for consistency — the codebase uses bg-primary-2 and bg-primary-3 elsewhere.

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

In `@src/app/`(main)/page.tsx around lines 172 - 187, Replace the invalid Tailwind
class and hardcoded colors on the floating login button: in the JSX block that
checks !isLoggedIn and calls openLoginModal(), change the positioning utility
from bottom-21 to a valid class (e.g., bottom-[5.25rem] or bottom-20) and swap
the inline hex color utilities bg-[`#7B6154`] and hover:bg-[`#5E4A40`] to the
project's design tokens (e.g., bg-primary-2 and hover:bg-primary-3) so the
button uses valid spacing and consistent theme tokens.

Comment on lines 100 to 107
if (response.isSuccess) {
onSuccess?.();
}
} catch (error: any) {
showToast(error.message || "정보 저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure when response.isSuccess is false — user gets no feedback.

If the API returns a non-success response (e.g., validation error), the catch block isn't triggered (no exception thrown), and the if (response.isSuccess) branch is skipped. The user sees the loading state end with no feedback.

Proposed fix
       if (response.isSuccess) {
         onSuccess?.();
+      } else {
+        showToast(response.message || "정보 저장에 실패했습니다.");
       }
🧰 Tools
🪛 ESLint

[error] 103-103: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

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

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts` around
lines 100 - 107, The code in useProfileImage.ts currently only handles
successful API responses (response.isSuccess) and exceptions, leaving
non-success responses silent; update the async handler so that when
response.isSuccess is false it calls showToast with the response error message
(e.g., response.message or response.error) or a default localized message, and
avoid swallowing the outcome; ensure you still call setIsLoading(false) in
finally, and keep onSuccess invoked only when response.isSuccess is true so the
UX shows an error toast on failed API responses instead of no feedback.

Comment on lines +41 to +42
// [Security] Include credentials (cookies) for all requests
credentials: "include",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: checkmo2025/FE

Length of output: 438


🏁 Script executed:

fd -t f -e ts -e js | grep -E "(client|api|csrf|auth)" | head -30

Repository: checkmo2025/FE

Length of output: 223


🏁 Script executed:

cat -n src/lib/api/client.ts

Repository: checkmo2025/FE

Length of output: 4028


🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.js" | xargs grep -l "CSRF\|csrf\|SameSite\|samesite" 2>/dev/null | head -20

Repository: checkmo2025/FE

Length of output: 3823


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.js" \) | xargs grep -l "credentials" 2>/dev/null | head -15

Repository: checkmo2025/FE

Length of output: 2708


🌐 Web query:

CSRF protection with credentials: "include" Fetch API cookie-based authentication best practices

💡 Result:

Best practices for cookie-based auth + fetch(..., { credentials: "include" }) + CSRF protection

1) Only use credentials: "include" when you truly need cross-origin cookies

  • Fetch’s default is credentials: "same-origin"; include sends cookies even on cross-origin requests. Use same-origin unless your API is on a different origin. [1]

2) If you do cross-origin cookies, lock down CORS (or you’ll widen CSRF risk)

  • To send cookies cross-origin you must use Access-Control-Allow-Credentials: true, and you cannot use Access-Control-Allow-Origin: * with credentials; you must allow only specific trusted origins. [6]

3) Don’t rely on cookies alone for CSRF protection—use a real CSRF defense for all state-changing requests

OWASP’s primary recommendation for stateful apps is the synchronizer token pattern (server issues a CSRF token; client must echo it; server validates). [5]
Common practice with Fetch/SPAs: send the token in a custom header (e.g., X-CSRF-Token) on POST/PUT/PATCH/DELETE (an attacker site can trigger cookie sending, but can’t read your page to obtain the token). [5]

If you can’t keep server-side CSRF token state, OWASP recommends a signed, session-bound double-submit cookie (avoid the naive double-submit approach). [7]

4) Add server-side “request source” checks (defense in depth)

  • Verify Origin (preferred) or Referer for state-changing requests and reject mismatches. [6]
  • Use Fetch Metadata headers (especially Sec-Fetch-Site) to reject cross-site unsafe-method requests (with an origin-check fallback for older clients). [5]

5) Set strong cookie attributes (helps, but doesn’t replace CSRF tokens)

  • Use Secure (HTTPS-only) and HttpOnly for session cookies; use SameSite=Lax or Strict where possible. [2]
  • If you must send cookies cross-site, you’ll need SameSite=None; Secure. [2]
  • Be aware many browsers treat missing SameSite as Lax by default (so you should set it explicitly). [3]

Minimal Fetch pattern (typical SPA)

await fetch("https://api.example.com/account/email", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": csrfToken, // synchronizer token (or signed double-submit)
  },
  body: JSON.stringify({ email }),
});

Sources: MDN on Fetch credentials [1], MDN on SameSite/Secure cookie behavior [2], Chrome “SameSite=Lax by default” change [3], OWASP CSRF Prevention Cheat Sheet (tokens, Fetch Metadata, Origin/Referer, CORS+credentials guidance, double-submit recommendations) [5][6][7].


🏁 Script executed:

grep -r "csrf\|CSRF\|X-CSRF" src/ --include="*.ts" --include="*.js" -i

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

cat -n src/services/authService.ts

Repository: checkmo2025/FE

Length of output: 2323


🏁 Script executed:

cat -n src/lib/api/endpoints.ts

Repository: checkmo2025/FE

Length of output: 699


🏁 Script executed:

cat -n src/types/auth.ts

Repository: checkmo2025/FE

Length of output: 1182


Confirm server-side CSRF protection for credentials: "include" cross-origin requests

Switching to credentials: "include" sends cookies on all requests, including cross-origin ones to https://api.checkmo.co.kr/api. This enables cookie-based authentication but also allows attacker-controlled pages to trigger authenticated requests unless the server enforces CSRF protections.

No CSRF token implementation is visible in the frontend. Please confirm that the server protects state-mutating requests via:

  1. SameSite cookie policy (SameSite=Lax or Strict) on the auth cookie, or
  2. CSRF token validation on POST/PUT/DELETE requests (e.g., via X-CSRF-Token header or equivalent)

Without either mitigation, the API is exposed to CSRF attacks.

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

In `@src/lib/api/client.ts` around lines 41 - 42, The client currently sets
credentials: "include" in src/lib/api/client.ts which will send cookies on
cross-origin requests; confirm and implement one of the following fixes: either
(A) ensure the server enforces CSRF protections (SameSite=Lax/Strict on the auth
cookie or CSRF token validation on state-mutating endpoints) and document that,
or (B) change the client behavior to require a CSRF token header by reading your
CSRF token (from a secure cookie or meta tag) and adding it to all
POST/PUT/DELETE requests in the API request wrapper (the function where
credentials: "include" is set), or (C) if the API is same-origin, change
credentials to "same-origin" instead; update the code in the request wrapper
(where credentials: "include" is defined) and add a short comment referencing
the chosen mitigation.

Copy link

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

🧹 Nitpick comments (3)
src/types/auth.ts (1)

22-25: Empty interface — use a type alias instead.

ESLint correctly flags this: an interface declaring no members is equivalent to its supertype. Replace with a type alias.

Proposed fix
-export interface LoginResponse extends ApiResponse<{
-  email: string;
-  accessToken?: string; // Optional if using cookies
-}> { }
+export type LoginResponse = ApiResponse<{
+  email: string;
+  accessToken?: string; // Optional if using cookies
+}>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/auth.ts` around lines 22 - 25, The empty interface LoginResponse
should be replaced with a type alias because it declares no new members; change
the declaration from "export interface LoginResponse extends ApiResponse<...>
{}" to a type alias like "export type LoginResponse = ApiResponse<{ email:
string; accessToken?: string; }>" so the type preserves the same shape while
satisfying ESLint; update the LoginResponse symbol accordingly and remove the
unused interface form.
src/services/authService.ts (1)

49-54: Inline endpoint URL — consider centralizing in endpoints.ts.

All other endpoints use AUTH_ENDPOINTS.* constants from endpoints.ts, but getPresignedUrl constructs its URL inline with a template literal. For consistency and easier maintenance, consider defining this endpoint in endpoints.ts.

Proposed change in endpoints.ts

Add to endpoints.ts:

export const IMAGE_ENDPOINTS = {
  UPLOAD_URL: (type: string) => `${API_BASE_URL}/image/${type}/upload-url`,
};

Then in authService.ts:

-      `${API_BASE_URL}/image/${type}/upload-url`,
+      IMAGE_ENDPOINTS.UPLOAD_URL(type),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/authService.ts` around lines 49 - 54, The getPresignedUrl method
builds its endpoint inline; instead centralize the URL in endpoints.ts by adding
an IMAGE_ENDPOINTS constant (e.g., IMAGE_ENDPOINTS.UPLOAD_URL = (type) =>
`${API_BASE_URL}/image/${type}/upload-url`) and update authService.ts to call
apiClient.post using that constant instead of the template literal; change the
getPresignedUrl implementation to import IMAGE_ENDPOINTS and call
IMAGE_ENDPOINTS.UPLOAD_URL(type) to construct the URL.
src/components/base-ui/Join/steps/useEmailVerification.ts (1)

80-86: Timer re-creates interval every second — consider functional updater.

The effect depends on timeLeft, so it tears down and re-creates the interval on every tick. Using a functional updater with setTimeLeft would allow the effect to run only once per timer start, reducing overhead.

Proposed optimization
   useEffect(() => {
     if (timeLeft === null || timeLeft === 0) return;
     const interval = setInterval(() => {
-      setTimeLeft(timeLeft > 0 ? timeLeft - 1 : 0);
+      setTimeLeft((prev: number | null) => (prev && prev > 0 ? prev - 1 : 0));
     }, 1000);
     return () => clearInterval(interval);
-  }, [timeLeft, setTimeLeft]);
+  }, [timeLeft === null || timeLeft === 0, setTimeLeft]);

Note: This requires setTimeLeft in SignupContext to support functional updaters (standard with useState-based setters).

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

In `@src/components/base-ui/Join/steps/useEmailVerification.ts` around lines 80 -
86, The effect in useEmailVerification.ts recreates the interval every second
because it lists timeLeft in the dependency array; change the effect to use a
functional updater with setTimeLeft inside the interval (e.g., setTimeLeft(prev
=> Math.max(prev - 1, 0))) and remove timeLeft from the dependency array so the
effect only starts once per timer start, keeping setTimeLeft (and any "timer
start" flag) in the deps; ensure SignupContext's setTimeLeft supports functional
updaters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx`:
- Around line 28-31: The signup flow currently catches any error from signup or
login together; change PasswordEntry so authService.signup({ email, password })
is awaited first and errors from signup still trigger the existing generic
error, but then wrap the subsequent authService.login({ email, password }) in
its own try/catch: if login succeeds call onNext(), and if login fails show a
specific toast/message (e.g., "계정이 생성되었습니다. 로그인 페이지에서 로그인해주세요.") that informs
the user the account was created and they should manually log in, rather than
treating it as a signup failure. Update the code around authService.signup,
authService.login, and onNext to reflect the separated error handling and avoid
moving to the next step unless login succeeds.
- Around line 27-37: Handle the case where authService.signup returns a
non-throwing failure and fix the catch typing: after calling
authService.signup({ email, password }) check response.isSuccess and if false
call showToast with a meaningful message (use response.message or a generic
"회원가입 중 오류가 발생했습니다." if no message) so the user gets feedback; change the catch
parameter from error: any to error: unknown and safely extract a message (e.g.,
typeof error === 'object' && error !== null && 'message' in error ? (error as
any).message : defaultMessage) when calling showToast; keep setIsLoading(false)
in finally and ensure onNext() is only called on success.

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts`:
- Around line 116-117: The mapping fallback CATEGORY_MAP[c] || c can pass raw
Korean display names to the API; update the mapping logic where categories is
built (referencing selectedInterests and CATEGORY_MAP) to assert or validate
each mapping: for every c in selectedInterests check CATEGORY_MAP[c] exists and
if not either (a) throw or return a clear error/log before calling the
additional-info endpoint, or (b) filter out/unmap the invalid entry and log a
warning with the unmapped key; ensure the final categories array only contains
validated enum keys that match INTEREST_CATEGORIES.
- Around line 111-114: The current branch in useProfileImage that excludes
imgUrl values containing "default_profile" leaves finalImageUrl as an empty
string; update the logic so that when imgUrl exists and contains
"default_profile" you still assign finalImageUrl to that path (or convert it to
a server-recognized sentinel like "DEFAULT_PROFILE") instead of skipping it.
Locate the imgUrl/finalImageUrl handling in useProfileImage and add an explicit
branch that sets finalImageUrl = imgUrl (or maps it to the sentinel) for default
images, ensuring the server receives a meaningful value. Ensure any downstream
upload or submitProfile call recognizes and handles the sentinel if you choose
to use one.
- Around line 133-134: The catch block in useProfileImage currently uses `error:
any`; change it to `error: unknown` and narrow the type before reading a
`.message` — e.g., in the catch of the async save flow in useProfileImage, test
`error instanceof Error` (or otherwise coerce with `String(error)`) to produce a
safe message, then call `showToast` with that message (falling back to the
existing Korean fallback string if empty).

In `@src/components/base-ui/Join/steps/useEmailVerification.ts`:
- Around line 48-49: Change both catch blocks in useEmailVerification (the
blocks that currently read "catch (error: any)") to "catch (error: unknown)" and
narrow the type before accessing .message: inside each catch, perform a type
guard (e.g., check instanceof Error or typeof error === 'object' && 'message' in
error) or cast after a guard, then call showToast with the extracted message or
the fallback string. Update the two spots around the showToast calls so they no
longer assume error is any but safely obtain the message for display.

In `@src/components/base-ui/Settings/EditProfile/CategorySelector.tsx`:
- Around line 46-62: The chip elements in CategorySelector (the clickable div
using onClick={() => onToggle?.(cat.value)}) are not keyboard-accessible;
replace the outer div with a semantic button (or add role="button",
tabIndex={0}, onKeyDown handling, and aria-pressed reflecting isSelected) so
keyboard and screen reader users can focus and toggle chips; ensure the element
still calls onToggle(cat.value) on activation and preserves the existing
className logic that depends on isSelected and cat.value.

---

Duplicate comments:
In `@src/components/base-ui/Settings/EditProfile/CategorySelector.tsx`:
- Around line 4-64: Previous mismatch between display labels and backend enum
values has been fixed: ensure CategorySelector uses the enum codes (cat.value)
for selection and toggling and uses cat.label only for display; specifically
confirm CATEGORIES array supplies {label, value},
selectedCategories.includes(cat.value) is used to determine isSelected,
onToggle?.(cat.value) emits the enum code, and the rendered text uses cat.label
— no further changes needed beyond keeping these usages.

---

Nitpick comments:
In `@src/components/base-ui/Join/steps/useEmailVerification.ts`:
- Around line 80-86: The effect in useEmailVerification.ts recreates the
interval every second because it lists timeLeft in the dependency array; change
the effect to use a functional updater with setTimeLeft inside the interval
(e.g., setTimeLeft(prev => Math.max(prev - 1, 0))) and remove timeLeft from the
dependency array so the effect only starts once per timer start, keeping
setTimeLeft (and any "timer start" flag) in the deps; ensure SignupContext's
setTimeLeft supports functional updaters.

In `@src/services/authService.ts`:
- Around line 49-54: The getPresignedUrl method builds its endpoint inline;
instead centralize the URL in endpoints.ts by adding an IMAGE_ENDPOINTS constant
(e.g., IMAGE_ENDPOINTS.UPLOAD_URL = (type) =>
`${API_BASE_URL}/image/${type}/upload-url`) and update authService.ts to call
apiClient.post using that constant instead of the template literal; change the
getPresignedUrl implementation to import IMAGE_ENDPOINTS and call
IMAGE_ENDPOINTS.UPLOAD_URL(type) to construct the URL.

In `@src/types/auth.ts`:
- Around line 22-25: The empty interface LoginResponse should be replaced with a
type alias because it declares no new members; change the declaration from
"export interface LoginResponse extends ApiResponse<...> {}" to a type alias
like "export type LoginResponse = ApiResponse<{ email: string; accessToken?:
string; }>" so the type preserves the same shape while satisfying ESLint; update
the LoginResponse symbol accordingly and remove the unused interface form.

Comment on lines 28 to 31
const response = await authService.signup({ email, password });
if (response.isSuccess) {
await authService.login({ email, password });
onNext();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Login failure after successful signup leaves user in a limbo state.

If authService.signup succeeds but authService.login throws, the catch block fires and the user sees a generic error toast. However, their account is already created — they may not realize they can simply go to the login page. Consider separating the error handling so that a login failure after successful signup gives more specific guidance (e.g., "계정이 생성되었습니다. 로그인 페이지에서 로그인해주세요.").

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

In `@src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx` around
lines 28 - 31, The signup flow currently catches any error from signup or login
together; change PasswordEntry so authService.signup({ email, password }) is
awaited first and errors from signup still trigger the existing generic error,
but then wrap the subsequent authService.login({ email, password }) in its own
try/catch: if login succeeds call onNext(), and if login fails show a specific
toast/message (e.g., "계정이 생성되었습니다. 로그인 페이지에서 로그인해주세요.") that informs the user
the account was created and they should manually log in, rather than treating it
as a signup failure. Update the code around authService.signup,
authService.login, and onNext to reflect the separated error handling and avoid
moving to the next step unless login succeeds.

Comment on lines +48 to +49
} catch (error: any) {
showToast(error.message || "인증번호 발송에 실패했습니다.");
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace error: any with error: unknown.

Both catch blocks use error: any, flagged by ESLint. Use unknown and narrow before accessing .message.

Proposed fix
-    } catch (error: any) {
-      showToast(error.message || "인증번호 발송에 실패했습니다.");
+    } catch (error: unknown) {
+      showToast(error instanceof Error ? error.message : "인증번호 발송에 실패했습니다.");
     } finally {
-    } catch (error: any) {
-      showToast(error.message || "인증 확인 중 오류가 발생했습니다.");
+    } catch (error: unknown) {
+      showToast(error instanceof Error ? error.message : "인증 확인 중 오류가 발생했습니다.");
     } finally {

Also applies to: 67-68

🧰 Tools
🪛 ESLint

[error] 48-48: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

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

In `@src/components/base-ui/Join/steps/useEmailVerification.ts` around lines 48 -
49, Change both catch blocks in useEmailVerification (the blocks that currently
read "catch (error: any)") to "catch (error: unknown)" and narrow the type
before accessing .message: inside each catch, perform a type guard (e.g., check
instanceof Error or typeof error === 'object' && 'message' in error) or cast
after a guard, then call showToast with the extracted message or the fallback
string. Update the two spots around the showToast calls so they no longer assume
error is any but safely obtain the message for display.

Comment on lines +46 to +62
<div
key={cat.value}
onClick={() => onToggle?.(cat.value)}
className={`flex items-center justify-center gap-[8px] rounded-[400px] border px-[16px] cursor-pointer transition-colors
h-[36px] w-full md:h-[44px] md:w-[122px]
${isSelected
? "border-primary-1 bg-primary-1"
: "border-Subbrown-3 bg-background hover:bg-white"
}`}
>
<span
className={`body_1_3 whitespace-nowrap ${isSelected ? "text-white" : "text-Gray-5"
}`}
>
{cat.label}
</span>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Chip elements are not keyboard-accessible.

The selectable chips are <div> elements with only onClick. Keyboard users cannot tab to or activate them — missing role="button", tabIndex={0}, and a onKeyDown handler (or use a native <button> instead).

♿ Proposed fix: replace the outer `div` with a semantic `button`
-            <div
-              key={cat.value}
-              onClick={() => onToggle?.(cat.value)}
-              className={`flex items-center justify-center gap-[8px] rounded-[400px] border px-[16px] cursor-pointer transition-colors
-                h-[36px] w-full md:h-[44px] md:w-[122px]
-                ${isSelected
-                  ? "border-primary-1 bg-primary-1"
-                  : "border-Subbrown-3 bg-background hover:bg-white"
-                }`}
-            >
+            <button
+              key={cat.value}
+              type="button"
+              onClick={() => onToggle?.(cat.value)}
+              aria-pressed={isSelected}
+              className={`flex items-center justify-center gap-[8px] rounded-[400px] border px-[16px] cursor-pointer transition-colors
+                h-[36px] w-full md:h-[44px] md:w-[122px]
+                ${isSelected
+                  ? "border-primary-1 bg-primary-1"
+                  : "border-Subbrown-3 bg-background hover:bg-white"
+                }`}
+            >
               <span
                 className={`body_1_3 whitespace-nowrap ${isSelected ? "text-white" : "text-Gray-5"
                   }`}
               >
                 {cat.label}
               </span>
-            </div>
+            </button>

aria-pressed communicates the toggle state to screen readers; native <button> handles focus and keyboard activation (Enter/Space) without extra handlers.

📝 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
key={cat.value}
onClick={() => onToggle?.(cat.value)}
className={`flex items-center justify-center gap-[8px] rounded-[400px] border px-[16px] cursor-pointer transition-colors
h-[36px] w-full md:h-[44px] md:w-[122px]
${isSelected
? "border-primary-1 bg-primary-1"
: "border-Subbrown-3 bg-background hover:bg-white"
}`}
>
<span
className={`body_1_3 whitespace-nowrap ${isSelected ? "text-white" : "text-Gray-5"
}`}
>
{cat.label}
</span>
</div>
<button
key={cat.value}
type="button"
onClick={() => onToggle?.(cat.value)}
aria-pressed={isSelected}
className={`flex items-center justify-center gap-[8px] rounded-[400px] border px-[16px] cursor-pointer transition-colors
h-[36px] w-full md:h-[44px] md:w-[122px]
${isSelected
? "border-primary-1 bg-primary-1"
: "border-Subbrown-3 bg-background hover:bg-white"
}`}
>
<span
className={`body_1_3 whitespace-nowrap ${isSelected ? "text-white" : "text-Gray-5"
}`}
>
{cat.label}
</span>
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Settings/EditProfile/CategorySelector.tsx` around
lines 46 - 62, The chip elements in CategorySelector (the clickable div using
onClick={() => onToggle?.(cat.value)}) are not keyboard-accessible; replace the
outer div with a semantic button (or add role="button", tabIndex={0}, onKeyDown
handling, and aria-pressed reflecting isSelected) so keyboard and screen reader
users can focus and toggle chips; ensure the element still calls
onToggle(cat.value) on activation and preserves the existing className logic
that depends on isSelected and cat.value.

Copy link

@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: 2

🧹 Nitpick comments (2)
src/constants/categories.ts (1)

1-17: Consider as const for stronger literal-type inference.

Without as const, TypeScript infers { label: string; value: string }[], losing the literal types. Adding as const enables exhaustive checks on value downstream.

♻️ Proposed refactor
-export const CATEGORIES = [
+export const CATEGORIES = [
     { label: "소설/시/희곡", value: "FICTION_POETRY_DRAMA" },
     ...
-];
+] as const;

If consumers need a mutable type, you can derive it: type Category = (typeof CATEGORIES)[number].

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

In `@src/constants/categories.ts` around lines 1 - 17, CATEGORIES is currently
inferred as { label: string; value: string }[] losing literal types; update the
CATEGORIES declaration to use "as const" so each entry's label and value are
literal types, and add a derived type alias like Category = (typeof
CATEGORIES)[number] for consumers that need the literal union; ensure any places
expecting mutable arrays map to a mutable type if needed (e.g., Array<Category>
or a mapped copy) while keeping CATEGORIES readonly.
src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts (1)

71-74: Redundant setIsLoading(false) before early returnfinally already handles it.

return inside a try block still triggers the finally block (line 98), so setIsLoading(false) on line 73 is called twice. The explicit call on line 73 is unnecessary.

♻️ Proposed fix
         } else {
           showToast(presignedResponse.message || "이미지 업로드 준비 중 오류가 발생했습니다.");
-          setIsLoading(false);
           return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts` around
lines 71 - 74, Remove the redundant setIsLoading(false) before the early return
in the error branch of the presigned-response handling inside useProfileImage
(the block that calls showToast(presignedResponse.message || "...") and
returns); the finally block already calls setIsLoading(false), so delete the
extra setIsLoading(false) to avoid duplicate state updates while leaving
showToast and the early return intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/base-ui/Join/steps/ProfileImage/ProfileImageUploader.tsx`:
- Line 26: The signup completion hook useSignupComplete (in
useSignupComplete.ts) still falls back to "/default_profile_1.svg" while
ProfileImageUploader uses "/profile.svg"; update the fallback value in
useSignupComplete (where the profile image/default is set, around the submission
payload creation) to use "/profile.svg" so the displayed avatar and the
submitted value match, or add a clear comment if the two must intentionally
differ.

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts`:
- Line 82: The code in useProfileImage.ts maps selectedInterests through
CATEGORY_MAP but selectedInterests already contains enum value strings (passed
from InterestCategorySelector via cat.value), so CATEGORY_MAP lookup is dead;
update the logic that computes categories (the const categories = ... line) to
stop referencing CATEGORY_MAP and use the enum strings directly (or a direct
identity map) so categories contains the actual enum values from
selectedInterests; adjust any downstream consumers of categories if they
expected label keys.

---

Duplicate comments:
In `@src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx`:
- Around line 23-37: handleNext currently awaits authService.signup(...) but
ignores its ApiResponse so a non‑success envelope lets code proceed to
authService.login and onNext; update handleNext to capture the signup result
(e.g., const signupRes = await authService.signup(...)), check
signupRes.isSuccess and if false call showToast with a clear signup error
message and return early (ensuring setIsLoading(false) in finally), only then
call authService.login and check/handle its result separately (distinguish login
errors from signup errors), and keep the existing guards (isMatch/isLoading) and
onNext invocation only after a successful login.

In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts`:
- Around line 56-99: The call to authService.additionalInfo(...) returns an
ApiResponse but its isSuccess isn't checked, so onSuccess() may run on a silent
failure; update the block that awaits authService.additionalInfo in
useProfileImage.ts to capture the response (e.g., const resp = await
authService.additionalInfo(...)), verify resp.isSuccess is true before calling
onSuccess, and if false call showToast(resp.message || "..."), and return early
(so onSuccess is not invoked); reference authService.additionalInfo, onSuccess,
and showToast to locate and fix the logic.

---

Nitpick comments:
In `@src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts`:
- Around line 71-74: Remove the redundant setIsLoading(false) before the early
return in the error branch of the presigned-response handling inside
useProfileImage (the block that calls showToast(presignedResponse.message ||
"...") and returns); the finally block already calls setIsLoading(false), so
delete the extra setIsLoading(false) to avoid duplicate state updates while
leaving showToast and the early return intact.

In `@src/constants/categories.ts`:
- Around line 1-17: CATEGORIES is currently inferred as { label: string; value:
string }[] losing literal types; update the CATEGORIES declaration to use "as
const" so each entry's label and value are literal types, and add a derived type
alias like Category = (typeof CATEGORIES)[number] for consumers that need the
literal union; ensure any places expecting mutable arrays map to a mutable type
if needed (e.g., Array<Category> or a mapped copy) while keeping CATEGORIES
readonly.

@shinwokkang shinwokkang merged commit a4327ca into main Feb 23, 2026
4 checks passed
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