Skip to content

Conversation

@yura0302
Copy link
Collaborator

@yura0302 yura0302 commented Dec 13, 2025

🔗 관련 이슈

Closes #

✨ 변경사항

🎯 리뷰 포인트

📝 추가 정보

Summary by CodeRabbit

  • New Features

    • Consolidated multi-step review forms for Paper, Interview, and Activity with improved step navigation
    • Bookmark toggle on club cards with visual state and mobile-friendly card variants
    • Mobile drawer-based filtering and compact tab controls
    • Dynamic Q&A component for question/answer entries
  • UI/UX Improvements

    • Updated card layouts, responsive images, and refined typography
    • Reduced header spacing and adjusted global background tone
  • Documentation

    • Added CLAUDE.md with developer guidance
  • Chores

    • Updated build/dev scripts and core tooling versions

✏️ Tip: You can customize this high-level summary in your review settings.

yura0302 and others added 13 commits August 30, 2025 02:41
* feat: 검색 부분 미비한 로직 추가

* fix: 이미지 수정

* Feat/#79 (#96)

* fix: 히어로 이미지 수정

* feat: 프리미엄 후기 구현

* feat: 세부페이지 구현

* fix: 에러해결

---------

Co-authored-by: yura <[email protected]>

* feat: 좋아요 API 명세 추가

---------

Co-authored-by: oaoong <[email protected]>
Co-authored-by: yura <[email protected]>
* style: 디자인토큰, 타이포그래피 수정

* fix: onsuccess 콜백에 mutaiton 인자 추가

---------

Co-authored-by: yura <[email protected]>
* style: 디자인토큰, 타이포그래피 수정

* fix: onsuccess 콜백에 mutaiton 인자 추가

* fix: mutation파일에 onmutationresult 추가

---------

Co-authored-by: yura <[email protected]>
style: 배경 기본 색상 변경
* style: 탐색하기 사이드바, 히어로 이미지 수정

* style: 탐색하기 레이아웃 수정

---------

Co-authored-by: yura <[email protected]>
* feat: 리뷰 작성 연결 페이지 재정의

* feat: 리뷰 UI 개발사항 반영

* fix: formatting

* feat: QnA 컴포넌트 추가
* refactor: card 컴포넌트 수정

* feat: 북마크 추가

* refactor: card 컴포넌트 수정

* feat: 모바일 카드 구현
* feat: drawer 구현

* feat: 탐색 반응형 구현
@vercel
Copy link

vercel bot commented Dec 13, 2025

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

Project Deployment Review Updated (UTC)
moyeoit Ready Ready Preview, Comment Dec 13, 2025 6:35am

@yura0302 yura0302 changed the title 머지 🚚 Merge Dec 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Consolidates and restructures the review creation UX (routes, forms, hooks, and shared form UI), refactors Card components into composable parts with mobile variants, adds responsive mobile filter/drawer primitives, updates styles/tokens and constants, bumps dependencies, updates MSW worker payloads, and removes environment placeholders and several legacy form files.

Changes

Cohort / File(s) Summary
Environment & Docs
\.example\.env, CLAUDE\.md
Removed GA/Chromatic/MSW placeholders from .example.env; added CLAUDE.md documentation.
Package & MSW
package\.json, public/mockServiceWorker\.js
Updated Next/React versions, added vaul, import-in-the-middle, require-in-the-middle; MSW constants bumped and requestInterceptedAt threaded through handlers (function signatures updated).
Route & Page Restructure
src/app/(root)/(routes)/review/new/page\.tsx, src/app/(root)/(routes)/review/new/[kind]/page\.tsx, src/app/(root)/(routes)/review/new/[kind]/[type]/page\.tsx
Added landing page with type selection, replaced kind/type page with client-side kind page; removed nested [kind]/[type] page.
Form Framework & API
src/components/pages/review/new/forms/**, src/components/pages/review/new/forms/hooks/**, src/components/pages/review/new/shared/**
Removed six legacy form components/hooks; introduced three consolidated forms (PaperForm, InterviewForm, ActivityForm) with multi-step structure, new hooks (usePaperForm, useInterviewForm, useActivityForm), shared ReviewFormHeader and StepNavigation, DynamicQASection, and per-kind step components.
Form Factory / AppPath
src/components/pages/review/new/forms/FormFactory\.tsx, src/shared/configs/appPath\.ts
Simplified FormFactory to dispatch by kind only (removed type variant); updated AppPath.reviewNew to drop type parameter.
Card System
src/components/molecules/card/Card\.tsx, src/components/molecules/card/index\.ts, src/components/molecules/card/*, src/components/molecules/card/presets\.ts, src/components/molecules/card/Card\.stories\.tsx
Consolidated card logic into a compound Card with new subcomponents (Image, Meta, Stats, Title, Bookmark, etc.), removed separate slot files, added CardOverlay and MobileCard, adjusted presets and story orientation.
Mobile Filters & Tabs
src/components/molecules/filterBar/MobileFilterBar\.tsx, src/components/molecules/multiDropDown/MobileMultiDropdown\.tsx, src/components/molecules/tab/MobileTab\.tsx, src/components/molecules/tab/TabOverlay\.tsx, src/components/molecules/tab/Tab\.tsx
Added mobile drawer-based multi-dropdown, mobile tab, TabOverlay, MobileFilterBar; updated Tab options (replaced recruit with deadline).
Drawer Primitive
src/components/molecules/drawer/drawer\.tsx, src/components/molecules/drawer/index\.ts
Added Vaul-based Drawer wrappers and composable subcomponents (Portal, Overlay, Content, Title, Description, Header/Footer, Trigger/Close).
Explore Page & Subscriptions
src/components/(pages)/club/explore/Explore\.tsx, src/features/subscribe/mutations\.ts, src/features/clubs/mutations\.ts, src/features/like/mutations\.ts
Reworked Explore to use CardOverlay, MobileFilterBar, TabOverlay; integrated useToggleClubSubscription and useUserSubscribes; threaded onMutateResult into mutation callbacks.
UI Atoms & Small Style Tweaks
src/components/atoms/Button/button\.tsx, src/components/atoms/Textarea/Textarea\.tsx, src/components/atoms/UnderLineTab/UnderLineTab\.tsx, src/components/atoms/sideBar/Sidebar\.tsx
Added asChild prop to Button (Radix Slot support); removed disabled:bg class in Textarea; adjusted divider height and sidebar checked styling.
Dynamic QA & Stories
src/components/molecules/dynamicQaSection/DynamicQASection\.tsx, .../DynamicQASection\.stories\.tsx, .../index\.ts
Added DynamicQASection component with react-hook-form field array support and Storybook stories.
Shared Constants & Icons
src/shared/constants/category\.ts, src/shared/constants/filters\.ts, src/assets/icons/index\.ts
Added CATEGORY_OPTIONS, HERO_IMAGES, SORT_OPTIONS, PART_OPTIONS, WAY_OPTIONS, TARGET_OPTIONS; exported five new icons (DocumentFile, Bookmark variants).
Styling / Tokens
src/styles/design-tokens\.css, src/styles/globals\.css, src/styles/typography\.css
Added caption/review typography tokens, changed body background to white, and introduced a larger set of typography utility classes.
Removed Legacy Form Files
src/components/pages/review/new/forms/*NormalForm\.tsx, *PremiumForm\.tsx, various per-kind old components
Deleted many legacy form components and their hook re-exports in favor of the new consolidated structure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Heterogeneous, large refactor spanning UI, routing, form validation, API transformation, and component API surface.
  • Areas needing extra attention:
    • New form hooks: schema validation and transformToApiRequest mappings (useActivityForm / useInterviewForm / usePaperForm).
    • Card compound API and CSS-variable sizing/ratio behavior across Card and MobileCard/CardOverlay.
    • MSW worker signature change (handleRequest/getResponse) and payload timestamp threading.
    • AppPath.reviewNew consumers and removed [type] usages.
    • Mutation callback signature changes that thread onMutateResult.

Possibly related PRs

Suggested labels

✨ Feature, 🎨 Style

Suggested reviewers

  • oaoong

Poem

🐰 I hopped through forms both old and new,

Trimmed the paths, and stitched a view,
Cards now pocketed, drawers swing wide,
Mobile filters by my side,
A little rabbit cheers — refactor true!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is entirely composed of unfilled template placeholders with no actual content about changes, review points, or related issues. Complete all required sections: provide related issue number, summarize key changes (form refactoring, component reorganization, dependency updates), specify review focus areas, and add relevant information such as migration notes or testing guidance.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.23% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title '🚚 Merge' is vague and generic, providing no meaningful information about the changeset despite extensive file modifications. Replace with a descriptive title that summarizes the main change, such as 'Refactor review form to multi-step architecture' or 'Restructure club explore page with new card components'.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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

❤️ Share

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

@gemini-code-assist
Copy link

Summary of Changes

Hello @yura0302, 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은 리뷰 제출 시스템을 전면적으로 개편하여, 기존의 복잡한 유형 구분을 없애고 '서류', '면접', '활동'이라는 명확한 카테고리 기반의 다단계 폼을 도입했습니다. 동시에 동아리 탐색 페이지의 모바일 사용자 경험을 혁신적으로 개선하기 위해 반응형 필터 바와 카드 디스플레이를 구현했습니다. 또한, 핵심 의존성 업데이트, 새로운 동적 Q&A 컴포넌트 추가, 그리고 디자인 시스템 전반에 걸친 스타일링 및 타이포그래피 개선을 통해 프로젝트의 안정성과 사용자 편의성을 크게 향상시켰습니다.

Highlights

  • 리뷰 폼 시스템 전면 개편: 기존의 '일반' 및 '프리미엄' 리뷰 유형을 통합하고, '서류', '면접', '활동' 카테고리별로 다단계 폼을 도입하여 리뷰 작성 과정을 간소화하고 사용자 경험을 개선했습니다. 이제 /review/new 페이지에서 리뷰 종류를 선택할 수 있습니다.
  • 모바일 UI/UX 대폭 개선: 동아리 탐색 페이지(club/explore)에 모바일 전용 필터 바와 카드 레이아웃을 적용하여 반응형 디자인을 강화했습니다. DrawerMobileCard와 같은 새로운 컴포넌트를 활용하여 모바일 환경에서의 사용성을 향상시켰습니다.
  • 동적 Q&A 컴포넌트 도입: 새로운 리뷰 폼에서 사용자가 질문과 답변 필드를 동적으로 추가할 수 있는 DynamicQASection 컴포넌트를 개발했습니다. 이는 상세한 피드백을 유연하게 수집할 수 있도록 돕습니다.
  • 핵심 의존성 업데이트 및 MSW 개선: Next.js, React 등 주요 라이브러리를 최신 버전으로 업데이트하고, Mock Service Worker(MSW)에 요청 가로채기 시간 추적 기능을 추가하여 디버깅 및 성능 모니터링을 강화했습니다.
  • 디자인 시스템 및 스타일링 정교화: 북마크 및 문서 유형을 위한 새로운 SVG 아이콘을 추가하고, 타이포그래피 유틸리티 클래스와 디자인 토큰을 대폭 리팩토링하여 디자인 시스템의 일관성과 유연성을 높였습니다.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

Copy link

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

Choose a reason for hiding this comment

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

Code Review

이 PR은 새로운 기능 추가와 대규모 리팩터링을 포함하고 있습니다. 특히 후기 작성 플로우가 단계별 فرم으로 개선되었고, 클럽 북마크 기능, 모바일 필터링 UI, 동적 Q&A 섹션 등 새로운 기능들이 인상적입니다. 또한 카드 레이아웃, 타이포그래피 시스템, 전역 스타일 등 UI/UX가 크게 향상되었습니다. 코드베이스 역시 상수 중앙화, 재사용 가능한 컴포넌트 추출 등 많은 부분이 개선되었습니다.

다만, 새로 구현된 Card 컴포넌트에서 스타일링 관련 버그를 발견했으며, 동아리 탐색 페이지에서 데이터 로딩 및 에러 상태 처리가 누락된 점을 확인했습니다. 자세한 내용은 아래 주석을 참고해주세요.

className={cn(
base,
'gap-[var(--card-gap)] p-[var(--card-pad)] rounded-[12px]',
'gap-(--card-gap) p-(--card-pad) rounded-[12px]',

Choose a reason for hiding this comment

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

critical

Tailwind CSS에서 CSS 변수를 사용하려면 대괄호([])를 사용해야 합니다. 현재 gap-(--card-gap) p-(--card-pad) 클래스는 유효하지 않아 카드 컴포넌트의 간격과 패딩 스타일이 적용되지 않습니다. gap-[var(--card-gap)] p-[var(--card-pad)]로 수정해야 합니다.

          'gap-[var(--card-gap)] p-[var(--card-pad)] rounded-[12px]',

isLoading: loading,
error: queryError,
} = useExploreClubs(queryParams)
const { data: clubsData } = useExploreClubs(queryParams)

Choose a reason for hiding this comment

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

high

useExploreClubs 훅에서 isLoadingisError 상태를 처리하지 않아 사용자 경험이 저하될 수 있습니다. 현재 구현에서는 데이터를 불러오는 동안 빈 화면이 표시되어 사용자가 결과가 없는 것으로 오인할 수 있습니다. 이전 구현처럼 로딩 및 오류 상태를 명시적으로 처리하여 로딩 인디케이터나 오류 메시지를 보여주는 것을 권장합니다.

Suggested change
const { data: clubsData } = useExploreClubs(queryParams)
const { data: clubsData, isLoading, isError } = useExploreClubs(queryParams)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (2)
src/components/molecules/tab/Tab.tsx (1)

7-23: Avoid mixing “values” and “labels” in Tab union (risk of invalid Tabs value)
Tab currently allows Korean label strings (e.g. '마감순') as actual values, but OPTIONS uses English values (e.g. 'deadline'). Passing a Korean value into <Tabs value=...> won’t match any trigger and can desync selection/state. Prefer separate unions: TabValue = 'deadline' | ... and labels purely in TabOption.label.

src/features/clubs/mutations.ts (1)

19-37: ...options currently can override your internal onSuccess, skipping invalidation (cache correctness bug).
Because ...options comes last, a caller-provided onSuccess replaces this hook’s onSuccess, so invalidateQueries won’t run.

 export function useUpdateClub(
   options?: UseMutationOptions<UpdateClubResponse, Error, UpdateClubPayload>,
 ): UseMutationResult<UpdateClubResponse, Error, UpdateClubPayload> {
   const queryClient = useQueryClient()
+  const { onSuccess, ...restOptions } = options ?? {}
 
   return useMutation({
+    ...restOptions,
     mutationFn: async ({ clubId, body }) => {
       const { data } = await apiClient.patch(`/api/v1/clubs/${clubId}`, body)
       return data
     },
-    onSuccess: (data, variables, onMutateResult, context) => {
+    onSuccess: (data, variables, context) => {
       queryClient.invalidateQueries({
         queryKey: clubKeys.detail(variables.clubId),
       })
-      options?.onSuccess?.(data, variables, onMutateResult, context)
+      onSuccess?.(data, variables, context)
     },
-    ...options,
   })
 }
🧹 Nitpick comments (34)
src/components/molecules/layout/MobileHeader.tsx (1)

14-14: Consider removing the unused hook call.

The useRouter() call without assignment serves no purpose. The comment suggests it prevents "unused var warning," but calling without assignment doesn't prevent unused import warnings—it only adds unnecessary hook overhead.

Apply this diff to clean up:

-  useRouter() // keep potential future navigation; avoid unused var warning
   const { setOpen } = useSearchUrlState()

If navigation is needed in the future, the hook can be re-added then.

src/components/pages/review/new/forms/paper/usePaperForm.ts (1)

19-24: Consider using a distinct type for the form's result options.

The 'NOT_PARTICIPATED' literal on line 22 isn't a member of ResultType enum, yet it's included in PAPER_RESULT_OPTIONS alongside enum values. While the transformation logic (lines 193-198) handles this correctly by mapping it to ResultType.Fail, the type inconsistency could cause confusion.

Consider defining a separate union type for the form-level result options:

+type PaperResultOption = ResultType | 'NOT_PARTICIPATED'
+
 export const PAPER_RESULT_OPTIONS = [
-  { id: ResultType.Pass, label: '합격' },
-  { id: ResultType.Fail, label: '불합격' },
-  { id: 'NOT_PARTICIPATED', label: '합격 후 참여하지않음' },
-  { id: ResultType.Ready, label: '결과 대기중' },
+  { id: ResultType.Pass as PaperResultOption, label: '합격' },
+  { id: ResultType.Fail as PaperResultOption, label: '불합격' },
+  { id: 'NOT_PARTICIPATED' as PaperResultOption, label: '합격 후 참여하지않음' },
+  { id: ResultType.Ready as PaperResultOption, label: '결과 대기중' },
 ] as const
src/styles/design-tokens.css (1)

32-47: New typography tokens look fine; consider clarifying relationship to existing --text-caption-m/sb
You now have both --text-caption-m/sb and --text-caption-1/2/3 with overlapping values; worth ensuring components don’t mix both schemes long-term.

src/components/molecules/tab/Tab.tsx (1)

33-48: defaultValue! + controlled/uncontrolled switch: ensure options always non-empty
If a caller passes options={[]}, defaultValue becomes 'popular' (Line 38) but there will be no matching trigger. Consider guarding empty options (fallback render or throw) to avoid a “selected value with no tab”.

src/components/molecules/layout/sections/ReviewWriteDropdown.tsx (1)

3-45: Remove commented-out DropdownMenu block (and confirm unused imports don’t fail CI)
Leaving the entire previous implementation commented makes the file noisy and can mask real diffs later; also, if your lint/TS settings enforce unused imports, Lines 3–8 will fail once the dropdown is gone.

 import Link from 'next/link'
 import { Button } from '@/components/atoms/Button'
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from '@/components/atoms/dropdownMenu'
 import AppPath from '@/shared/configs/appPath'

 export default function ReviewWriteDropdown() {
   return (
-    // <DropdownMenu>
-    //   ...
-    // </DropdownMenu>
     <Button size="small" variant="solid" className="typo-button" asChild>
       <Link href={AppPath.reviewNew()}>후기 작성</Link>
     </Button>
   )
 }
src/components/pages/review/new/forms/paper/index.tsx (1)

20-20: Consider using useWatch for better performance.

form.watch('clubId') causes the component to re-render on every form field change. For better performance, use the useWatch hook to subscribe only to the specific field.

Apply this diff:

+'use client'
+
+import React from 'react'
+import { useWatch } from 'react-hook-form'
 import { Form } from '@/components/molecules/Form'
 // ... other imports

 export default function PaperForm() {
   const {
     form,
     // ... other destructured values
   } = usePaperForm()

-  const watchedClubId = form.watch('clubId')
+  const watchedClubId = useWatch({ control: form.control, name: 'clubId' })
src/components/pages/review/new/forms/activity/ActivityStep2.tsx (1)

20-111: Consider extracting shared form field patterns.

ActivityStep2 and PaperStep2 share nearly identical implementations for oneLineComment, tip, and freeReview fields (only differing in placeholders). This represents significant duplication across step components.

Consider creating reusable field components like:

// Example: CharacterLimitedTextarea.tsx
interface CharacterLimitedTextareaProps {
  control: Control<any>
  name: string
  label: string
  placeholder: string
  maxLength: number
  minHeight?: string
  required?: boolean
}

export function CharacterLimitedTextarea({ ... }) {
  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <div className="flex items-center gap-1 mb-4">
            <FormLabel className="typo-body-1-b text-black-color">
              {label}
            </FormLabel>
            {required && <span className="text-primary-color">*</span>}
          </div>
          <FormControl>
            <Textarea {...field} placeholder={placeholder} className={minHeight} maxLength={maxLength} />
          </FormControl>
          <div className="text-right typo-caption-m text-grey-color-3">
            {field.value?.length || 0}/{maxLength}
          </div>
          <FormMessage />
        </FormItem>
      )}
    />
  )
}

This would reduce duplication and improve maintainability across all step components.

src/components/pages/review/new/forms/interview/index.tsx (1)

1-56: Significant structural duplication with PaperForm.

InterviewForm shares nearly identical structure with PaperForm (src/components/pages/review/new/forms/paper/index.tsx), differing only in the title, hook name, and step components. This represents ~50 lines of duplicated boilerplate.

Consider extracting a generic ReviewFormWrapper component:

// ReviewFormWrapper.tsx
interface ReviewFormWrapperProps<T extends FieldValues> {
  title: string
  form: UseFormReturn<T>
  currentStep: 1 | 2
  watchedClubId?: number
  Step1Component: React.ComponentType<{ form: UseFormReturn<T> }>
  Step2Component: React.ComponentType<{ form: UseFormReturn<T> }>
  onSubmit: (data: T) => void
  goToNextStep: () => void
  goToPreviousStep: () => void
  isSubmitting: boolean
}

export function ReviewFormWrapper<T extends FieldValues>({
  title,
  form,
  currentStep,
  watchedClubId,
  Step1Component,
  Step2Component,
  onSubmit,
  goToNextStep,
  goToPreviousStep,
  isSubmitting,
}: ReviewFormWrapperProps<T>) {
  return (
    <div className="bg-white-color rounded-2xl p-8 shadow-sm">
      <h2 className="typo-title-1 text-black-color mb-8">{title}</h2>
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-12">
          <ReviewFormHeader control={form.control} selectedClubId={watchedClubId} />
          {currentStep === 1 ? <Step1Component form={form} /> : <Step2Component form={form} />}
          <StepNavigation
            currentStep={currentStep}
            onPrevious={goToPreviousStep}
            onNext={goToNextStep}
            isSubmitting={isSubmitting}
          />
        </form>
      </Form>
    </div>
  )
}

Then simplify each form:

// InterviewForm simplified
export default function InterviewForm() {
  const formState = useInterviewForm()
  return (
    <ReviewFormWrapper
      title="면접 후기 작성"
      {...formState}
      watchedClubId={formState.form.watch('clubId')}
      Step1Component={InterviewStep1}
      Step2Component={InterviewStep2}
    />
  )
}

This would also apply to PaperForm and ActivityForm.

src/components/molecules/tab/TabOverlay.tsx (1)

8-17: Consider documenting or warning about mobile-only props.

The trigger and onReset props are marked as "Mobile only" in comments but are silently ignored when isDesktop is true. This could lead to confusion if a developer passes these props expecting them to work on all devices.

Consider one of these approaches:

Option 1: Add runtime warning

React.useEffect(() => {
  if (isDesktop && (trigger || onReset)) {
    console.warn('TabOverlay: trigger and onReset props are only supported on mobile')
  }
}, [isDesktop, trigger, onReset])

Option 2: Use discriminated union types

type TabOverlayProps = 
  | { mode: 'desktop'; options?: TabOption[]; value?: Tab; defaultValue?: Tab; onChange?: (value: Tab) => void; className?: string }
  | { mode: 'mobile'; options?: TabOption[]; value?: Tab; defaultValue?: Tab; onChange?: (value: Tab) => void; className?: string; trigger?: React.ReactNode; onReset?: () => void }
src/components/molecules/card/CardOverlay.tsx (1)

60-64: Consider extracting the recruitment badge as a Card subcomponent.

The recruitment badge uses absolute positioning with hardcoded styles. For consistency and reusability, consider extracting it as Card.Badge or similar.

Example:

// In Card component file
const CardBadge = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, children, ...props }, ref) => (
    <div
      ref={ref}
      className={cn(
        "absolute top-4 left-4 bg-white text-grey-color-5 typo-caption-sb rounded-full border border-light-color-3 z-10 px-3 py-1.5",
        className
      )}
      {...props}
    >
      {children}
    </div>
  )
)

// Usage in CardOverlay
{club.isRecruiting && (
  <Card.Badge>모집중</Card.Badge>
)}
src/components/pages/review/new/forms/paper/PaperStep1.tsx (1)

102-126: Consider using FormField wrapper for consistency.

Q1 uses manual error rendering (lines 121-125) while Q2 and Q3 use FormField with FormMessage. This inconsistency makes the code harder to maintain.

Refactor Q1 to use FormField for consistency:

-      <FormItem>
+      <FormField
+        control={form.control}
+        name="q1ImportantAppeal"
+        render={({ field }) => (
+          <FormItem>
           <FormLabel className="typo-body-2-sb text-black-color mb-4">
             Q1. 지원서 작성에 있어 가장 중요하게 어필한 것은 무엇이었나요?
             <span className="typo-caption-m text-grey-color-3 ml-2">
               (최대 4개)
             </span>
           </FormLabel>
+          <FormControl>
             <div className="flex flex-col desktop:flex-row gap-3 desktop:flex-wrap">
               {Q1_IMPORTANT_APPEAL_OPTIONS.map((option) => (
                 <OptionButton
                   key={option.id}
                   selected={watchedQ1.includes(option.id)}
                   onClick={() => handleQ1Change(option.id)}
                   className="w-full desktop:w-auto"
                 >
                   {option.label}
                 </OptionButton>
               ))}
             </div>
-          {form.formState.errors.q1ImportantAppeal && (
-            <p className="text-failure-color typo-caption-m mt-1">
-              {form.formState.errors.q1ImportantAppeal.message}
-            </p>
-          )}
-        </FormItem>
+          </FormControl>
+          <FormMessage />
+          </FormItem>
+        )}
+      />
src/components/pages/review/new/shared/ReviewFormHeader.tsx (1)

138-142: Consider making the generation range configurable.

The hardcoded 30 generations (line 138) works for now but could be made configurable via props if different forms need different ranges.

src/components/pages/review/new/forms/interview/InterviewStep1.tsx (1)

103-127: Consider using FormField wrapper for consistency.

Similar to PaperStep1, Q1 uses manual error rendering (lines 122-126) while Q2-Q4 use FormField with FormMessage. Consider refactoring for consistency (see comment on PaperStep1.tsx lines 102-126).

src/shared/configs/appPath.ts (1)

20-21: Avoid duplicating the “review kind” union type across layers.

Now that routes depend on kind, consider moving the union to a shared type (e.g., src/shared/types/review.ts) and importing it here + in FormFactory to prevent drift. (Based on coding guidelines, keep cross-cutting types in src/shared/.)

src/components/molecules/card/MobileCard.tsx (2)

108-124: Bookmark 토글 버튼은 aria-pressed도 같이 주는 게 좋습니다.

스크린리더에 “토글 상태”를 전달하려면 aria-pressed={isSubscribed} 추가를 권장합니다.


1-2: TODO는 이슈로 트래킹하거나 제거해주세요.

릴리즈 코드에 남을 가능성이 크면 PR 내에서 처리하거나, 최소한 이슈 링크를 TODO에 붙여 추적 가능하게 해주세요.

src/app/(root)/(routes)/review/new/[kind]/page.tsx (2)

41-64: Deduplicate the two identical “로딩 중...” renders.

Both branches render the same markup; consolidate to a Loading() component/variable to reduce drift.


67-71: <Suspense> without fallback is probably not buying you much.

Unless FormFactory (or children) actually suspends, this boundary is a no-op; if it does suspend, you likely want a fallback.

public/mockServiceWorker.js (1)

91-117: Timestamp is fine, but consider lazily computing it only when you’ll actually handle the request.
Date.now() runs even for early-return bypasses (navigate, only-if-cached, no active clients). Tiny cost, but easy to avoid.

 addEventListener('fetch', function (event) {
-  const requestInterceptedAt = Date.now()
-
   // Bypass navigation requests.
   if (event.request.mode === 'navigate') {
     return
   }
@@
   if (activeClientIds.size === 0) {
     return
   }
 
+  const requestInterceptedAt = Date.now()
   const requestId = crypto.randomUUID()
   event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
 })
src/components/pages/review/new/forms/FormFactory.tsx (1)

33-59: Optional: hoist validKinds / maps to module scope to avoid re-allocations and tighten typing.
This keeps the type-guard/mappings “single source of truth” and avoids recreating objects per call.

 export type FormKind = 'paper' | 'interview' | 'activity'
+
+const VALID_KINDS = ['paper', 'interview', 'activity'] as const
+
+const FORM_DESCRIPTIONS: Record<FormKind, string> = {
+  paper: '서류 전형 경험을 공유해주세요',
+  interview: '면접 전형 경험을 공유해주세요',
+  activity: '동아리 활동 경험을 공유해주세요',
+}
+
+const FORM_TITLES: Record<FormKind, string> = {
+  paper: '서류 후기 작성',
+  interview: '면접 후기 작성',
+  activity: '활동 후기 작성',
+}
@@
 export const isValidFormKind = (kind: string): kind is FormKind => {
-  const validKinds: FormKind[] = ['paper', 'interview', 'activity']
-  return validKinds.includes(kind as FormKind)
+  return (VALID_KINDS as readonly string[]).includes(kind)
 }
@@
 export const getFormDescription = (kind: FormKind): string => {
-  const descriptions = {
-    paper: '서류 전형 경험을 공유해주세요',
-    interview: '면접 전형 경험을 공유해주세요',
-    activity: '동아리 활동 경험을 공유해주세요',
-  }
-
-  return descriptions[kind] || ''
+  return FORM_DESCRIPTIONS[kind] ?? ''
 }
@@
 export const getFormTitle = (kind: FormKind): string => {
-  const titles = {
-    paper: '서류 후기 작성',
-    interview: '면접 후기 작성',
-    activity: '활동 후기 작성',
-  }
-
-  return titles[kind] || '후기 작성'
+  return FORM_TITLES[kind] ?? '후기 작성'
 }
src/components/molecules/filterBar/MobileFilterBar.tsx (1)

94-125: Optional: add basic a11y attributes for the drawer triggers.
Since each tab button opens a drawer, consider aria-expanded and aria-controls (if MobileMultiDropdown has a stable id) so screen readers get state.

src/components/pages/review/new/forms/activity/ActivityStep1.tsx (1)

158-183: Q3 section should use FormField wrapper for consistency.

Unlike other fields (activityStatus, rate, q1WeeklyHours, q2Difficulty), the Q3 section manually accesses form.formState.errors and displays errors with custom markup. This breaks consistency and may cause issues with form state synchronization.

Consider wrapping Q3 in a FormField for consistency:

-        {/* Q3 */}
-        <FormItem>
-          <FormLabel className="typo-body-2-sb text-black-color mb-4">
+        {/* Q3 */}
+        <FormField
+          control={form.control}
+          name="q3Satisfaction"
+          render={() => (
+            <FormItem>
+              <FormLabel className="typo-body-2-sb text-black-color mb-4">
             Q3. 참여하신 활동 중 가장 큰 만족감을 느꼈던 부분은 무엇이었나요?
             <span className="typo-caption-m text-grey-color-3 ml-2">
               (최대 4개)
             </span>
           </FormLabel>
           <div className="flex flex-col desktop:flex-row gap-3 desktop:flex-wrap">
             {Q3_SATISFACTION_OPTIONS.map((option) => (
               <OptionButton
                 key={option.id}
                 selected={watchedQ3.includes(option.id)}
                 onClick={() => handleQ3Change(option.id)}
                 className="w-full desktop:w-auto"
               >
                 {option.label}
               </OptionButton>
             ))}
           </div>
-          {form.formState.errors.q3Satisfaction && (
-            <p className="text-failure-color typo-caption-m mt-1">
-              {form.formState.errors.q3Satisfaction.message}
-            </p>
-          )}
-        </FormItem>
+              <FormMessage />
+            </FormItem>
+          )}
+        />
src/components/molecules/tab/MobileTab.tsx (2)

46-48: Extract the hardcoded default value to avoid duplication.

The default tab value '마감순' as Tab appears twice (lines 47 and 68). This duplication can lead to inconsistency if one is updated without the other.

+const DEFAULT_TAB_VALUE = '마감순' as Tab
+
 export default function MobileTab({
   ...
 }: MobileTabProps) {
   const [open, setOpen] = React.useState(false)
   const isControlled = value !== undefined
   const [inner, setInner] = React.useState<Tab>(
-    defaultValue || ('마감순' as Tab),
+    defaultValue || DEFAULT_TAB_VALUE,
   )
   ...
   const defaultTabValue = React.useMemo(() => {
-    return defaultValue || ('마감순' as Tab)
+    return defaultValue || DEFAULT_TAB_VALUE
   }, [defaultValue])

Also applies to: 67-69


53-55: useEffect resets tempSelected on every selected change, even when closed.

The current dependency array [selected, open] will reset tempSelected whenever selected changes, regardless of whether the drawer is open. If the intent is to sync only when the drawer opens, consider checking the open state:

   React.useEffect(() => {
-    setTempSelected(selected)
-  }, [selected, open])
+    if (open) {
+      setTempSelected(selected)
+    }
+  }, [selected, open])

This ensures tempSelected syncs when the drawer is open (initial open and subsequent selected changes while open), but not when closed.

src/components/pages/review/new/forms/interview/useInterviewForm.ts (1)

222-230: Consider improving error feedback to users.

The onSubmit catch block only logs the error with console.error. Users won't see any feedback if submission fails. Consider adding toast notification or form-level error state.

   const onSubmit = async (data: InterviewFormType) => {
     try {
       const apiData = transformToApiRequest(data)
       await postBasicReviewMutation.mutateAsync(apiData)
       router.push(AppPath.reviewSubmitted())
     } catch (error) {
-      console.error('Form submission error:', error)
+      // Consider using a toast or form error state for user feedback
+      console.error('Form submission error:', error)
+      // Example: toast.error('제출에 실패했습니다. 다시 시도해주세요.')
     }
   }
src/components/molecules/multiDropDown/MobileMultiDropdown.tsx (1)

225-229: Hardcoded tab.id === 'part' check is brittle.

This condition tightly couples the component to a specific tab ID. Consider adding a showGroupTitles property to MobileMultiDropdownTab to make this configurable.

 export type MobileMultiDropdownTab = {
   id: string
   label: string
   type: 'sort' | 'multi'
   options: TabOption[] | Group[]
   value: string | string[]
   defaultValue?: string | string[]
   onChange: (value: string | string[]) => void
   onReset?: () => void
+  showGroupTitles?: boolean
 }

Then use:

-{group.title && tab.id === 'part' && (
+{group.title && tab.showGroupTitles && (
src/components/(pages)/club/explore/Explore.tsx (6)

36-68: Consider extracting repetitive array parsing logic into a helper.

The partArray, wayArray, and targetArray memos share identical logic with only the variable name differing. This violates DRY and increases maintenance burden.

+const parseFilterValue = (value: string | null | undefined): string[] => {
+  if (value === 'all') return ['all']
+  if (value === null || value === undefined) return []
+  return value.split(',').filter(Boolean)
+}
+
-const partArray = React.useMemo(() => {
-  if (part === 'all') {
-    return ['all']
-  }
-  if (part === null || part === undefined) {
-    return []
-  }
-  return part ? part.split(',').filter(Boolean) : []
-}, [part])
+const partArray = React.useMemo(() => parseFilterValue(part), [part])
+const wayArray = React.useMemo(() => parseFilterValue(way), [way])
+const targetArray = React.useMemo(() => parseFilterValue(target), [target])

70-104: Similar refactoring opportunity for filter change handlers.

handlePartChange, handleWayChange, and handleTargetChange share identical logic. Consider a factory function or parameterized handler.

+const createFilterChangeHandler = (
+  setter: (value: string | null) => void
+) => (values: string[]) => {
+  if (values.includes('all')) {
+    setter('all')
+  } else {
+    setter(values.length > 0 ? values.join(',') : null)
+  }
+}
+
+const handlePartChange = React.useCallback(createFilterChangeHandler(setPart), [setPart])
+const handleWayChange = React.useCallback(createFilterChangeHandler(setWay), [setWay])
+const handleTargetChange = React.useCallback(createFilterChangeHandler(setTarget), [setTarget])

110-112: Unused mapCategory function.

This function is an identity function that simply returns its input unchanged. It adds no value and can be removed.

-const mapCategory = (category: string): string => {
-  return category
-}
-
 const queryParams = {
   page: 0,
   size: 14,
-  field: currentField !== 'all' ? mapCategory(currentField) : undefined,
+  field: currentField !== 'all' ? currentField : undefined,

124-131: Handle loading and error states for data fetching.

The queries useExploreClubs and useUserSubscribes lack explicit loading/error handling. Users may see empty states or encounter silent failures.

Consider adding loading indicators and error handling:

const { data: clubsData, isLoading, error } = useExploreClubs(queryParams)
const { data: subscribesData } = useUserSubscribes()

if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage />

131-131: Potential performance concern with Set recreation on every render.

subscribedClubIds is created as a new Set on every render when subscribes changes. Consider wrapping in useMemo.

-const subscribedClubIds = new Set(subscribes.map((s) => s.clubId))
+const subscribedClubIds = React.useMemo(
+  () => new Set(subscribes.map((s) => s.clubId)),
+  [subscribes]
+)

280-292: Consider handling empty state for clubs.

When clubs array is empty, the grid renders nothing. A user-friendly empty state message would improve UX.

 {/* 카드 그리드 */}
 <div
   className={`grid ${isDesktop ? 'grid-cols-3 gap-8 pt-0 pb-12' : 'grid-cols-1 gap-4 px-5'}`}
 >
+  {clubs.length === 0 && (
+    <div className="col-span-full text-center py-12 text-grey-color-2">
+      검색 결과가 없습니다.
+    </div>
+  )}
   {clubs.map((club) => (
     <CardOverlay
       key={club.clubId}
       club={club}
       isSubscribed={subscribedClubIds.has(club.clubId)}
       onBookmarkClick={handleBookmarkClick}
     />
   ))}
 </div>
src/styles/typography.css (1)

145-185: Consider deprecation strategy for legacy utilities.

The "이전버전" (previous version) section contains legacy utilities that may cause confusion. Consider:

  1. Adding deprecation comments with migration paths
  2. Using a separate file for legacy support
  3. Adding a timeline for removal
 /* 이전버전 */
+/* @deprecated - Use typo-title-1-b instead. Will be removed in v2.0 */
 @utility typo-title-1 {
   font-size: var(--text-title-1);
   line-height: 1.5;
   font-weight: 700;
 }
src/components/molecules/card/Card.tsx (1)

168-170: Limited aspect ratio handling may need extension.

The aspect ratio logic only handles two cases (113/108 and defaults to 3/2). If ratioOverride or p.ratio contains other values, they won't be properly mapped to CSS classes.

Consider using the aspectRatio style property directly instead of predefined classes, or support more aspect ratios:

-const aspectClass = ratio === '113/108' ? 'aspect-[113/108]' : 'aspect-[3/2]'
+const aspectClass = `aspect-[${ratio?.replace('/', '/')}]`

Or handle more cases explicitly:

const aspectClasses: Record<string, string> = {
  '113/108': 'aspect-[113/108]',
  '3/2': 'aspect-[3/2]',
  '16/9': 'aspect-[16/9]',
  '1/1': 'aspect-square',
}
const aspectClass = aspectClasses[ratio ?? '3/2'] ?? 'aspect-[3/2]'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f74aca and 85dd5e8.

⛔ Files ignored due to path filters (8)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/images/heroAll.svg is excluded by !**/*.svg
  • src/assets/icons/bookmark-empty-icon.svg is excluded by !**/*.svg
  • src/assets/icons/bookmark-filled-icon.svg is excluded by !**/*.svg
  • src/assets/icons/bookmark-mobile-empty.svg is excluded by !**/*.svg
  • src/assets/icons/bookmark-mobile-filled.svg is excluded by !**/*.svg
  • src/assets/icons/document-file-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (82)
  • .example.env (0 hunks)
  • CLAUDE.md (1 hunks)
  • package.json (3 hunks)
  • public/mockServiceWorker.js (5 hunks)
  • src/app/(root)/(routes)/review/new/[kind]/[type]/page.tsx (0 hunks)
  • src/app/(root)/(routes)/review/new/[kind]/page.tsx (1 hunks)
  • src/app/(root)/(routes)/review/new/page.tsx (1 hunks)
  • src/assets/icons/index.ts (1 hunks)
  • src/components/(pages)/club/explore/Explore.tsx (2 hunks)
  • src/components/atoms/Button/button.tsx (2 hunks)
  • src/components/atoms/Textarea/Textarea.tsx (1 hunks)
  • src/components/atoms/UnderLineTab/UnderLineTab.tsx (1 hunks)
  • src/components/atoms/sideBar/Sidebar.tsx (1 hunks)
  • src/components/molecules/card/Card.stories.tsx (2 hunks)
  • src/components/molecules/card/Card.tsx (4 hunks)
  • src/components/molecules/card/CardContent.tsx (0 hunks)
  • src/components/molecules/card/CardDescription.tsx (0 hunks)
  • src/components/molecules/card/CardFooter.tsx (0 hunks)
  • src/components/molecules/card/CardHeader.tsx (0 hunks)
  • src/components/molecules/card/CardImage.tsx (0 hunks)
  • src/components/molecules/card/CardMeta.tsx (0 hunks)
  • src/components/molecules/card/CardOverlay.tsx (1 hunks)
  • src/components/molecules/card/CardStats.tsx (0 hunks)
  • src/components/molecules/card/CardTitle.tsx (0 hunks)
  • src/components/molecules/card/MobileCard.tsx (1 hunks)
  • src/components/molecules/card/index.ts (4 hunks)
  • src/components/molecules/card/presets.ts (2 hunks)
  • src/components/molecules/drawer/drawer.tsx (1 hunks)
  • src/components/molecules/drawer/index.ts (1 hunks)
  • src/components/molecules/dynamicQaSection/DynamicQASection.stories.tsx (1 hunks)
  • src/components/molecules/dynamicQaSection/DynamicQASection.tsx (1 hunks)
  • src/components/molecules/dynamicQaSection/index.ts (1 hunks)
  • src/components/molecules/filterBar/MobileFilterBar.tsx (1 hunks)
  • src/components/molecules/layout/Header.tsx (1 hunks)
  • src/components/molecules/layout/MobileHeader.tsx (1 hunks)
  • src/components/molecules/layout/sections/ReviewWriteDropdown.tsx (1 hunks)
  • src/components/molecules/multiDropDown/MobileMultiDropdown.tsx (1 hunks)
  • src/components/molecules/multiDropDown/MultiDropDown.tsx (4 hunks)
  • src/components/molecules/tab/MobileTab.tsx (1 hunks)
  • src/components/molecules/tab/Tab.tsx (1 hunks)
  • src/components/molecules/tab/TabOverlay.tsx (1 hunks)
  • src/components/pages/review/new/ReviewCardTemplate.tsx (0 hunks)
  • src/components/pages/review/new/forms/ActivityNormalForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/ActivityPremiumForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/FormFactory.tsx (1 hunks)
  • src/components/pages/review/new/forms/InterviewNormalForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/InterviewPremiumForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/PaperNormalForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/PaperPremiumForm.tsx (0 hunks)
  • src/components/pages/review/new/forms/activity/ActivityStep1.tsx (1 hunks)
  • src/components/pages/review/new/forms/activity/ActivityStep2.tsx (1 hunks)
  • src/components/pages/review/new/forms/activity/index.tsx (1 hunks)
  • src/components/pages/review/new/forms/activity/useActivityForm.ts (1 hunks)
  • src/components/pages/review/new/forms/hooks/index.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/useActivityNormalForm.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/useActivityPremiumForm.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/useInterviewNormalForm.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/useInterviewPremiumForm.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/usePaperNormalForm.ts (0 hunks)
  • src/components/pages/review/new/forms/hooks/usePaperPremiumForm.ts (0 hunks)
  • src/components/pages/review/new/forms/index.ts (1 hunks)
  • src/components/pages/review/new/forms/interview/InterviewStep1.tsx (1 hunks)
  • src/components/pages/review/new/forms/interview/InterviewStep2.tsx (1 hunks)
  • src/components/pages/review/new/forms/interview/index.tsx (1 hunks)
  • src/components/pages/review/new/forms/interview/useInterviewForm.ts (1 hunks)
  • src/components/pages/review/new/forms/paper/PaperStep1.tsx (1 hunks)
  • src/components/pages/review/new/forms/paper/PaperStep2.tsx (1 hunks)
  • src/components/pages/review/new/forms/paper/index.tsx (1 hunks)
  • src/components/pages/review/new/forms/paper/usePaperForm.ts (1 hunks)
  • src/components/pages/review/new/shared/ReviewFormHeader.tsx (1 hunks)
  • src/components/pages/review/new/shared/StepNavigation.tsx (1 hunks)
  • src/components/pages/review/new/shared/index.ts (1 hunks)
  • src/features/clubs/mutations.ts (3 hunks)
  • src/features/like/mutations.ts (2 hunks)
  • src/features/review/mutations.ts (2 hunks)
  • src/features/subscribe/mutations.ts (1 hunks)
  • src/shared/configs/appPath.ts (1 hunks)
  • src/shared/constants/category.ts (1 hunks)
  • src/shared/constants/filters.ts (1 hunks)
  • src/styles/design-tokens.css (1 hunks)
  • src/styles/globals.css (1 hunks)
  • src/styles/typography.css (1 hunks)
💤 Files with no reviewable changes (24)
  • src/components/molecules/card/CardStats.tsx
  • src/components/molecules/card/CardFooter.tsx
  • src/components/molecules/card/CardTitle.tsx
  • src/components/molecules/card/CardDescription.tsx
  • src/components/pages/review/new/forms/hooks/useActivityNormalForm.ts
  • src/components/pages/review/new/ReviewCardTemplate.tsx
  • src/components/molecules/card/CardMeta.tsx
  • src/components/pages/review/new/forms/hooks/useInterviewNormalForm.ts
  • .example.env
  • src/components/pages/review/new/forms/ActivityNormalForm.tsx
  • src/app/(root)/(routes)/review/new/[kind]/[type]/page.tsx
  • src/components/molecules/card/CardHeader.tsx
  • src/components/pages/review/new/forms/InterviewNormalForm.tsx
  • src/components/pages/review/new/forms/hooks/usePaperPremiumForm.ts
  • src/components/molecules/card/CardContent.tsx
  • src/components/pages/review/new/forms/hooks/usePaperNormalForm.ts
  • src/components/pages/review/new/forms/InterviewPremiumForm.tsx
  • src/components/pages/review/new/forms/PaperNormalForm.tsx
  • src/components/pages/review/new/forms/hooks/useActivityPremiumForm.ts
  • src/components/pages/review/new/forms/hooks/index.ts
  • src/components/pages/review/new/forms/PaperPremiumForm.tsx
  • src/components/pages/review/new/forms/ActivityPremiumForm.tsx
  • src/components/pages/review/new/forms/hooks/useInterviewPremiumForm.ts
  • src/components/molecules/card/CardImage.tsx
🧰 Additional context used
📓 Path-based instructions (5)
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/components/**/*.{ts,tsx}: Use Class Variance Authority (CVA) for type-safe styling variants in component definitions
Use Tailwind CSS with cn() utility (clsx + tailwind-merge) from @/shared/utils/cn for conditional styling
Use React Hook Form + Zod validation with @hookform/resolvers for form handling
Use Radix UI primitives wrapped via shadcn/ui (New York style) for UI components
Organize components using atomic design: atoms/ -> molecules/ -> (pages)/

Files:

  • src/components/molecules/drawer/index.ts
  • src/components/molecules/layout/sections/ReviewWriteDropdown.tsx
  • src/components/atoms/UnderLineTab/UnderLineTab.tsx
  • src/components/atoms/sideBar/Sidebar.tsx
  • src/components/pages/review/new/forms/interview/index.tsx
  • src/components/molecules/card/MobileCard.tsx
  • src/components/pages/review/new/forms/activity/ActivityStep2.tsx
  • src/components/pages/review/new/forms/paper/index.tsx
  • src/components/molecules/dynamicQaSection/index.ts
  • src/components/molecules/filterBar/MobileFilterBar.tsx
  • src/components/atoms/Button/button.tsx
  • src/components/atoms/Textarea/Textarea.tsx
  • src/components/pages/review/new/forms/activity/index.tsx
  • src/components/molecules/tab/TabOverlay.tsx
  • src/components/pages/review/new/forms/activity/useActivityForm.ts
  • src/components/molecules/card/CardOverlay.tsx
  • src/components/molecules/dynamicQaSection/DynamicQASection.stories.tsx
  • src/components/pages/review/new/shared/index.ts
  • src/components/pages/review/new/forms/interview/InterviewStep2.tsx
  • src/components/molecules/multiDropDown/MobileMultiDropdown.tsx
  • src/components/pages/review/new/forms/FormFactory.tsx
  • src/components/pages/review/new/forms/activity/ActivityStep1.tsx
  • src/components/molecules/drawer/drawer.tsx
  • src/components/pages/review/new/forms/paper/usePaperForm.ts
  • src/components/pages/review/new/forms/paper/PaperStep1.tsx
  • src/components/molecules/dynamicQaSection/DynamicQASection.tsx
  • src/components/molecules/multiDropDown/MultiDropDown.tsx
  • src/components/molecules/layout/MobileHeader.tsx
  • src/components/molecules/tab/Tab.tsx
  • src/components/pages/review/new/forms/interview/useInterviewForm.ts
  • src/components/molecules/tab/MobileTab.tsx
  • src/components/pages/review/new/shared/ReviewFormHeader.tsx
  • src/components/(pages)/club/explore/Explore.tsx
  • src/components/pages/review/new/forms/index.ts
  • src/components/molecules/layout/Header.tsx
  • src/components/pages/review/new/shared/StepNavigation.tsx
  • src/components/molecules/card/Card.stories.tsx
  • src/components/molecules/card/index.ts
  • src/components/pages/review/new/forms/paper/PaperStep2.tsx
  • src/components/pages/review/new/forms/interview/InterviewStep1.tsx
  • src/components/molecules/card/presets.ts
  • src/components/molecules/card/Card.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: Use centralized path constants from src/shared/configs/appPath.ts for routing
Use TanStack React Query with Axios for data fetching
Use TypeScript for type safety across all source files

Files:

  • src/components/molecules/drawer/index.ts
  • src/components/molecules/layout/sections/ReviewWriteDropdown.tsx
  • src/components/atoms/UnderLineTab/UnderLineTab.tsx
  • src/components/atoms/sideBar/Sidebar.tsx
  • src/app/(root)/(routes)/review/new/page.tsx
  • src/shared/constants/category.ts
  • src/components/pages/review/new/forms/interview/index.tsx
  • src/components/molecules/card/MobileCard.tsx
  • src/components/pages/review/new/forms/activity/ActivityStep2.tsx
  • src/components/pages/review/new/forms/paper/index.tsx
  • src/shared/constants/filters.ts
  • src/components/molecules/dynamicQaSection/index.ts
  • src/components/molecules/filterBar/MobileFilterBar.tsx
  • src/components/atoms/Button/button.tsx
  • src/components/atoms/Textarea/Textarea.tsx
  • src/components/pages/review/new/forms/activity/index.tsx
  • src/components/molecules/tab/TabOverlay.tsx
  • src/components/pages/review/new/forms/activity/useActivityForm.ts
  • src/components/molecules/card/CardOverlay.tsx
  • src/components/molecules/dynamicQaSection/DynamicQASection.stories.tsx
  • src/components/pages/review/new/shared/index.ts
  • src/components/pages/review/new/forms/interview/InterviewStep2.tsx
  • src/components/molecules/multiDropDown/MobileMultiDropdown.tsx
  • src/components/pages/review/new/forms/FormFactory.tsx
  • src/components/pages/review/new/forms/activity/ActivityStep1.tsx
  • src/features/like/mutations.ts
  • src/components/molecules/drawer/drawer.tsx
  • src/components/pages/review/new/forms/paper/usePaperForm.ts
  • src/components/pages/review/new/forms/paper/PaperStep1.tsx
  • src/components/molecules/dynamicQaSection/DynamicQASection.tsx
  • src/assets/icons/index.ts
  • src/components/molecules/multiDropDown/MultiDropDown.tsx
  • src/components/molecules/layout/MobileHeader.tsx
  • src/app/(root)/(routes)/review/new/[kind]/page.tsx
  • src/components/molecules/tab/Tab.tsx
  • src/components/pages/review/new/forms/interview/useInterviewForm.ts
  • src/features/review/mutations.ts
  • src/components/molecules/tab/MobileTab.tsx
  • src/components/pages/review/new/shared/ReviewFormHeader.tsx
  • src/components/(pages)/club/explore/Explore.tsx
  • src/components/pages/review/new/forms/index.ts
  • src/features/clubs/mutations.ts
  • src/components/molecules/layout/Header.tsx
  • src/components/pages/review/new/shared/StepNavigation.tsx
  • src/components/molecules/card/Card.stories.tsx
  • src/features/subscribe/mutations.ts
  • src/components/molecules/card/index.ts
  • src/shared/configs/appPath.ts
  • src/components/pages/review/new/forms/paper/PaperStep2.tsx
  • src/components/pages/review/new/forms/interview/InterviewStep1.tsx
  • src/components/molecules/card/presets.ts
  • src/components/molecules/card/Card.tsx
src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Next.js 15 App Router with route groups in src/app/(root)/(routes)/ for organizing routes

Files:

  • src/app/(root)/(routes)/review/new/page.tsx
  • src/app/(root)/(routes)/review/new/[kind]/page.tsx
src/shared/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place cross-cutting concerns (configs, hooks, providers, utils, types) in src/shared/

Files:

  • src/shared/constants/category.ts
  • src/shared/constants/filters.ts
  • src/shared/configs/appPath.ts
src/features/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Feature-Sliced Architecture for organizing code by domain (clubs, review, like, oauth, user, etc.) in src/features/

Files:

  • src/features/like/mutations.ts
  • src/features/review/mutations.ts
  • src/features/clubs/mutations.ts
  • src/features/subscribe/mutations.ts
🧠 Learnings (9)
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Use Class Variance Authority (CVA) for type-safe styling variants in component definitions

Applied to files:

  • src/components/atoms/sideBar/Sidebar.tsx
  • src/components/atoms/Button/button.tsx
  • src/components/molecules/multiDropDown/MultiDropDown.tsx
  • src/components/molecules/card/Card.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/app/**/*.{ts,tsx} : Use Next.js 15 App Router with route groups in `src/app/(root)/(routes)/` for organizing routes

Applied to files:

  • src/app/(root)/(routes)/review/new/page.tsx
  • src/components/(pages)/club/explore/Explore.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Use React Hook Form + Zod validation with `hookform/resolvers` for form handling

Applied to files:

  • src/components/pages/review/new/forms/interview/index.tsx
  • src/components/pages/review/new/forms/paper/index.tsx
  • src/components/pages/review/new/forms/activity/index.tsx
  • src/components/pages/review/new/forms/activity/useActivityForm.ts
  • src/components/molecules/dynamicQaSection/DynamicQASection.stories.tsx
  • src/components/pages/review/new/forms/interview/InterviewStep2.tsx
  • src/components/pages/review/new/forms/FormFactory.tsx
  • src/components/pages/review/new/forms/activity/ActivityStep1.tsx
  • src/components/pages/review/new/forms/paper/usePaperForm.ts
  • src/components/pages/review/new/forms/paper/PaperStep1.tsx
  • src/components/pages/review/new/forms/interview/useInterviewForm.ts
  • src/components/pages/review/new/shared/ReviewFormHeader.tsx
  • src/components/pages/review/new/forms/interview/InterviewStep1.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/features/**/*.{ts,tsx} : Use Feature-Sliced Architecture for organizing code by domain (clubs, review, like, oauth, user, etc.) in `src/features/`

Applied to files:

  • src/components/pages/review/new/forms/interview/index.tsx
  • src/components/pages/review/new/shared/ReviewFormHeader.tsx
  • src/components/(pages)/club/explore/Explore.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Use Radix UI primitives wrapped via shadcn/ui (New York style) for UI components

Applied to files:

  • src/components/atoms/Button/button.tsx
  • src/components/molecules/drawer/drawer.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Organize components using atomic design: `atoms/` -> `molecules/` -> `(pages)/`

Applied to files:

  • src/components/molecules/drawer/drawer.tsx
  • src/components/molecules/tab/MobileTab.tsx
  • src/components/molecules/layout/Header.tsx
  • src/components/molecules/card/index.ts
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/**/*.{ts,tsx} : Use TanStack React Query with Axios for data fetching

Applied to files:

  • src/components/(pages)/club/explore/Explore.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/**/*.{ts,tsx} : Use centralized path constants from `src/shared/configs/appPath.ts` for routing

Applied to files:

  • src/shared/configs/appPath.ts
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/mocks/**/*.{ts,tsx} : Use MSW (Mock Service Worker) for API mocking in `src/mocks/`

Applied to files:

  • public/mockServiceWorker.js
🧬 Code graph analysis (23)
src/app/(root)/(routes)/review/new/page.tsx (1)
src/app/(root)/(routes)/review/new/[kind]/page.tsx (1)
  • Page (17-74)
src/shared/constants/category.ts (1)
src/components/atoms/sideBar/Sidebar.tsx (1)
  • SideOption (7-7)
src/components/pages/review/new/forms/activity/ActivityStep2.tsx (4)
src/components/pages/review/new/forms/activity/useActivityForm.ts (1)
  • ActivityFormType (81-81)
src/components/molecules/Form/Form.tsx (5)
  • FormField (166-166)
  • FormItem (161-161)
  • FormLabel (162-162)
  • FormControl (163-163)
  • FormMessage (165-165)
src/components/atoms/Textarea/Textarea.tsx (1)
  • Textarea (23-23)
src/components/molecules/dynamicQaSection/DynamicQASection.tsx (1)
  • DynamicQASection (28-155)
src/components/pages/review/new/forms/paper/index.tsx (5)
src/components/pages/review/new/forms/paper/usePaperForm.ts (1)
  • usePaperForm (87-230)
src/components/molecules/Form/Form.tsx (1)
  • Form (160-160)
src/components/pages/review/new/shared/ReviewFormHeader.tsx (1)
  • ReviewFormHeader (26-152)
src/components/pages/review/new/forms/paper/PaperStep1.tsx (1)
  • PaperStep1 (26-187)
src/components/pages/review/new/forms/paper/PaperStep2.tsx (1)
  • PaperStep2 (20-111)
src/shared/constants/filters.ts (2)
src/components/molecules/tab/Tab.tsx (1)
  • TabOption (16-16)
src/components/molecules/multiDropDown/MultiDropDown.tsx (1)
  • Group (16-16)
src/components/molecules/filterBar/MobileFilterBar.tsx (3)
src/components/molecules/multiDropDown/MobileMultiDropdown.tsx (2)
  • MobileMultiDropdownTab (16-25)
  • MobileMultiDropdown (36-298)
src/components/molecules/multiDropDown/MultiDropDown.tsx (1)
  • Group (16-16)
src/components/molecules/tab/Tab.tsx (1)
  • TabOption (16-16)
src/components/pages/review/new/forms/activity/index.tsx (5)
src/components/pages/review/new/forms/activity/useActivityForm.ts (1)
  • useActivityForm (83-215)
src/components/pages/review/new/shared/ReviewFormHeader.tsx (1)
  • ReviewFormHeader (26-152)
src/components/pages/review/new/forms/activity/ActivityStep1.tsx (1)
  • ActivityStep1 (26-187)
src/components/pages/review/new/forms/activity/ActivityStep2.tsx (1)
  • ActivityStep2 (20-111)
src/components/pages/review/new/shared/StepNavigation.tsx (1)
  • StepNavigation (14-44)
src/components/molecules/tab/TabOverlay.tsx (3)
src/components/molecules/tab/Tab.tsx (1)
  • TabOption (16-16)
src/shared/hooks/useMediaQuery.ts (1)
  • useMediaQuery (6-23)
src/components/molecules/tab/MobileTab.tsx (1)
  • MobileTab (35-184)
src/components/molecules/dynamicQaSection/DynamicQASection.stories.tsx (1)
src/components/molecules/dynamicQaSection/DynamicQASection.tsx (1)
  • DynamicQASection (28-155)
src/components/molecules/multiDropDown/MobileMultiDropdown.tsx (4)
src/components/molecules/tab/Tab.tsx (1)
  • TabOption (16-16)
src/components/molecules/multiDropDown/MultiDropDown.tsx (1)
  • Group (16-16)
src/components/molecules/drawer/drawer.tsx (4)
  • Drawer (124-124)
  • DrawerContent (129-129)
  • DrawerHeader (130-130)
  • DrawerFooter (131-131)
src/components/atoms/UnderLineTab/UnderLineTab.tsx (1)
  • UnderLineTab (66-145)
src/components/pages/review/new/forms/FormFactory.tsx (4)
src/components/pages/review/new/forms/index.ts (4)
  • FormKind (3-3)
  • isValidFormKind (5-5)
  • getFormDescription (6-6)
  • getFormTitle (7-7)
src/components/pages/review/new/forms/paper/index.tsx (1)
  • PaperForm (10-56)
src/components/pages/review/new/forms/interview/index.tsx (1)
  • InterviewForm (10-56)
src/components/pages/review/new/forms/activity/index.tsx (1)
  • ActivityForm (10-56)
src/components/pages/review/new/forms/activity/ActivityStep1.tsx (2)
src/components/pages/review/new/forms/activity/useActivityForm.ts (5)
  • ActivityFormType (81-81)
  • ACTIVITY_STATUS_OPTIONS (19-22)
  • Q1_WEEKLY_HOURS_OPTIONS (25-30)
  • Q2_DIFFICULTY_OPTIONS (33-38)
  • Q3_SATISFACTION_OPTIONS (41-50)
src/components/molecules/Form/Form.tsx (5)
  • FormField (166-166)
  • FormItem (161-161)
  • FormLabel (162-162)
  • FormControl (163-163)
  • FormMessage (165-165)
src/components/pages/review/new/forms/paper/usePaperForm.ts (3)
src/shared/configs/appValidation.ts (1)
  • appValidation (3-24)
src/features/review/mutations.ts (1)
  • usePostBasicReview (16-32)
src/features/review/types.ts (2)
  • BasicReviewCreateRequest (158-191)
  • AnswerRequest (28-41)
src/components/pages/review/new/forms/paper/PaperStep1.tsx (2)
src/components/pages/review/new/forms/paper/usePaperForm.ts (5)
  • PaperFormType (85-85)
  • PAPER_RESULT_OPTIONS (19-24)
  • Q1_IMPORTANT_APPEAL_OPTIONS (27-36)
  • Q2_REFERENCE_INFO_OPTIONS (39-44)
  • Q3_TECH_DESCRIPTION_OPTIONS (47-52)
src/components/molecules/Form/Form.tsx (5)
  • FormField (166-166)
  • FormItem (161-161)
  • FormLabel (162-162)
  • FormControl (163-163)
  • FormMessage (165-165)
src/components/molecules/multiDropDown/MultiDropDown.tsx (1)
src/shared/utils/index.ts (1)
  • cn (2-2)
src/app/(root)/(routes)/review/new/[kind]/page.tsx (2)
src/shared/providers/auth-provider.tsx (1)
  • useAuth (138-144)
src/components/pages/review/new/forms/FormFactory.tsx (3)
  • FormKind (8-8)
  • isValidFormKind (34-37)
  • FormFactory (14-31)
src/components/molecules/tab/Tab.tsx (1)
src/components/molecules/tab/index.ts (1)
  • Tab (1-1)
src/components/pages/review/new/forms/interview/useInterviewForm.ts (3)
src/shared/configs/appValidation.ts (1)
  • appValidation (3-24)
src/features/review/mutations.ts (1)
  • usePostBasicReview (16-32)
src/features/review/types.ts (2)
  • BasicReviewCreateRequest (158-191)
  • AnswerRequest (28-41)
src/features/subscribe/mutations.ts (1)
src/features/subscribe/keys.ts (1)
  • subscribeKeys (1-6)
src/components/molecules/card/index.ts (1)
src/components/molecules/card/Card.tsx (2)
  • CardImage (151-209)
  • CardBookmark (298-330)
src/components/pages/review/new/forms/paper/PaperStep2.tsx (4)
src/components/pages/review/new/forms/paper/usePaperForm.ts (1)
  • PaperFormType (85-85)
src/components/molecules/Form/Form.tsx (5)
  • FormField (166-166)
  • FormItem (161-161)
  • FormLabel (162-162)
  • FormControl (163-163)
  • FormMessage (165-165)
src/components/atoms/Textarea/Textarea.tsx (1)
  • Textarea (23-23)
src/components/molecules/dynamicQaSection/DynamicQASection.tsx (1)
  • DynamicQASection (28-155)
src/components/pages/review/new/forms/interview/InterviewStep1.tsx (2)
src/components/pages/review/new/forms/interview/useInterviewForm.ts (6)
  • InterviewFormType (90-90)
  • INTERVIEW_RESULT_OPTIONS (19-24)
  • Q1_QUESTION_TYPE_OPTIONS (27-36)
  • Q2_INTERVIEWER_ATTITUDE_OPTIONS (39-44)
  • Q3_MAIN_TOPIC_OPTIONS (47-52)
  • Q4_EMPHASIZED_SKILL_OPTIONS (55-60)
src/components/molecules/Form/Form.tsx (5)
  • FormField (166-166)
  • FormItem (161-161)
  • FormLabel (162-162)
  • FormControl (163-163)
  • FormMessage (165-165)
src/components/molecules/card/Card.tsx (3)
src/components/molecules/card/presets.ts (1)
  • PRESET (3-36)
src/assets/icons/GreyThumbsUp.tsx (1)
  • GreyThumbsUp (2-18)
src/assets/icons/GreyMessage.tsx (1)
  • GreyMessage (2-11)

"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"import-in-the-middle": "^2.0.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check package.json structure around lines 41 and 47
cat -n package.json | sed -n '30,55p'

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 1110


🏁 Script executed:

#!/bin/bash
# Check if these packages are peer dependencies of Sentry
npm info @sentry/[email protected] peerDependencies 2>/dev/null | head -30

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Check all dependencies of @sentry/nextjs to see if it includes these packages
npm info @sentry/[email protected] dependencies | head -40

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Also check the actual installed node_modules to see the dependency tree
if [ -d "node_modules/@sentry/nextjs" ]; then
  cat node_modules/@sentry/nextjs/package.json | grep -A 30 '"dependencies"'
else
  echo "node_modules not available - checking package-lock.json instead"
  # Try to extract from package-lock if available
  if [ -f "package-lock.json" ]; then
    cat package-lock.json | grep -A 5 '"import-in-the-middle"' | head -20
  fi
fi

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 651


🏁 Script executed:

#!/bin/bash
# Let's check if there's any documentation or comments in package.json about these dependencies
cat -n package.json | grep -B 3 -A 3 "import-in-the-middle\|require-in-the-middle"

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 575


🏁 Script executed:

#!/bin/bash
# Check @sentry/node specifically since it has these as dependencies
npm info @sentry/[email protected] dependencies | grep -A 2 "import-in-the-middle\|require-in-the-middle"

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 192


Remove import-in-the-middle and require-in-the-middle from main dependencies.

These packages are transitive dependencies of @sentry/nextjs and its sub-packages (@sentry/node, @sentry/opentelemetry). They should not be explicitly listed in the main dependencies section—they will be installed automatically through Sentry's dependency tree. Explicitly pinning them in main dependencies can cause version conflicts and unnecessary duplication. Remove them from lines 41 and 47 unless there's a specific reason documented in comments (e.g., monorepo resolution or peer dependency requirement).

🤖 Prompt for AI Agents
In package.json around lines 41 and 47, remove the explicit entries for
"import-in-the-middle" and "require-in-the-middle" from the main "dependencies"
section; these are transitive dependencies brought in by @sentry/nextjs and
should not be pinned here. Delete those two lines (or move them to an
appropriate resolutions/overrides with a documented comment only if you have a
specific monorepo/peer-resolution reason), then run your package manager install
and verify with npm/yarn/pnpm that no resolution conflicts remain (e.g., npm ls)
to ensure removal is safe.

Comment on lines 10 to 11
const PACKAGE_VERSION = '2.12.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /repo && cat package.json | grep -A 2 -B 2 '"msw"'

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 135


🏁 Script executed:

cd /repo && grep -n "const PACKAGE_VERSION\|const INTEGRITY_CHECKSUM" public/mockServiceWorker.js | head -5

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 135


🏁 Script executed:

cat package.json | grep -A 2 -B 2 '"msw"'

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 286


🏁 Script executed:

grep -n "const PACKAGE_VERSION\|const INTEGRITY_CHECKSUM" public/mockServiceWorker.js

Repository: dnd-side-project/dnd-13th-7-frontend

Length of output: 180


Regenerate public/mockServiceWorker.js to match the installed MSW version.

The worker file declares PACKAGE_VERSION = '2.12.2', but package.json specifies msw@^2.10.4. This mismatch can cause silent runtime failures when the client runtime expects different API payloads or integrity behavior. Since this is a generated artifact, regenerate it using your MSW setup command (typically msw init or equivalent) to align with the installed version.

🤖 Prompt for AI Agents
In public/mockServiceWorker.js around lines 10 to 11, the embedded
PACKAGE_VERSION and INTEGRITY_CHECKSUM are out of sync with the installed MSW;
regenerate the worker file to match the installed msw version by running the MSW
initialization command (e.g., npx msw init <public-dir> --save or the project’s
msw init script) so the generated PACKAGE_VERSION and checksum are updated, then
commit the regenerated public/mockServiceWorker.js.

Comment on lines 1 to 40
'use client'

const validKinds = ['paper', 'interview', 'activity'] as const
import { Suspense, useEffect, useState } from 'react'
import { notFound, redirect } from 'next/navigation'
import {
FormFactory,
isValidFormKind,
type FormKind,
} from '@/components/pages/review/new/forms'
import AppPath from '@/shared/configs/appPath'
import { useAuth } from '@/shared/providers/auth-provider'

export async function generateMetadata({
params,
}: {
interface PageProps {
params: Promise<{ kind: string }>
}): Promise<Metadata> {
const { kind } = await params
const isValidKind = (validKinds as readonly string[]).includes(kind)
}

if (!isValidKind) {
return {
title: '페이지를 찾을 수 없습니다',
}
}
export default function Page({ params }: PageProps) {
const { user, isLoading } = useAuth()
const [formKind, setFormKind] = useState<FormKind | null>(null)

const getKindDisplayName = (kind: string) => {
switch (kind) {
case 'paper':
return '서류'
case 'interview':
return '인터뷰/면접'
case 'activity':
return '활동'
default:
return kind
}
}
useEffect(() => {
async function loadParams() {
const { kind } = await params

const kindName = getKindDisplayName(kind)
if (!isValidFormKind(kind)) {
notFound()
}

return {
title: `${kindName} 후기 작성`,
description: `IT 동아리 ${kindName} 후기를 작성해보세요. 일반 후기와 프리미엄 후기 중 선택하여 경험을 공유하고 다른 분들에게 도움을 주세요.`,
keywords: [`IT 동아리 ${kindName} 후기`, '동아리 후기 작성', '경험 공유'],
openGraph: {
title: `${kindName} 후기 작성 | 모여잇`,
description: `IT 동아리 ${kindName} 후기를 작성해보세요. 일반 후기와 프리미엄 후기 중 선택하여 경험을 공유하고 다른 분들에게 도움을 주세요.`,
},
}
}
setFormKind(kind as FormKind)
}

export default async function Page({
params,
}: {
params: Promise<{ kind: string }>
}) {
const { kind } = await params
const isValidKind = (validKinds as readonly string[]).includes(kind)
if (!isValidKind) {
notFound()
}
loadParams()
}, [params])

const getKindDisplayName = (kind: string) => {
switch (kind) {
case 'paper':
return '서류'
case 'interview':
return '인터뷰/면접'
case 'activity':
return '활동'
default:
return kind
useEffect(() => {
if (!isLoading && !user) {
redirect(AppPath.login())
}
}
}, [user, isLoading])

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Next.js 15 redirect() Client Component useEffect event handler

💡 Result:

Short answer: In Next.js (v15/app-router) you should NOT call redirect() inside a client-side event handler. redirect() may be used in Client Components during render (or inside Server Actions) but it intentionally throws to end render. For event handlers use the client router (useRouter().push/replace) or call a Server Action that calls redirect().

Examples:

  • Redirect during render in a Client Component:
    'use client'
    import { redirect, usePathname } from 'next/navigation'
    export default function C(){ const p = usePathname(); if(p.startsWith('/admin')) redirect('/login'); return

    }

  • Redirect from an event handler (client-side):
    'use client'
    import { useRouter } from 'next/navigation'
    export default function C(){ const router = useRouter(); return <button onClick={()=>router.push('/target')}>Go }

  • Or call a Server Action from a form/button; the Server Action can call redirect():
    // server action: redirect(/post/${id})
    // client:

    Submit

Docs: Next.js redirect() reference and routing guides cover this behavior and recommend useRouter for event handlers and Server Actions for server-side redirects. [1][2][3]

References:
[1] Next.js redirect() API reference.
[2] Next.js routing — Redirecting guide.
[3] useRouter / client navigation docs.


🌐 Web query:

Next.js 15 notFound() Client Component useEffect

💡 Result:

Short answer: notFound() is a server-side mechanism and must be triggered during server rendering (or inside a Server Component). Calling notFound() from a Client Component (including inside useEffect) will not produce the server 404 behavior — instead, do a client-side navigation to your 404 route (or call router.replace('/404')) from a Client Component. [1][2]

What to do

  • If you decide on server-side detection: call notFound() from a Server Component or from server code while rendering the route segment so Next renders your app/not-found UI and returns the correct status. [1]
  • If you must act on the client (e.g. inside useEffect): use next/navigation's client router to navigate to a 404 page (router.replace('/404') or router.push('/404')), or render a local “Not found” UI. Example:

'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'

export default function ClientCheck({ id }) {
const router = useRouter()
useEffect(() => {
async function check() {
const res = await fetch(/api/exists/${id})
if (!res.ok) router.replace('/404') // client-side navigation to 404
}
check()
}, [id, router])

return

Loading…

}

References

  • Next.js not-found file / notFound() behavior. [1]
  • next/navigation useRouter (router.push / router.replace) for client navigation. [2]

[1] Next.js docs — not-found / notFound behavior.
[2] Next.js docs — useRouter / client navigation.


🌐 Web query:

Next.js 15 useParams() hook dynamic segment

💡 Result:

Short summary — how to use useParams() in Next.js 15 for dynamic segments

  • Where: import from next/navigation and only in Client Components ('use client'). [1]
  • What it returns: an object whose keys are the dynamic segment names and whose values are either string (for single segments) or string[] (for catch-all segments). If no dynamic segments, returns {}. In Pages Router it may be null on initial render. [1]
  • Catch-all / optional catch-all:
    • [...slug] → params.slug is string[] (e.g. ['a','b']).
    • [[...slug]] → params.slug may be undefined or string[]. [2]
  • Typing: you can provide a generic for TypeScript: const params = useParams<{ tag: string; item: string }>(). [1]

Minimal example (Client component)
'use client'
import { useParams } from 'next/navigation'

export default function C() {
const params = useParams<{ slug: string }>()
// route: app/blog/[slug]/page.tsx — URL /blog/hello => params.slug === 'hello'
return

{params.slug}

}

Sources: Next.js docs — useParams API and Dynamic Segments. [1][2]


Don't call redirect() or notFound() inside useEffect on Client Components.

Per Next.js 15 documentation, redirect() in Client Components is only supported during render, not in event handlers or effects. Similarly, notFound() is a server-side mechanism and cannot be reliably called from Client Components. For client-side effects, use useRouter().replace() instead.

Suggested refactor: use useParams() to access the [kind] dynamic segment and useRouter().replace() in effects for conditional navigation:

 'use client'

-import { Suspense, useEffect, useState } from 'react'
-import { notFound, redirect } from 'next/navigation'
+import { Suspense, useEffect, useMemo } from 'react'
+import { useParams, useRouter } from 'next/navigation'
 import {
   FormFactory,
   isValidFormKind,
   type FormKind,
 } from '@/components/pages/review/new/forms'
 import AppPath from '@/shared/configs/appPath'
 import { useAuth } from '@/shared/providers/auth-provider'

-interface PageProps {
-  params: Promise<{ kind: string }>
-}
-
-export default function Page({ params }: PageProps) {
+export default function Page() {
+  const router = useRouter()
+  const params = useParams<{ kind?: string }>()
   const { user, isLoading } = useAuth()
-  const [formKind, setFormKind] = useState<FormKind | null>(null)
+  const kind = params?.kind
+  const formKind = useMemo<FormKind | null>(
+    () => (kind && isValidFormKind(kind) ? kind : null),
+    [kind],
+  )

   useEffect(() => {
-    async function loadParams() {
-      const { kind } = await params
-
-      if (!isValidFormKind(kind)) {
-        notFound()
-      }
-
-      setFormKind(kind as FormKind)
-    }
-
-    loadParams()
-  }, [params])
+    if (kind && !formKind) router.replace(AppPath.reviewNew())
+  }, [kind, formKind, router])

   useEffect(() => {
     if (!isLoading && !user) {
-      redirect(AppPath.login())
+      router.replace(AppPath.login())
     }
-  }, [user, isLoading])
+  }, [user, isLoading, router])

Alternatively, move the 404 logic to a Server Component wrapper if you need proper 404 handling with app/not-found.tsx.

🤖 Prompt for AI Agents
In src/app/(root)/(routes)/review/new/[kind]/page.tsx around lines 1 to 40, the
component calls next/navigation.redirect() and next/navigation.notFound() inside
client-side useEffect hooks which is unsupported; replace those calls with
client-safe navigation: obtain the route param via useParams() (instead of
awaiting params Promise) and in the effect use
useRouter().replace(AppPath.login()) for redirecting unauthenticated users, and
for invalid kinds either call router.replace('/404') or move the notFound()
validation into a parent Server Component (or a server wrapper) so the 404 is
handled server-side; update state setting to use the value from useParams and
remove any direct uses of redirect() / notFound() inside client effects.

Comment on lines 22 to 95
export default async function Page() {
return (
<main className="w-full h-full">
<div className="max-w-[530px] h-full mx-auto flex flex-col items-center justify-center">
{/* 제목 */}
<div className="text-center mb-8">
<h2 className="typo-title-1 text-black-color mb-8">후기 작성</h2>
</div>

{/* 카드 컨테이너 */}
<div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-[530px] shadow-sm">
<p className="typo-body-2-sb text-grey-color-4">
작성하실 후기 종류를 선택해주세요
</p>
<div className="w-full flex flex-col gap-4">
{/* 서류 후기 카드 */}
<Link href={AppPath.reviewNew('paper')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentPencilIcon role="img" aria-label="서류 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
서류 후기
</h3>
<p className="typo-button-m text-grey-color-3">
서류 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 면접 후기 카드 */}
<Link href={AppPath.reviewNew('interview')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentDiamondIcon role="img" aria-label="면접 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
면접 후기
</h3>
<p className="typo-button-m text-grey-color-3">
면접 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 활동 후기 카드 */}
<Link href={AppPath.reviewNew('activity')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentFileIcon role="img" aria-label="활동 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
활동 후기
</h3>
<p className="typo-button-m text-grey-color-3">
동아리 활동 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
</div>
</div>
</div>
</main>
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Layout is likely not responsive due to fixed w-[530px] container/card sizing
This page hard-codes w-[530px] (Line 32) which will overflow on smaller screens; consider w-full max-w-[530px] and min-h-screen to center reliably.

 export default async function Page() {
   return (
-    <main className="w-full h-full">
-      <div className="max-w-[530px] h-full mx-auto flex flex-col items-center justify-center">
+    <main className="w-full min-h-screen">
+      <div className="w-full max-w-[530px] mx-auto flex flex-col items-center justify-center px-4">
         {/* 제목 */}
         <div className="text-center mb-8">
           <h2 className="typo-title-1 text-black-color mb-8">후기 작성</h2>
         </div>

         {/* 카드 컨테이너 */}
-        <div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-[530px] shadow-sm">
+        <div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-full shadow-sm">
📝 Committable suggestion

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

Suggested change
export default async function Page() {
return (
<main className="w-full h-full">
<div className="max-w-[530px] h-full mx-auto flex flex-col items-center justify-center">
{/* 제목 */}
<div className="text-center mb-8">
<h2 className="typo-title-1 text-black-color mb-8">후기 작성</h2>
</div>
{/* 카드 컨테이너 */}
<div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-[530px] shadow-sm">
<p className="typo-body-2-sb text-grey-color-4">
작성하실 후기 종류를 선택해주세요
</p>
<div className="w-full flex flex-col gap-4">
{/* 서류 후기 카드 */}
<Link href={AppPath.reviewNew('paper')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentPencilIcon role="img" aria-label="서류 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
서류 후기
</h3>
<p className="typo-button-m text-grey-color-3">
서류 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 면접 후기 카드 */}
<Link href={AppPath.reviewNew('interview')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentDiamondIcon role="img" aria-label="면접 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
면접 후기
</h3>
<p className="typo-button-m text-grey-color-3">
면접 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 활동 후기 카드 */}
<Link href={AppPath.reviewNew('activity')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentFileIcon role="img" aria-label="활동 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
활동 후기
</h3>
<p className="typo-button-m text-grey-color-3">
동아리 활동 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
</div>
</div>
</div>
</main>
)
export default async function Page() {
return (
<main className="w-full min-h-screen">
<div className="w-full max-w-[530px] mx-auto flex flex-col items-center justify-center px-4">
{/* 제목 */}
<div className="text-center mb-8">
<h2 className="typo-title-1 text-black-color mb-8">후기 작성</h2>
</div>
{/* 카드 컨테이너 */}
<div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-full shadow-sm">
<p className="typo-body-2-sb text-grey-color-4">
작성하실 후기 종류를 선택해주세요
</p>
<div className="w-full flex flex-col gap-4">
{/* 서류 후기 카드 */}
<Link href={AppPath.reviewNew('paper')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentPencilIcon role="img" aria-label="서류 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
서류 후기
</h3>
<p className="typo-button-m text-grey-color-3">
서류 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 면접 후기 카드 */}
<Link href={AppPath.reviewNew('interview')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentDiamondIcon role="img" aria-label="면접 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
면접 후기
</h3>
<p className="typo-button-m text-grey-color-3">
면접 전형 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
{/* 활동 후기 카드 */}
<Link href={AppPath.reviewNew('activity')}>
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group hover:border-primary-color transition-colors">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<DocumentFileIcon role="img" aria-label="활동 후기" />
</div>
<div className="flex-1">
<h3 className="typo-body-3-b text-black-color mb-1">
활동 후기
</h3>
<p className="typo-button-m text-grey-color-3">
동아리 활동 경험을 공유해주세요
</p>
</div>
</div>
</div>
</Link>
</div>
</div>
</div>
</main>
)
}
🤖 Prompt for AI Agents
In src/app/(root)/(routes)/review/new/page.tsx around lines 22 to 95, the layout
uses fixed w-[530px] on the outer centered container and the inner card which
will overflow on small screens; change the outer wrapper to use w-full
max-w-[530px] and add min-h-screen to the main (or the centered container) to
ensure vertical centering on short/tall viewports, and update the inner card to
use w-full (remove the fixed 530px width) so cards scale responsively while
keeping the max width constraint.

Comment on lines 165 to 166
const [failed, setFailed] = React.useState(false)
const src = failed || !logoUrl ? fallbackSrc! : logoUrl!
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential runtime error when both logoUrl and fallbackSrc are null.

If logoUrl is falsy and fallbackSrc is explicitly passed as null, the non-null assertion fallbackSrc! will result in null being passed to the Image src, causing a runtime error.

-const src = failed || !logoUrl ? fallbackSrc! : logoUrl!
+const src = failed || !logoUrl ? (fallbackSrc ?? '/images/default.svg') : logoUrl
🤖 Prompt for AI Agents
In src/components/molecules/card/Card.tsx around lines 165-166, the current
expression uses non-null assertions and can pass null to the Image src when both
logoUrl and fallbackSrc are null; update the src computation to safely handle
null/undefined by using a nullish coalescing fallback (e.g. const src = failed
|| !logoUrl ? (fallbackSrc ?? '') : logoUrl) or otherwise ensure src is a
string/undefined (not null) before passing to the Image component, and adjust
types if needed so Image never receives null.

Comment on lines 24 to 49
<FormField
control={form.control}
name="oneLineComment"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-1 mb-4">
<FormLabel className="typo-body-1-b text-black-color">
한줄평
</FormLabel>
<span className="text-primary-color">*</span>
</div>
<FormControl>
<Textarea
{...field}
placeholder="면접에서 느낀 가장 핵심적인 소감이나 느낌을 요약해 주세요."
className="min-h-[60px]"
maxLength={30}
/>
</FormControl>
<div className="text-right typo-caption-m text-grey-color-3">
{field.value?.length || 0}/30
</div>
<FormMessage />
</FormItem>
)}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against uncontrolled→controlled warnings for textareas.

If defaultValues ever leave these fields as undefined, {...field} can produce React warnings. Safer pattern is to coerce value to '' at the component boundary.

-              <Textarea
-                {...field}
+              <Textarea
+                {...field}
+                value={field.value ?? ''}
                 placeholder="면접에서 느낀 가장 핵심적인 소감이나 느낌을 요약해 주세요."
                 className="min-h-[60px]"
                 maxLength={30}
               />

Repeat similarly for tip and freeReview.

Also applies to: 61-108

🤖 Prompt for AI Agents
In src/components/pages/review/new/forms/interview/InterviewStep2.tsx around
lines 24 to 49 (and similarly for the other textareas at lines 61-108), the
Textarea can receive undefined via {...field} which may trigger React
uncontrolled→controlled warnings; coerce the incoming field.value to an empty
string when passing into the Textarea (e.g. set value={field.value ?? ''} or
spread {...field} but override value) and apply the same fix for the tip and
freeReview fields so all textareas always receive a string value.

Comment on lines 212 to 220
const onSubmit = async (data: PaperFormType) => {
try {
const apiData = transformToApiRequest(data)
await postBasicReviewMutation.mutateAsync(apiData)
router.push(AppPath.reviewSubmitted())
} catch (error) {
console.error('Form submission error:', error)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add user-facing error feedback on submission failure.

Currently, errors are only logged to console. Users won't know if their submission failed or why.

Consider adding toast notifications or form-level error state:

 const onSubmit = async (data: PaperFormType) => {
   try {
     const apiData = transformToApiRequest(data)
     await postBasicReviewMutation.mutateAsync(apiData)
     router.push(AppPath.reviewSubmitted())
   } catch (error) {
     console.error('Form submission error:', error)
+    // TODO: Add user-facing error notification (e.g., toast)
   }
 }
📝 Committable suggestion

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

Suggested change
const onSubmit = async (data: PaperFormType) => {
try {
const apiData = transformToApiRequest(data)
await postBasicReviewMutation.mutateAsync(apiData)
router.push(AppPath.reviewSubmitted())
} catch (error) {
console.error('Form submission error:', error)
}
}
const onSubmit = async (data: PaperFormType) => {
try {
const apiData = transformToApiRequest(data)
await postBasicReviewMutation.mutateAsync(apiData)
router.push(AppPath.reviewSubmitted())
} catch (error) {
console.error('Form submission error:', error)
// TODO: Add user-facing error notification (e.g., toast)
}
}
🤖 Prompt for AI Agents
In src/components/pages/review/new/forms/paper/usePaperForm.ts around lines 212
to 220, the submission catch only logs errors to the console so users get no
feedback; update the catch block to surface a user-facing error by either
invoking the app's toast/notification helper with a clear message (and include
brief error details for support) or by calling the form's setError/formState to
display a form-level error banner, ensure the submit button state is
reset/enabled after failure, and keep console.error for debugging.

Comment on lines 1 to 8
import type { SideOption } from '@/components/atoms/sideBar/Sidebar'

export const CATEGORY_OPTIONS: SideOption[] = [
{ label: '전체', value: 'all' },
{ label: '기획', value: '기획' },
{ label: '디자인', value: '디자인' },
{ label: '개발', value: '개발' },
]
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

src/shared/** should not import types from src/components/** (dependency direction / circular risk)
src/shared/constants/category.ts importing SideOption from @/components/atoms/sideBar/Sidebar couples shared → UI and can create circular deps as the app grows. Move SideOption to src/shared/types/... (or define locally) and have Sidebar.tsx import from shared instead.

-import type { SideOption } from '@/components/atoms/sideBar/Sidebar'
+import type { SideOption } from '@/shared/types/sideOption'

(Then create src/shared/types/sideOption.ts exporting that type, and update src/components/atoms/sideBar/Sidebar.tsx to import it.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/shared/constants/category.ts lines 1-8, the file currently imports
SideOption from src/components which creates an undesirable shared→UI
dependency; to fix, create a new type file src/shared/types/sideOption.ts that
exports the SideOption interface/type, change src/shared/constants/category.ts
to import SideOption from that new shared types path (or define the type inline
if preferred), and update src/components/atoms/sideBar/Sidebar.tsx to import
SideOption from src/shared/types/sideOption instead of its local definition;
adjust any relative/alias imports and run a build/TS check to ensure no
remaining circular or missing imports.

Comment on lines 10 to 32
export const PART_OPTIONS: Group[] = [
{
title: '전체',
options: [{ label: '전체', value: 'all' }],
},
{
title: '기획',
options: [{ label: 'PM/PO', value: 'PM/PO' }],
},
{
title: '디자인',
options: [{ label: '프로덕트 디자이너', value: '프로덕트 디자이너' }],
},
{
title: '개발자',
options: [
{ label: '백엔드 개발자', value: '백엔드 개발자' },
{ label: '프론트엔드 개발자', value: '프론트엔드 개발자' },
{ label: '안드로이드 개발자', value: '안드로이드 개발자' },
{ label: 'iOS 개발자', value: 'iOS 개발자' },
],
},
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent option value format in PART_OPTIONS.

The options use mixed value formats:

  • Line 13: value: 'all' (English)
  • Lines 17, 21, 26-29: Korean labels used as values (e.g., value: 'PM/PO', value: '프로덕트 디자이너')

This inconsistency could cause filtering issues if the backend or consuming components expect a consistent format.

Consider standardizing to either:

  1. All English values with Korean labels, OR
  2. All Korean values consistently

Example standardization:

export const PART_OPTIONS: Group[] = [
  {
    title: '전체',
    options: [{ label: '전체', value: 'all' }],
  },
  {
    title: '기획',
    options: [{ label: 'PM/PO', value: 'pm-po' }], // Standardized
  },
  {
    title: '디자인',
    options: [{ label: '프로덕트 디자이너', value: 'product-designer' }], // Standardized
  },
  {
    title: '개발자',
    options: [
      { label: '백엔드 개발자', value: 'backend-developer' },
      { label: '프론트엔드 개발자', value: 'frontend-developer' },
      { label: '안드로이드 개발자', value: 'android-developer' },
      { label: 'iOS 개발자', value: 'ios-developer' },
    ],
  },
]
🤖 Prompt for AI Agents
In src/shared/constants/filters.ts around lines 10 to 32, the PART_OPTIONS array
mixes value formats (English 'all' and Korean strings) which will break
filtering; change the option.value fields to a consistent canonical format (for
example, kebab-case English identifiers like "all", "pm-po", "product-designer",
"backend-developer", etc.) while keeping the label fields as-is for display,
then update any consumers (filters, API payloads, tests) to use the new
canonical values or add a simple mapping layer to translate legacy Korean values
to the new identifiers.

Comment on lines 37 to 41
@utility typo-body-3-2-m {
font-size: var(--text-body-3);
line-height: 1.5;
font-weight: 500;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate utility definition: typo-body-3-2-m is defined twice.

This utility is defined at lines 37-41 and again at lines 174-178 with identical values. This creates redundancy and potential maintenance issues.

Remove the duplicate definition at lines 174-178 since it's redundant:

-@utility typo-body-3-2-m {
-  font-size: var(--text-body-3);
-  line-height: 1.5;
-  font-weight: 500;
-}

Also applies to: 174-178

🤖 Prompt for AI Agents
In src/styles/typography.css around lines 37-41 and 174-178, the utility
typo-body-3-2-m is defined twice; remove the duplicate definition at lines
174-178 so only the original at 37-41 remains, and then run a quick search to
ensure no references expect the second definition before committing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/components/molecules/card/Card.tsx (2)

68-77: Fix invalid Tailwind arbitrary-value syntax for CSS vars (gap-(--card-gap) p-(--card-pad)).
This won’t apply spacing; Tailwind needs bracket syntax for arbitrary values.

-          'gap-(--card-gap) p-(--card-pad) rounded-[12px]',
+          'gap-[var(--card-gap)] p-[var(--card-pad)] rounded-[12px]',

140-167: Prevent next/image runtime error: src can become null (non-null assertions on Line 166).
With logoUrl falsy and fallbackSrc={null}, src={null} can be passed to Image.

 export interface CardImageProps extends React.HTMLAttributes<HTMLDivElement> {
   logoUrl?: string | null
   fallbackSrc?: string | null
@@
 export function CardImage({
   logoUrl,
   fallbackSrc = '/images/default.svg',
@@
 }: CardImageProps) {
@@
   const [failed, setFailed] = React.useState(false)
-  const src = failed || !logoUrl ? fallbackSrc! : logoUrl!
+  const src = failed || !logoUrl ? (fallbackSrc ?? '/images/default.svg') : logoUrl

If you want to be stricter, consider making fallbackSrc?: string (non-nullable) and/or guarding when !src to avoid rendering Image at all.

🧹 Nitpick comments (1)
src/components/molecules/card/Card.tsx (1)

41-85: Consider CVA for orientation/interactive variants (guideline alignment).
You already have clear variants (orientation, interactive, border) that CVA can model cleanly.

Also applies to: 151-209, 297-329

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85dd5e8 and 815f309.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • src/components/atoms/Button/button.tsx (2 hunks)
  • src/components/atoms/checkItem/button.tsx (1 hunks)
  • src/components/atoms/tag/Tag.tsx (1 hunks)
  • src/components/molecules/card/Card.tsx (4 hunks)
  • src/components/molecules/card/MobileCard.tsx (1 hunks)
  • src/components/molecules/standardReview/StandardReview.tsx (8 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/components/atoms/checkItem/button.tsx
  • src/components/atoms/tag/Tag.tsx
  • src/components/molecules/standardReview/StandardReview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/molecules/card/MobileCard.tsx
🧰 Additional context used
📓 Path-based instructions (2)
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/components/**/*.{ts,tsx}: Use Class Variance Authority (CVA) for type-safe styling variants in component definitions
Use Tailwind CSS with cn() utility (clsx + tailwind-merge) from @/shared/utils/cn for conditional styling
Use React Hook Form + Zod validation with @hookform/resolvers for form handling
Use Radix UI primitives wrapped via shadcn/ui (New York style) for UI components
Organize components using atomic design: atoms/ -> molecules/ -> (pages)/

Files:

  • src/components/atoms/Button/button.tsx
  • src/components/molecules/card/Card.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: Use centralized path constants from src/shared/configs/appPath.ts for routing
Use TanStack React Query with Axios for data fetching
Use TypeScript for type safety across all source files

Files:

  • src/components/atoms/Button/button.tsx
  • src/components/molecules/card/Card.tsx
🧠 Learnings (2)
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Use Radix UI primitives wrapped via shadcn/ui (New York style) for UI components

Applied to files:

  • src/components/atoms/Button/button.tsx
📚 Learning: 2025-12-09T03:41:08.364Z
Learnt from: CR
Repo: dnd-side-project/dnd-13th-7-frontend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-09T03:41:08.364Z
Learning: Applies to src/components/**/*.{ts,tsx} : Use Class Variance Authority (CVA) for type-safe styling variants in component definitions

Applied to files:

  • src/components/atoms/Button/button.tsx
  • src/components/molecules/card/Card.tsx
🔇 Additional comments (2)
src/components/atoms/Button/button.tsx (2)

2-2: LGTM! Standard Radix Slot pattern implementation.

The addition of Radix Slot support with the asChild prop follows shadcn/ui conventions and aligns with the coding guidelines to use Radix UI primitives. This enables composition by allowing the Button to render as any element via Slot.

As per coding guidelines and retrieved learnings: This correctly uses Radix UI primitives wrapped via shadcn/ui style.

Also applies to: 63-67


69-92: LGTM! Correct asChild implementation.

The component correctly:

  • Switches between Slot and button based on asChild prop
  • Omits type attribute when rendering as Slot (line 86) since Slot delegates to its child
  • Preserves all styling via cn(buttonVariants(...))
  • Maintains ref forwarding

This follows the standard shadcn/ui Button pattern for enabling composition with Radix components.

Comment on lines 106 to 120
export function CardDescription({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn(
'desktop:typo-caption-m phone:typo-body-4-m text-grey-color-2 text-ellipsis overflow-hidden line-clamp-2 flex-col mt-1',
className,
)}
{...props}
/>
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove flex-col (no-op without flex) or add flex to match intent.
Right now flex-col does nothing in CardDescription, CardMeta, CardTitle.

-        'desktop:typo-caption-m phone:typo-body-4-m text-grey-color-2 text-ellipsis overflow-hidden line-clamp-2 flex-col mt-1',
+        'desktop:typo-caption-m phone:typo-body-4-m text-grey-color-2 text-ellipsis overflow-hidden line-clamp-2 mt-1',
@@
-        'typo-caption-m text-main-color-1 flex-col desktop:mt-2 phone:mt-1',
+        'typo-caption-m text-main-color-1 desktop:mt-2 phone:mt-1',
@@
-        'desktop:typo-body-2-sb phone:typo-body-3-b text-black-color text-ellipsis overflow-hidden line-clamp-1 flex-col',
+        'desktop:typo-body-2-sb phone:typo-body-3-b text-black-color text-ellipsis overflow-hidden line-clamp-1',

Also applies to: 211-242, 274-288

🤖 Prompt for AI Agents
In src/components/molecules/card/Card.tsx around lines 106-120 (and similarly
lines 211-242, 274-288), the className includes "flex-col" which is a no-op
unless "flex" is present; either remove "flex-col" or add "flex" to the class
list to match the intended layout. Update CardDescription (and
CardMeta/CardTitle in the other ranges) to either drop "flex-col" or change it
to "flex flex-col" depending on whether the element should be a flex container,
ensuring consistency across the three locations.

@yura0302 yura0302 merged commit 0fc7c70 into main Dec 13, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants