Conversation
[Refactor] 회원 가입 피드백 반영
[Style] UI 재수정
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ✏️ 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 📝 WalkthroughWalkthroughIntroduces a centralized signup context system to manage multi-step signup form state, adds dynamic step-based routing via Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @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의 동적 라우팅과 Highlights
🧠 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
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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 | 🟠 MajorMemory leak: cleanup effect captures a stale
imagePreviewsreference.The empty dependency array
[]means the cleanup function closes over the initial emptyimagePreviews. When the component unmounts, it revokes zero URLs while the actual object URLs remain leaked. Either track URLs in aref, or includeimagePreviewsin 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 | 🟡 MinorButton labeled "임시저장" (temp save) but handler only navigates back.
handleCancel(called by this button) simply callsrouter.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 | 🟠 MajorPassword 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
isMatchcheck on lines 18–19 only verifieslength > 0 && length <= 20. This means:
- Passwords shorter than 6 characters are accepted.
- No upper bound of 12 characters is enforced (allows up to 20).
- 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
maxLengthon both inputs from20to12to 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 | 🟡 MinorRemove
console.logbefore 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.jsLinkwill 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 | 🟡 MinorChange
flex1toflex-1—flex1is not a valid Tailwind class. The standard flexbox utility in Tailwind isflex-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 | 🟡 MinorRemove
console.logstatements before merging to main.The PR checklist explicitly asks to verify removal of unnecessary console logs. Lines 9, 14, and 19 contain debug
console.logcalls that should be removed. Additionally,handleSearchMeetingandhandleCreateMeetingare 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 | 🔴 CriticalBlob URL cleanup on unmount will invalidate the context-held image reference.
Since
profileImagenow lives inSignupContext(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
useRefto the imports:-import { useEffect } from "react"; +import { useEffect, useRef } from "react";Note: Consider moving blob URL lifecycle management to the
SignupContext(e.g., inresetSignup) 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: Mixedmin-h-63/h-63toggle may cause layout jumps.When
selectedOptionis"vote", the container switches tomin-h-63(growable), otherwiseh-63(fixed). On tablet+ the height is alwayst: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 usesflex flex-wrap justify-centerwithout explicit item widths, so the number of columns depends entirely on the intrinsic width ofBookStoryCardLarge. 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 orbasis-*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-tand--breakpoint-dare already defined in the@theme inlineblock at lines 35–36. This second@themeblock 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:autoprefixermay be unnecessary with Tailwind CSS v4.Tailwind CSS v4 uses Lightning CSS internally, which handles vendor prefixes automatically. The
autoprefixerdependency 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:onClosein the dependency array may cause unnecessary effect re-runs.If the parent doesn't wrap
onCloseinuseCallback, this effect will re-register the escape handler and toggleoverflowon every render. Consider using a ref to hold the latestonCloseor 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-0at the large breakpoint, while this breadcrumb retainslg:px-[20px]. This may be intentional given the widermax-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:MyPageBreadcrumbused for the other-user profile page.This component lives under
Profile/OtherUser/and renders "다른 사람 페이지", yet is namedMyPageBreadcrumb— identical to the component inMyPage/MyPageBreadcrumb.tsx. This is confusing and could lead to import errors. Consider renaming toProfileBreadcrumborOtherUserBreadcrumb.Additionally, this component is nearly a 1:1 duplicate of
MyPage/MyPageBreadcrumb.tsx(only the label text differs). A sharedBreadcrumbcomponent accepting alabelprop 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 —BookStoryCardLargealready accepts anonClickprop.The wrapping
<div>adds click handling andcursor-pointer, butBookStoryCardLargealready supports anonClickprop and appliescursor-pointerinternally. PassonClickdirectly 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-sideuseEffectredirect.The current approach renders a blank page (
null) while the client-side redirect fires. In Next.js App Router, you can useredirect()fromnext/navigationin a Server Component (orpermanentRedirect()) 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.tsredirect 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-fiton grid may cause alignment issues with fixed-width cards.The inner grid uses
w-fitwhileBookStoryCardhas fixed pixel widths (w-[161px]/md:w-[336px]). At themin-[540px]:grid-cols-3breakpoint, three 161px cards (~483px + gaps) may not fill the available space cleanly, and thew-fitgrid won't stretch. This is fine if intentional centering via the parent'sitems-centeris 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.
MyBookStoryListalready imports its mock data from@/constants/mocks/mypage. For consistency and to avoid duplicating mock patterns, consider movingMOCK_STORIESto 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:recommendedBooksarray 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_BOOKSinstead ofrecommendedBooksin 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 withJoinButton.tsx.The
hasHeight/hasPx/hasPystring-includes checks are identical to those inJoinButton.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: Passwordpr-[40px]is always appended and may conflict with consumer-provided padding.When
isPasswordTypeis true,pr-[40px]is unconditionally added regardless of what the consumer passes inclassName. If a consumer explicitly setspr-[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:showToastis already available insideuseProfileSetup— avoid a seconduseSignup()call.
useProfileSetupalready callsuseSignup()and destructuresshowToastinternally. Rather than calling the context hook again in the component, expose the needed function throughuseProfileSetup's return value. This keeps the component decoupled fromSignupContextand 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: Buttondisabledcondition diverges fromhandleNextClickvalidation.The button is enabled when all fields are non-empty, but
handleNextClickadditionally requiresisNicknameChecked(viaisValid). 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!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/gis 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 liketailwind-merge/clsxto 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 extractingTERMS_CONTENTto 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:renderMarkdowndoes 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 ifselectedTermIddoesn't match aTERMS_CONTENTkey.
TERMS_CONTENT[selectedTermId]could beundefinedif someone adds a term toTERMS_DATAwithout a correspondingTERMS_CONTENTentry. This would cause a runtime error onterm.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:toggleInterestuses closure values instead of functional updaters — potential stale state.
setSelectedInterestsis called with the closure-capturedselectedInterestsarray 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 totimeLeftin the dependency array.Because
setTimeLeftaccepts a plain value (not a functional updater), the effect must includetimeLeftin 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
useRefand 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: SingleuseStatefor 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
useReducerif 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:showToastcould be returned fromuseProfileImageto avoid a redundantuseSignup()call.
useProfileImagealready callsuseSignup()internally and destructuresshowToastfrom it. Rather than callinguseSignup()a second time here, consider exposingshowToastfrom theuseProfileImagereturn 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.useRefwhileuseState/useEffectare destructured imports on Line 3. Consider importinguseRefalongside 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
isButtonDisabledvariable being hardcoded tofalseis a bit of dead indirection — you could simply passdisabled={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)} |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
🧩 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" -iRepository: 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-hideand handle scrollbar styling via CSS rules inglobals.css - Install and configure
tailwind-scrollbar-hideplugin intailwind.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.
| 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 />, | ||
| }; |
There was a problem hiding this comment.
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.
| if (!currentStep) { | ||
| // Falls back to terms if step is invalid | ||
| return null; |
There was a problem hiding this comment.
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("공지사항 저장:", { |
There was a problem hiding this comment.
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일부터 시행합니다.`, |
There was a problem hiding this comment.
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.
| 본 약관은부터 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.
| <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> |
There was a problem hiding this comment.
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.
| const handleVerify = () => { | ||
| if (isCodeValid) { | ||
| setIsVerified(true); | ||
| setShowToast(true); | ||
| showToast("인증이 완료되었습니다."); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/base-ui/Join/steps/useEmailVerification.ts | head -80Repository: checkmo2025/FE
Length of output: 2266
🏁 Script executed:
rg -n --type=ts --type=tsx "verify|verification" -C 5 -g '!node_modules' -g '!*.test.*' | head -150Repository: checkmo2025/FE
Length of output: 85
🏁 Script executed:
fd -e ts -e tsx . src/components/base-ui/Join --type fRepository: checkmo2025/FE
Length of output: 1204
🏁 Script executed:
cat -n src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsxRepository: checkmo2025/FE
Length of output: 4188
🏁 Script executed:
rg -n "verification|verify" src/contexts --type ts | head -50Repository: checkmo2025/FE
Length of output: 320
🏁 Script executed:
find src -type f \( -name "*[Cc]ontext*" -o -name "*api*" \) | grep -i signupRepository: checkmo2025/FE
Length of output: 88
🏁 Script executed:
cat -n src/contexts/SignupContext.tsxRepository: checkmo2025/FE
Length of output: 5898
🏁 Script executed:
rg -n "api\.|axios\.|fetch" src/contexts/SignupContext.tsx -C 3Repository: 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 -10Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
find src -type f \( -name "*api*" -o -name "*service*" \) | head -20Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
rg -n "startTimer|handleVerify" src/components/base-ui/Join -B 2 -A 5Repository: checkmo2025/FE
Length of output: 4848
🏁 Script executed:
rg -n "fetch\|axios\|api" src --type ts --type tsx -l | head -15Repository: 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 -10Repository: 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 -15Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
ls -la src/lib/ 2>/dev/null || ls -la src/ | head -20Repository: checkmo2025/FE
Length of output: 276
🏁 Script executed:
rg -n "POST\|GET\|send.*email\|verify.*code" src -t js -t ts | head -30Repository: 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 fRepository: checkmo2025/FE
Length of output: 155
🏁 Script executed:
cat -n src/lib/api/* 2>/dev/null | head -100Repository: checkmo2025/FE
Length of output: 3712
🏁 Script executed:
cat -n src/lib/api/endpoints.tsRepository: checkmo2025/FE
Length of output: 231
🏁 Script executed:
rg -n "auth\|signup\|email\|verif" src/lib/api/ -A 2Repository: 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 -50Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
rg -n "startTimer\|인증.*발송\|verification.*send\|send.*verification" src -B 5 -A 10Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
rg -n "handleVerify" src --type ts -B 5 -A 10Repository: checkmo2025/FE
Length of output: 5765
🏁 Script executed:
find src -type f -name "*.ts" -o -name "*.js" | xargs grep -l "verificationCode" | head -10Repository: checkmo2025/FE
Length of output: 115
🏁 Script executed:
cat -n src/lib/api/client.ts | tail -50Repository: checkmo2025/FE
Length of output: 2100
🏁 Script executed:
rg -n "POST\|GET\|put\|delete" src/lib/api/client.tsRepository: 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 -20Repository: 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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| 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); | ||
| }, []); |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
Code Review
이번 PR은 동적 라우팅과 상태 관리를 위한 React Context를 사용하여 회원가입 플로우를 대대적으로 리팩토링한 매우 긍정적인 변화를 담고 있습니다. 이를 통해 코드의 구조와 유지보수성이 크게 향상되었습니다. 컨텍스트 내에 전역 토스트 알림 시스템을 추가한 것도 좋은 구현입니다. 또한, 검색 모달 및 다양한 목록 페이지 등 애플리케이션 전반에 걸쳐 반응형 UI와 사용자 피드백 관련 수많은 개선이 이루어졌습니다. 몇 가지 추가 개선을 위한 사소한 제안을 남겼지만, 전반적으로 훌륭한 변경 사항이라고 생각합니다.
| 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> | ||
| ); | ||
| }); | ||
| }; |
[Style] Style-103 UI 수정
There was a problem hiding this comment.
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 | 🟠 MajorRemove
console.logdebug artifacts before merging.
console.logon 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-pointerandhover:brightness-95on a<div>with noonClickhandler 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-pointerand 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 | 🟡 MinorDuplicate
cursor-pointerin the<Link>className.
cursor-pointerappears 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 | 🟡 MinorProfile/name block has
hover:brightness-95 cursor-pointerbut noonClickhandler.Same deceptive-affordance issue as in
TeamMemberItem.tsx— the div dims and shows a pointer cursor, but there is no interaction. Add anonClick(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-interactivedivcreates a deceptive affordance.The left container (profile image + name) has
hover:brightness-95 cursor-pointerbut noonClickhandler. 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-pointershould be on the<button>element, not the inner<span>.Browsers set
<button>cursor todefaultby default. Placingcursor-pointeronly 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-starton a<span>is a CSS no-op.
align-items(whatitems-startmaps to) only affects flex/grid containers. A<span>is an inline element, so this class produces no layout change. Themin-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-50is significantly more aggressive than the rest of the PR.All other interactive buttons in this PR use
hover:brightness-90orhover:brightness-95for subtle feedback. Thebrightness-50here 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 lowercasebody_1_2on 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 — usealt=""on the image inside anaria-label-ed button.The
<button>hasaria-label={iconAlt}and the inner<Image>also hasalt={iconAlt}. When a button has an explicitaria-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-0renders the arrow fully black on hover — verify this is intentional.
brightness-0appliesfilter: brightness(0), making the SVG completely black regardless of its original color. The same pattern appears on the Contact modal close button ingroups/[id]/page.tsx, so this appears to be a deliberate design choice — just confirm the/Arrow-Right2.svglooks 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 useshover: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 retainshover: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-pointerandhover:brightness-98are applied to<div>elements inside the<button>toggle. The browser cursor is alreadypointerover 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-Whiteis 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, adddisabled: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.jsImageintrinsic size (width=28) is smaller than the CSS desktop size (d:w-[40px])The tablet+ profile
<Image>declareswidth={28} height={28}for Next.js optimization but the Tailwind classd:w-[40px] d:h-[40px]renders it at 40×40 on desktop. Next.js uses thewidth/heightprops 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 asizesprop.♻️ 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.
| import FloatingFab from "@/components/base-ui/Float"; | ||
| import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books"; | ||
| import Image from "next/image"; |
There was a problem hiding this comment.
🧩 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:
- 1: https://cloud.google.com/blog/products/identity-security/responding-to-cve-2025-55182/
- 2: https://cloud.google.com/blog/products/identity-security/responding-to-cve-2025-55182/
- 3: https://cloud.google.com/blog/products/identity-security/responding-to-cve-2025-55182/
- 4: https://cloud.google.com/blog/products/identity-security/responding-to-cve-2025-55182/
- 5: https://nvd.nist.gov/vuln/detail/CVE-2025-55182
- 6: https://cve.circl.lu/vuln/cve-2025-66478
🌐 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:
- 1: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
- 2: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
- 3: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
- 4: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
- 5: https://www.akamai.com/blog/security-research/2026/jan/cve-2026-23864-react-nextjs-denial-of-service
- 6: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
- 7: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
🌐 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:
- 19.2.4 (safe)
- 19.1.5 (safe)
- 19.0.4 (safe) (advisories.gitlab.com)
Affected examples (19.2 line):
- 19.2.0, 19.2.1, 19.2.2, 19.2.3 are affected; upgrade to 19.2.4+. (advisories.gitlab.com)
Citations:
- 1: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components?utm_source=openai
- 2: https://advisories.gitlab.com/pkg/npm/react-server-dom-webpack/CVE-2026-23864/?utm_source=openai
- 3: https://advisories.gitlab.com/pkg/npm/react-server-dom-webpack/CVE-2026-23864/?utm_source=openai
🌐 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.6If 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.0and< 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.5is patched, and16.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.
| <FloatingFab | ||
| iconSrc="/icons_calling.svg" | ||
| iconAlt="문의하기" | ||
| /> |
There was a problem hiding this comment.
"문의하기" 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.
| <FloatingFab | ||
| iconSrc="/icons_calling.svg" | ||
| iconAlt="문의하기" | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| <FloatingFab | ||
| iconSrc="/icons_chat.svg" | ||
| iconAlt="문의하기" | ||
| /> |
There was a problem hiding this comment.
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.
| <FloatingFab | ||
| iconSrc="/icons_pencil.svg" | ||
| iconAlt="문의하기" | ||
| onClick={() => router.push(`/groups/${groupId}/admin/bookcase/new`)} | ||
| /> |
There was a problem hiding this comment.
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.
| <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"> |
There was a problem hiding this comment.
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.
| <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.
| <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> |
There was a problem hiding this comment.
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.
| <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> |
| <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.
| <Image | ||
| src={profileSrc} | ||
| alt="" | ||
| width={28} | ||
| height={28} | ||
| className="rounded-full object-cover w-[24px] h-[24px]" | ||
| /> |
There was a problem hiding this comment.
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 tow-[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 tod:w-[40px] d:h-[40px]. Because Next.js uses thewidth/heightprops 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" |
There was a problem hiding this comment.
🧩 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"
fiRepository: 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 tofilter: brightness(<number>%);(for example,brightness-50,brightness-100,brightness-200). [1] - Arbitrary numeric values: use
brightness-[<value>]to set any valid CSSbrightness()argument, e.g.:brightness-[1.75]→filter: brightness(1.75);[1]- You can also use percentages like
brightness-[40%](CSSbrightness()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.tsxRepository: 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.
| <FloatingFab | ||
| iconSrc="/icons_pencil.svg" | ||
| iconAlt="문의하기" | ||
| /> |
There was a problem hiding this comment.
FloatingFab is non-functional — onClick is missing, and iconAlt is semantically wrong.
- No
onClickprop → clicking the FAB does nothing (same issue as inmeeting/page.tsx). iconAlt="문의하기"("inquiry") is passed asaria-labelbyFloat.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.
| <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.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit
New Features
Improvements