Skip to content

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

Open
shinwokkang wants to merge 30 commits intomainfrom
dev
Open

피드백 내용 수정 최신화#100
shinwokkang wants to merge 30 commits intomainfrom
dev

Conversation

@shinwokkang
Copy link
Contributor

@shinwokkang shinwokkang commented Feb 17, 2026

📌 개요 (Summary)

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

🛠️ 변경 사항 (Changes)

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

📸 스크린샷 (Screenshots)

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

✅ 체크리스트 (Checklist)

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

Summary by CodeRabbit

  • New Features

    • Floating Action Button (FAB) added across pages for quick actions
    • Multi-step signup flow with step navigation and centralized toast system
    • Pagination controls on group notices
  • Improvements

    • Entry animations for modals and smoother transitions (fade/slide)
    • Refined layouts, spacing, and responsive behavior across many pages and cards
    • Updated story card display for improved readability on small screens

@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 19, 2026 2:46pm
checkmo Ready Ready Preview, Comment Feb 19, 2026 2:46pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

 _____________________________________________________________________
< This is 'temporary' in the same way legacy systems are 'temporary'. >
 ---------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

Tip

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

📝 Walkthrough

Walkthrough

Introduces a centralized signup context system to manage multi-step signup form state, adds dynamic step-based routing via [step]/page.tsx, refactors signup step components to use the context instead of local state, and applies layout/styling improvements across multiple UI components and pages.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added baseline-browser-mapping dev dependency (^2.9.19).
Signup Context & State Management
src/contexts/SignupContext.tsx
New context file introducing SignupState, SignupActions, SignupContextType interfaces, SignupProvider component with toast system, and useSignup hook for centralized signup form state management.
Signup Routing & Layout
src/app/(public)/signup/[step]/page.tsx, src/app/(public)/signup/layout.tsx, src/app/(public)/signup/page.tsx
Added dynamic step-routing page, wrapped layout with SignupProvider, and changed root signup page to redirect to /signup/terms on mount.
Join Step Components
src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx, src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsx, src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx, src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx, src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx, src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts, src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts, src/components/base-ui/Join/steps/useEmailVerification.ts
Refactored step components and hooks to consume useSignup context instead of local useState; added context-driven toast integration, validation logic, and state management. TermsAgreement adds markdown-rendered detailed term view with modal-like interaction. ProfileSetup adds nickname duplication-check validation flow.
Join Base Components
src/components/base-ui/Join/JoinButton.tsx, src/components/base-ui/Join/JoinInput.tsx, src/components/base-ui/Join/JoinLayout.tsx
Added hover styles to JoinButton, added description prop to JoinInput, integrated toast display in JoinLayout using SignupContext.
Animations & Global Styles
src/app/globals.css
Added fade-in and slide-down animation keyframes and corresponding utility classes.
Layout Components
src/components/layout/Header.tsx, src/components/layout/SearchModal.tsx, src/components/layout/BookSelectModal.tsx
Updated SearchModal with animation classes and responsive width containers; adjusted Header padding; adjusted BookSelectModal overlay positioning.
Story & Book Components
src/app/(main)/books/[id]/page.tsx, src/app/(main)/stories/page.tsx, src/app/(main)/stories/new/page.tsx, src/components/base-ui/BookStory/bookstory_card.tsx
Replaced BookStoryCard with BookStoryCardLarge in book detail page; added modal-visibility conditional rendering in stories creation page; updated stories page tab layout with overflow controls and flex-wrap grid; adjusted card mobile layout styling.
MyPage Components
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
Responsive layout refinements: replaced fixed widths with max-width constraints, adjusted horizontal padding across breakpoints, updated grid/flex containers, centered content horizontally, refined typography and spacing.
Profile Components
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, src/components/base-ui/Profile/items/MeetingCard.tsx
Similar responsive layout updates: replaced fixed responsive widths with max-width containers, adjusted padding/spacing across breakpoints, removed rotation classes from breadcrumb image, added line-clamp behavior.
Miscellaneous UI Components
src/components/base-ui/Comment/comment_list.tsx, src/components/base-ui/Comment/comment_section.tsx, src/app/(main)/page.tsx, src/app/groups/[id]/admin/notice/new/page.tsx, src/app/groups/[id]/notice/page.tsx, src/components/base-ui/Search/search_bookresult.tsx
Updated comment input styling (min-w-0 instead of fixed width), prepend new comments to array, added pagination UI to notice page, standardized quote styles, adjusted notice creation form layout and state handling, minor whitespace/formatting updates.

Sequence Diagram

sequenceDiagram
    participant User
    participant SignupPage as /signup Page
    participant StepRouter as /signup/[step] Router
    participant SignupProvider
    participant StepComponent as Step Component<br/>(Terms/Email/etc)
    participant SignupContext

    User->>SignupPage: Visits /signup
    SignupPage->>StepRouter: Redirects to /signup/terms (useRouter.push)
    
    StepRouter->>SignupProvider: Rendered with SignupContext
    SignupProvider->>SignupContext: Initializes state & actions
    
    StepRouter->>StepComponent: Renders current step based on URL param
    StepComponent->>SignupContext: useSignup() → reads state
    
    User->>StepComponent: Interacts (fill form, agree, verify)
    StepComponent->>SignupContext: Updates state (setEmail, setAgreements, etc)
    SignupContext->>SignupProvider: Updates context state
    SignupProvider-->>StepComponent: Triggers re-render
    
    User->>StepComponent: Clicks Next/Continue
    StepComponent->>SignupContext: onNext callback checks validation
    StepComponent->>StepRouter: navigateTo(nextStep)
    StepRouter->>StepComponent: Renders next step component
    
    Note over User,SignupContext: Flow repeats for each step
    
    User->>StepComponent: Completes final step (SignupComplete)
    StepComponent->>SignupContext: Reads accumulated state (email, nickname, etc)
    StepComponent->>User: Shows success summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feat 36 login api #37: Introduces the Join/signup step components and hooks that are directly wired into the new SignupContext system and multi-step routing introduced in this PR.

Suggested labels

✨ feat, 📦 build

Poem

🐰 Hops through signup steps with grace and care,
Context whispers state everywhere,
Terms, email, profile in a row—
Watch the form's progression glow!
Refactored flows and layouts too,
A rabbit's touch makes all things new!

🚥 Pre-merge checks | ✅ 1 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (43 files):

⚔️ package.json (content)
⚔️ pnpm-lock.yaml (content)
⚔️ src/app/(main)/books/[id]/page.tsx (content)
⚔️ src/app/(main)/page.tsx (content)
⚔️ src/app/(main)/stories/new/page.tsx (content)
⚔️ src/app/(main)/stories/page.tsx (content)
⚔️ src/app/(public)/signup/layout.tsx (content)
⚔️ src/app/(public)/signup/page.tsx (content)
⚔️ src/app/globals.css (content)
⚔️ src/app/groups/[id]/admin/notice/new/page.tsx (content)
⚔️ src/app/groups/[id]/notice/page.tsx (content)
⚔️ src/components/base-ui/BookStory/bookstory_card.tsx (content)
⚔️ src/components/base-ui/Comment/comment_list.tsx (content)
⚔️ src/components/base-ui/Comment/comment_section.tsx (content)
⚔️ src/components/base-ui/Join/JoinButton.tsx (content)
⚔️ src/components/base-ui/Join/JoinInput.tsx (content)
⚔️ src/components/base-ui/Join/JoinLayout.tsx (content)
⚔️ src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsx (content)
⚔️ src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx (content)
⚔️ src/components/base-ui/Join/steps/ProfileImage/ProfileImage.tsx (content)
⚔️ src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts (content)
⚔️ src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx (content)
⚔️ src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts (content)
⚔️ src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts (content)
⚔️ src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx (content)
⚔️ src/components/base-ui/Join/steps/useEmailVerification.ts (content)
⚔️ src/components/base-ui/MyPage/MyBookStoryList.tsx (content)
⚔️ src/components/base-ui/MyPage/MyMeetingList.tsx (content)
⚔️ src/components/base-ui/MyPage/MyNotificationList.tsx (content)
⚔️ src/components/base-ui/MyPage/MyPageBreadcrumb.tsx (content)
⚔️ src/components/base-ui/MyPage/UserProfile.tsx (content)
⚔️ src/components/base-ui/MyPage/items/MyMeetingCard.tsx (content)
⚔️ src/components/base-ui/MyPage/items/MyNotificationItem.tsx (content)
⚔️ src/components/base-ui/Profile/BookStoryList.tsx (content)
⚔️ src/components/base-ui/Profile/LibraryList.tsx (content)
⚔️ src/components/base-ui/Profile/MeetingList.tsx (content)
⚔️ src/components/base-ui/Profile/OtherUser/ProfileBreadcrumb.tsx (content)
⚔️ src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx (content)
⚔️ src/components/base-ui/Profile/items/MeetingCard.tsx (content)
⚔️ src/components/base-ui/Search/search_bookresult.tsx (content)
⚔️ src/components/layout/BookSelectModal.tsx (content)
⚔️ src/components/layout/Header.tsx (content)
⚔️ src/components/layout/SearchModal.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
Title check ❓ Inconclusive The title '피드백 내용 수정 최신화' (Update feedback content) is vague and does not clearly describe the specific changes in this large, multi-faceted PR covering signup flow, UI/responsiveness, components, and styling. Consider using a more specific title that highlights the main change, such as 'Refactor signup flow with context and UI improvements' or 'Add signup context and responsive layout updates'.
✅ 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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

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 39 to 44
const handleVerify = () => {
if (isCodeValid) {
setIsVerified(true);
setShowToast(true);
showToast("인증이 완료되었습니다.");
}
};
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:

cat -n src/components/base-ui/Join/steps/useEmailVerification.ts | head -80

Repository: checkmo2025/FE

Length of output: 2266


🏁 Script executed:

rg -n --type=ts --type=tsx "verify|verification" -C 5 -g '!node_modules' -g '!*.test.*' | head -150

Repository: checkmo2025/FE

Length of output: 85


🏁 Script executed:

fd -e ts -e tsx . src/components/base-ui/Join --type f

Repository: checkmo2025/FE

Length of output: 1204


🏁 Script executed:

cat -n src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsx

Repository: checkmo2025/FE

Length of output: 4188


🏁 Script executed:

rg -n "verification|verify" src/contexts --type ts | head -50

Repository: checkmo2025/FE

Length of output: 320


🏁 Script executed:

find src -type f \( -name "*[Cc]ontext*" -o -name "*api*" \) | grep -i signup

Repository: checkmo2025/FE

Length of output: 88


🏁 Script executed:

cat -n src/contexts/SignupContext.tsx

Repository: checkmo2025/FE

Length of output: 5898


🏁 Script executed:

rg -n "api\.|axios\.|fetch" src/contexts/SignupContext.tsx -C 3

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

find src -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l "email.*verif|verif.*email" | head -10

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

find src -type f \( -name "*api*" -o -name "*service*" \) | head -20

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

rg -n "startTimer|handleVerify" src/components/base-ui/Join -B 2 -A 5

Repository: checkmo2025/FE

Length of output: 4848


🏁 Script executed:

rg -n "fetch\|axios\|api" src --type ts --type tsx -l | head -15

Repository: checkmo2025/FE

Length of output: 85


🏁 Script executed:

rg -n "email" src -t js -t ts --max-count=50 | grep -i "send\|send.*code\|api\|fetch"

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

fd -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l "email" | head -10

Repository: checkmo2025/FE

Length of output: 653


🏁 Script executed:

find src -type f \( -name "*.ts" -o -name "*.js" \) ! -path "*/node_modules/*" | xargs rg -l "api\|fetch\|axios" | head -15

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

ls -la src/lib/ 2>/dev/null || ls -la src/ | head -20

Repository: checkmo2025/FE

Length of output: 276


🏁 Script executed:

rg -n "POST\|GET\|send.*email\|verify.*code" src -t js -t ts | head -30

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

ls -la src/lib/api/

Repository: checkmo2025/FE

Length of output: 415


🏁 Script executed:

find src/lib/api -type f

Repository: checkmo2025/FE

Length of output: 155


🏁 Script executed:

cat -n src/lib/api/* 2>/dev/null | head -100

Repository: checkmo2025/FE

Length of output: 3712


🏁 Script executed:

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

Repository: checkmo2025/FE

Length of output: 231


🏁 Script executed:

rg -n "auth\|signup\|email\|verif" src/lib/api/ -A 2

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

find src -type f \( -name "*.ts" -o -name "*.js" \) ! -path "*/node_modules/*" -exec grep -l "verif\|email" {} \; | xargs rg -n "verif.*code\|send.*code\|check.*code" -B 2 -A 5 | head -50

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

rg -n "startTimer\|인증.*발송\|verification.*send\|send.*verification" src -B 5 -A 10

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

rg -n "handleVerify" src --type ts -B 5 -A 10

Repository: checkmo2025/FE

Length of output: 5765


🏁 Script executed:

find src -type f -name "*.ts" -o -name "*.js" | xargs grep -l "verificationCode" | head -10

Repository: checkmo2025/FE

Length of output: 115


🏁 Script executed:

cat -n src/lib/api/client.ts | tail -50

Repository: checkmo2025/FE

Length of output: 2100


🏁 Script executed:

rg -n "POST\|GET\|put\|delete" src/lib/api/client.ts

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

find src -type f \( -name "*.ts" -o -name "*.js" \) | xargs grep -n "email\|verif" | grep -i "api\|fetch\|axios\|request" | head -20

Repository: checkmo2025/FE

Length of output: 40


Add server-side verification API call to validate email verification code.

The handleVerify function currently marks verification as complete purely on the client side when the verification code is 6 digits long, without any backend validation. Users can bypass email verification by entering any 6-digit code. Implement an API call to validate the code server-side before setting isVerified = true. Similarly, ensure the "인증 번호 발송" (send verification code) button triggers an API call to actually send the verification code to the user's email address.

🤖 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 39 -
44, The client currently flips isVerified in handleVerify without backend
validation; update handleVerify to call your email-verification API (e.g., POST
/api/verify-email with email and code) and only setIsVerified(true) and
showToast("인증이 완료되었습니다.") when the API returns success, otherwise show an error
toast and keep isVerified false; also implement or hook the "인증 번호 발송" action
(the send-code handler, e.g., handleSendCode or sendVerificationCode) to call
your send-code API (e.g., POST /api/send-verification with email) and handle
loading, success (showToast like "인증 번호가 발송되었습니다."), and error paths
appropriately. Ensure you pass the correct identifiers (email and code) from
state, handle HTTP/network errors, and don't rely on isCodeValid length check as
the only gate for verification in useEmailVerification.ts.

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

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이번 PR은 동적 라우팅과 상태 관리를 위한 React Context를 사용하여 회원가입 플로우를 대대적으로 리팩토링한 매우 긍정적인 변화를 담고 있습니다. 이를 통해 코드의 구조와 유지보수성이 크게 향상되었습니다. 컨텍스트 내에 전역 토스트 알림 시스템을 추가한 것도 좋은 구현입니다. 또한, 검색 모달 및 다양한 목록 페이지 등 애플리케이션 전반에 걸쳐 반응형 UI와 사용자 피드백 관련 수많은 개선이 이루어졌습니다. 몇 가지 추가 개선을 위한 사소한 제안을 남겼지만, 전반적으로 훌륭한 변경 사항이라고 생각합니다.

Comment on lines +199 to +238
const renderMarkdown = (text: string) => {
return text.split("\n").map((line, index) => {
const trimmed = line.trim();
if (trimmed.startsWith("# ")) {
return (
<h1 key={index} className="text-Gray-6 font-sans text-[20px] md:text-[24px] font-bold leading-[135%] tracking-[-0.02px] mb-[16px]">
{trimmed.replace("# ", "")}
</h1>
);
}
if (trimmed.startsWith("## ")) {
return (
<h2 key={index} className="text-Gray-6 font-sans text-[18px] md:text-[20px] font-semibold leading-[135%] tracking-[-0.02px] mb-[12px] mt-[8px]">
{trimmed.replace("## ", "")}
</h2>
);
}
if (trimmed.startsWith("### ")) {
return (
<h3 key={index} className="text-Gray-6 font-sans text-[16px] md:text-[18px] font-semibold leading-[135%] tracking-[-0.018px] mb-[8px] mt-[4px]">
{trimmed.replace("### ", "")}
</h3>
);
}
if (trimmed.startsWith("- ")) {
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">•</span>
<span>{trimmed.replace("- ", "")}</span>
</div>
);
}
if (trimmed === "") return <div key={index} className="h-[1em]" />;
return (
<p key={index} className="text-Gray-6 font-sans text-[14px] md:text-[18px] font-medium leading-[145%] md:leading-[135%] tracking-[-0.018px] mb-[8px]">
{line}
</p>
);
});
};

Choose a reason for hiding this comment

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

medium

직접 구현하신 마크다운 렌더링 로직이 인상적입니다. 다만, 현재 방식은 #이나 -로 시작하는 일반 텍스트가 있을 경우 의도치 않게 제목이나 리스트로 렌더링될 수 있는 잠재적인 위험이 있습니다.

향후 유지보수성과 안정성을 높이기 위해, react-markdown과 같은 가벼운 마크다운 라이브러리를 도입하는 것을 고려해보시는 것은 어떨까요? 라이브러리를 사용하면 더 다양한 마크다운 문법을 지원하면서도 엣지 케이스를 더 안전하게 처리할 수 있습니다.

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.

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

Comments